Python3のimportパターン13個

Pythonで意味のある単位にファイルを分割してWebアプリケーションを作成したい。
どうやら、Pythonにはあるファイルから他のファイルを読み込む際のパターンが複数あるようなので、まとめておく。

Pythonにexportはない

Pythonにおいてあるファイルから他のファイルを読み込む場合、関数やオブジェクトを提供するファイルにexportのような記述をする必要はない
したがって、読み込む方法だけ確認しておけば、ファイルを意味のある単位に分割して開発していける。

読み込みのパターン

MECE(漏れなくダブりなく)じゃないけど、読み込みのパターンはおおよそ以下の通り。

  1. import モジュール
  2. import モジュール as モジュールの別名
  3. from モジュール import 変数、関数、クラス、モジュール
  4. from モジュール import 変数、関数、クラス、モジュール as 別名
  5. from モジュール import *
  6. from パッケージ.モジュール import 変数、関数、クラス、モジュール (子ディレクトリから読み込む)
  7. from . import 変数、関数、クラス、モジュール (相対importで同じディレクトリから読み込む)
  8. from .. import 変数、関数、クラス、モジュール (相対importで親ディレクトリから読み込む)
  9. from ..パッケージ import 変数、関数、クラス、モジュール (相対importで兄弟ディレクトリから読み込む)
  10. import 標準ライブラリ(sysmathdatetimeなど)
  11. import サードパーティライブラリ(flasknumpyなど)
  12. sys.pathによる検索モジュールの追加 + from 親モジュール (attempted relative import beyond top-level packageの対応)
  13. 環境変数PYTHONPATHでモジュール検索パスを追加

用語整理

モジュールとは

Pythonでは変数や関数、クラスなどを書いたファイルをモジュールと呼ぶ。

パッケージとは

Pythonにおけるパッケージはほぼディレクトリ。(厳密には違うらしい)
パッケージは特別なモジュールの一種である。パッケージは通常のモジュールとは違い__path__属性を持つ。 パッケージは階層構造をなし、複数のモジュールおよび、サブパッケージをもつことができる。
パッケージの種類は「通常のパッケージ」と「名前空間パッケージ」の2つである。 「通常のパッケージ」はディレクトリに__init__.pyファイルがある。一方で、「名前空間パッケージ」のディレクトリには__init__.pyがない。なお、「名前空間パッケージ」はPython 3.3から導入された概念である。

標準ライブラリとは

Pythonの標準ライブラリには組み込み関数やデータ型、システム、ファイルIOなど汎用的なモジュールが含まれる。
通常のPythonインストーラによりPythonをインストールすると、標準ライブラリも一緒にインストールされる。

サードパーティライブラリとは

この記事ではFlaskやDjangoのようなフレームワークやNumPyのような計算を行うためのパッケージなど第三者によってPyPIに公開されているパッケージのことサードパーティライブラリと呼ぶ。

1. import モジュール

import モジュールの形式で変数、関数、クラスを読み込む。

ディレクトリ構成

.
├── hello.py
└── main.py

hello.py 読み込まれるファイル

val = 100


def hello():
    return 'hello'


class Cls:
    def __init__(self, prop):
        self.prop = prop

main.py 読み込むファイル

import hello

# 変数のimport
print(hello.val)
# 関数のimport
print(hello.hello())
# クラスのimport
print(vars(hello.Cls('こんにちは')))

main.pyを実行すると変数、関数、クラスが読み込まれていることがわかる。

$ main.py
100
hello
{'prop': 'こんにちは'}

2. import モジュール as モジュールの別名

import モジュール as モジュールの別名の形式でファイル内でモジュールの別名.変数モジュールの別名.関数モジュールの別名.クラス`のように別名でアクセスできる。

以下の内容で1. import モジュールと同じ出力結果を得られる。
main.py

import hello as he
print(he.val)
print(he.hello())
print(vars(he.Cls('こんにちは')))

3. from モジュール import 変数、関数、クラス、モジュール

変数、関数、クラスだけでなくモジュールもimportできることを確認するため、モジュールを追加する。

ディレクトリ構成

.
├── goodnight.py
├── hello.py
└── main.py

goodnight.py

gn = 'Good night!'

hello.py

import goodnight
# 以下略

毎回モジュール.変数モジュール.関数のようにすることなく、直接変数や関数、クラスを使うことができる。
また、モジュールも読み込むことができている。

main.py

from hello import val
from hello import hello
from hello import Cls
from hello import goodnight

print(val)
print(hello())
print(vars(Cls('こんにちは')))
print(goodnight)
$ main.py
100
hello
{'prop': 'こんにちは'}
<module 'goodnight' from 'goodnight.pyへのパス'>

4. from モジュール import 変数、関数、クラス、モジュール as 別名

3と同じ構成で以下のように別名をつけても、別名をつけなかった時と同じ出力になる。
main.py

from hello import val as v
from hello import hello as h
from hello import Cls as C
from hello import goodnight as g

print(v)
print(h())
print(vars(C('こんにちは')))
print(g)

5. from モジュール import *

3と同じ構成でfrom モジュール import *形式で読み込むことでfromに指定したモジュールの変数や関数など、まとめてモジュール名なしでアクセスできるようになる。
なお、暗黙的にすべてのものがimportされてしまうため、*(アスタリスク)による読み込みは推奨されておらず、明示的にimportした方がよい。

main.py

from hello import *

print(val)
print(hello())
print(vars(Cls('こんにちは')))
print(goodnight)

6. from パッケージ.モジュール import 変数、関数、クラス、モジュール (子ディレクトリから読み込む)

main.pyから1階層下のモジュールを読み込む。
from パッケージ.モジュール import モジュール(モジュール以外も同様に読み込める)で読み込める。

ディレクトリ構成(6と同じ)

.
├── __init__.py
├── main.py
└── package
    ├── child1.py
    └── child2.py

main.py

from package.child1 import child2
print('main:' , child2)

実行結果

$ python main.py 
<module 'package.child2' from 'パス/package/child2.py'>
main: <module 'package.child2' from 'パス/package/child2.py'>

7. from . import 変数、関数、クラス、モジュール (相対importで同じディレクトリから読み込む)

相対importはパッケージの中でしか使えないことに注意する。

ためしにトップ階層で同じディレクトリのモジュールを相対import形式のfrom . import モジュールで指定してみる。

ディレクトリ構成

.
├── __init__.py
├── hello.py
└── main.py

main.py

from . import hello
print(hello)

実行結果

$ python main.py 
Traceback (most recent call last):
  File "main.py", line 1, in <module>
    from . import hello
ImportError: cannot import name 'hello' from '__main__' (main.py)

これはエラーで動かない。
そこで、次のようにディレクトリ(packageという名前)を作成し、トップ階層ではないパッケージ内のモジュール(child1.py)から同階層のモジュール(child2.py)の読み込みを試した。

ディレクトリ構成

.
├── __init__.py
└── package
    ├── child1.py
    └── child2.py

main.py

from package import child1
print(child1)

child1.py

from . import child2
print(child2)
ch1 = 'data ch1'

child2.py

ch2 = 'data ch2'

これは想定通りchild1.pyから同階層のchild2.pyを読み込むことができる。

実行結果

$ python main.py
<module 'package.child2' from 'パス/package/child2.py'>
<module 'package.child1' from 'パス/package/child1.py'>

8. from .. import 変数、関数、クラス、モジュール (相対importで親ディレクトリから読み込む)

そこで、packageディレクトリにさらにsubpackageディレクトリを作成する。そのsubpackageディレクトリにあるgrandchild1.pyから親ディレクトリのpackageにあるchild1.pyを読み込んでみる。

ディレクトリ構成

.
├── __init__.py
├── main.py
├── package
│   ├── child1.py
│   └── subpackage
│       └── grandchild1.py

main.py

from package.subpackage import grandchild1
print(grandchild1)

child1.py

child1_val = 200

grandchild1.py

from .. import child1
print(child1)
print(child1.child1_val)

subpackageパッケージにあるgrandchild1.pyから、親パッケージにあるchild1.pyが読み込めた。

実行結果

$ python main.py 
<module 'package.child1' from 'パス/package/child1.py'>
200
<module 'package.subpackage.grandchild1' from 'パス/package/subpackage/grandchild1.py'>

9. from ..パッケージ import 変数、関数、クラス、モジュール (相対importで兄弟ディレクトリから読み込む)

孫のモジュールgrandchild1.pyから1階層上がりpackageパッケージにあるsubpackage2パッケージのsub2_grandchild1モジュールを読み込む。

ディレクトリ構成

.
├── main.py
├── package
│   ├── subpackage
│   │   └── grandchild1.py
│   └── subpackage2
│       └── sub2_grandchild1.py

main.py

from package.subpackage import grandchild1
print(grandchild1)

grandchild1.py

from ..subpackage2 import sub2_grandchild1
print(sub2_grandchild1)
print(sub2_grandchild1.val)

sub2_grandchild1.py

val = 300

実行結果

$ python main.py 
<module 'package.subpackage2.sub2_grandchild1' from 'パス/package/subpackage2/sub2_grandchild1.py'>
300
<module 'package.subpackage.grandchild1' from 'パス/package/subpackage/grandchild1.py'>

10. import 標準ライブラリ(sysmathdatetimeなど)

今まで見てきたように、import 標準ライブラリで読み込んだり、import 標準ライブラリ as 別名で別名をつけたり、from 標準ライブラリ import 変数、関数、クラス標準ライブラリ.変数、関数、クラスとせず直接、変数や関数にアクセスできるようになる。

import sys
import sys as ss
from sys import path

11. import サードパーティライブラリ(flasknumpyなど)

自作のモジュールや標準ライブラリと同様、特に違いはない。
下記のいずれも可能。

import flask
import flask as fl
from flask import Flask

12. sys.pathによる検索モジュールの追加 + from 親モジュール (attempted relative import beyond top-level packageの対応)

子ディレクトリにあるchild1.pyから親ディレクトリにあるparent.pyを読み込んでみる。

ディレクトリ構成

.
├── __init__.py
├── main.py
├── parent.py
└── package
    └── child1.py

parent.py

val = 100

child1.py

from .. import parent
print(parent)
print(parent.val)

main.py

from package import child1
print(child1)

実行結果

$ python main.py
Traceback (most recent call last):
  File "main.py", line 1, in <module>
    from package import child1
  File "パス/package/child1.py", line 8, in <module>
    from .. import parent
ValueError: attempted relative import beyond top-level package

これはValueError: attempted relative import beyond top-level packageというエラーになる。
一番上の階層のパッケージでは相対パスが使えないようだ。

この場合、対応方法は2つある。

1つめは実行するモジュールの実行ディレクトリに依存する方法である。

実行ディレクトリに依存する親モジュールの読み込み方

Pythonはモジュールの検索対象をsys.pathにリスト形式で保持している。
このリストには実行したファイルのパスが格納されている。
次のようにすることでsys.pathに格納されているパスの一覧を確認できる。
sys.pathの内容をリストをきれいに表示できるpprintモジュールで表示する。

main.py

import sys
from pprint import pprint
pprint(sys.path)

実行結果を見ると、一番上に実行したmain.pyのパスが格納されている。
つまり、実行したファイルのディレクトリにあるモジュールはimport モジュールで読み込むことができる。

~/git/python-dependencies/top $ python main.py 
['/Users/nancy/git/python-dependencies/top',
 '/Users/nancy/.pyenv/versions/3.7.3/lib/python37.zip',
 '/Users/nancy/.pyenv/versions/3.7.3/lib/python3.7',
 '/Users/nancy/.pyenv/versions/3.7.3/lib/python3.7/lib-dynload',
 '/Users/nancy/.pyenv/versions/3.7.3/lib/python3.7/site-packages']

よって、親ディレクトリにあるモジュールであるにも関わらず、 from .. import parentと書いていた部分をimport parentとすることで親のモジュールを読み込める。

child1.py

import parent
print(parent)
print(parent.val)

実行結果は以下の通り。

~/git/python-dependencies/top $ python main.py 
<module 'parent' from '/Users/nancy/git/python-dependencies/top/parent.py'>
100
<module 'package.child1' from '/Users/nancy/git/python-dependencies/top/package/child1.py'>

実行ディレクトリに依存しない親モジュールの読み込み方

さきほどのchild1.pyを同階層のchild_main.pyから実行してみる。

ディレクトリ構成

.
├── package
│   ├── child1.py
│   └── child_main.py
└── parent.py

この場合、sys.pathに追加されるパスは次のように、親のディレクトリが含まれていない

~/git/python-dependencies/top $ python package/child_main.py 
['/Users/nancy/git/python-dependencies/top/package',
 '/Users/nancy/.pyenv/versions/3.7.3/lib/python37.zip',
 '/Users/nancy/.pyenv/versions/3.7.3/lib/python3.7',
 '/Users/nancy/.pyenv/versions/3.7.3/lib/python3.7/lib-dynload',
 '/Users/nancy/.pyenv/versions/3.7.3/lib/python3.7/site-packages']

つまり、child1.pyで書いた親ディレクトリにあるモジュールの読み込みは、実行するモジュールのディレクトリに依存しているため、モジュールを読み込めない。
そこで、親のディレクトリパスを明示的にsys.pathに追加することで、実行するモジュールのディレクトリに依存しないようにする。
親のディレクトリパスを追加する方法は2つある。 os.pathを使ってos.path.join(os.path.dirname(__file__), '..')とする方法と、
pathlib.Pathを使ってstr(pathlib.Path(__file__).parent.parent.resolve())とする方法である。
いずれの方法でも親のディレクトリをモジュールの検索パスに追加できればよい。
なお、pathlibモジュールはPython3.4から導入されている。

child1.py

 
 
 
 
 




import sys
import pathlib
parent_dir = str(pathlib.Path(__file__).parent.parent.resolve())
sys.path.append(parent_dir)
import parent

print(parent)
print(parent.val)

実行結果

~/git/python-dependencies/top $ python package/child_main.py
<module 'parent' from '/Users/nancy/git/python-dependencies/top/parent.py'>
100
<module 'child1' from '/Users/nancy/git/python-dependencies/top/package/child1.py'>

13. 環境変数PYTHONPATHでモジュール検索パスを追加

ディレクトリパス

.
├── main.py
└── python_path
    └── special.py

以下のようにpython_pathパッケージのspecialモジュールを直接読み込もうとする。
main.py

import special
print(special)
print(special.val)

special.py

val=500

実行すると、specialが見つからずエラーになる。

$ python main.py 
Traceback (most recent call last):
  File "main.py", line 1, in <module>
    import special
ModuleNotFoundError: No module named 'special'

しかし、PYTHONPATHにパスを追加すると、エラーにならず読み込めるようになる。

$ export PYTHONPATH=$PYTHONPATH:./python_path
$ python main.py 
<module 'special' from '/Users/nancy/git/python-dependencies/top/python_path/special.py'>
500

参考

https://docs.python.org/ja/3/reference/import.html
https://note.nkmk.me/python-import-module-search-path/
https://chaika.hatenablog.com/entry/2018/08/24/090000