理解が曖昧だと感じていたためPythonのモジュールについて復習&まとめてみました。当たり前のことも多いですが、中には知らないこともあるかもしれませんよ!
基本
Pythonでは拡張子が「py,pyc,pyo」のファイルをモジュールとして読み込むことができます。pyc(pyo)は初回import時にバイトコンパイルされ、同じディレクトリに生成されます。モジュールファイルの更新時刻に変更がなければ次回以降はpyc(pyo)が読み込まれます。バイトコンパイルにより高速化されるのは読込速度であり実行速度ではありません。
モジュールは読み込まれた時点で実行され、モジュールオブジェクトとしてアクセスできるようになります。Pythonにおけるグローバルスコープはモジュールに限定されており、意図的に書き換えない限り実行されたコードが他のモジュールの値に影響を及ぼすことはありません。また、グローバルスコープに宣言されたオブジェクトはモジュールオブジェクトの属性としてアクセスすることができます。これをモジュール変数といいます。
| mod1 |
mod2 |
|
|
1
2
| test = 2
raise Exception('some exception') |
|
| __main__ |
>>> test = 0
>>> import mod1
>>> # 他のモジュール変数には影響されない
>>> print(test)
0
>>> # mod1のモジュール変数testはmod1の属性となる
>>> print(mod1.test)
1
>>> import mod2
Traceback (most recent call last):
File "", line 1, in
File "mod2.py", line 2, in
raise Exception('some exception')
Exception: some exception
>>> # 最後まで無事に実行されなければ変数として宣言されない
>>> mod2
Traceback (most recent call last):
File "", line 1, in
NameError: name 'mod2' is not defined |
|
パッケージ
ディレクトリを使ってモジュールを構造化する仕組みです。パッケージはモジュールの入れ物であると同時に自身もモジュールなのです。しかしディレクトリに処理を書くことはできないため代わりに__init__.pyに処理を記述します。単純に構造化するだけなら内容は空でも構いませんが__init__.pyはパッケージモジュールの実体であるため、存在しないとそのディレクトリはパッケージとして認識されません。
| valid_package.py |
valid_package/__init__.py |
valid_package/mod.py |
1
| print('valid_package.py file') |
|
|
|
| __main__ |
>>> # モジュールパスに記述したすべてのモジュールが実行される
>>> import valid_package.mod
valid_package
valid_package.mod
>>> # 単一のモジュールよりもパッケージが優先される
>>> import valid_package
>>> # __init__.pyが存在しないディレクトリ(invalid_package)はimportできない
>>> import invalid_package
Traceback (most recent call last):
File "", line 1, in
ImportError: No module named invalid_package |
|
シングルトン?
importはモジュールサーチパスから該当のモジュールを探し出し、実行するという比較的重い処理です。そのためimportしたモジュールはキャッシュされプロセス内で共有します。2回目以降のimportではキャッシュされたモジュールオブジェクトが参照されるというわけです。
| callee.py |
caller.py |
1
2
| print(__name__)
test = 1 |
|
|
| __main__ |
>>> import callee
callee
>>> # 2回目以降はキャッシュが使用される
>>> import callee
>>> # 別モジュールからの読み込みでもキャッシュが使用される
>>> import caller |
|
reload
reloadを使うことでキャッシュ済みのモジュールを再読込することができます。importは文ですがreloadは関数のため引数にはモジュールオブジェクトを指定する必要があります。変数名は関係ないということです。
キャッシュを管理しているのはsys.modulesというモジュールパス(文字列)をキーとする辞書オブジェクトです。お察しの通り、sys.modulesから該当モジュールパスを削除することで擬似的なreloadを実現できます。しかしimportはreloadと違いオブジェクトを新たに生成する(アドレスが変わる)ため、シングルトンの仕組みが崩れます。複数箇所からモジュールが参照されている場合には注意が必要です。
>>> # 現状では以下のモジュールが読み込まれている(callee.pyとcaller.pyも)
>>> import sys
>>> sys.modules.keys()
['copy_reg', 'encodings', 'site', '__builtin__', '__main__', 'encodings.encodings', 'abc', 'posixpath', '_weakrefset', 'errno', 'encodings.codecs', '_abcoll', 'types', '_codecs', '_warnings', 'genericpath', 'stat', 'zipimport', 'encodings.__builtin__', 'warnings', 'UserDict', 'encodings.utf_8', 'sys', 'codecs', 'readline', 'os.path', 'sitecustomize', 'callee', 'signal', 'caller', 'linecache', 'posix', 'encodings.aliases', 'exceptions', 'os', '_weakref']
>>> print(callee.test)
1
>>> # このタイミングでtestの値を更新(2)
>>> reload(callee)
callee
<module 'callee' from 'callee.pyc'>
>>> # リロードすると属性値が更新される
>>> print(callee.test)
2
>>> print(caller.callee.test)
2
>>> # モジュールのアドレスは同じ
>>> callee is caller.callee
True
>>> # 再度testの値を更新(3)
>>> # 擬似的なreload
>>> del sys.modules['callee']
>>> import callee
callee
>>> print(callee.test)
3
>>> print(caller.callee.test)
2
>>> # モジュールのアドレスも更新されてしまう
>>> callee is caller.callee
False |
モジュール検索パス
モジュールは同一パッケージ、PYTHONPATHの順に検索されます。
PYTHONPATH
同一パッケージに以外に参照されるディレクトリを列挙したのがPYTHONPATHです。ビルトインモジュールやサードパーティモジュールはPYTHONPATHに登録されたディレクトリに配置されています。PYTHONPATHはsys.pathで参照できます。
| datetime.py |
/tmp/datetime.py |
1
| print('[current] dummy %s' % __name__) |
|
1
| print('[tmp] dummy %s' % __name__) |
|
| __main__ |
>>> import sys
>>> sys.path
['', '/usr/local/lib/python2.7/dist-packages/pip-1.5.6-py2.7.egg', '/usr/lib/python2.7', '/usr/lib/python2.7/plat-x86_64-linux-gnu', '/usr/lib/python2.7/lib-tk', '/usr/lib/python2.7/lib-old', '/usr/lib/python2.7/lib-dynload', '/usr/local/lib/python2.7/dist-packages', '/usr/lib/python2.7/dist-packages', '/usr/lib/python2.7/dist-packages/PILcompat', '/usr/lib/python2.7/dist-packages/gtk-2.0', '/usr/lib/python2.7/dist-packages/ubuntu-sso-client']
>>> # 先頭「''(カレントディレクトリ)」のモジュールが優先して読み込まれる
>>> import datetime
[current] dummy datetime
>>> del sys.path[0]
>>> reload(datetime)
<module 'datetime' from '/usr/lib/python2.7/lib-dynload/datetime.x86_64-linux-gnu.so'>
>>> # /tmp/のモジュールを再優先にしてみる
>>> sys.path.insert(0, '/tmp/')
>>> reload(datetime)
[tmp] dummy datetime
<module 'datetime' from '/tmp/datetime.py'>
>>> # PYTHONPATHを空にすると
>>> sys.path = []
>>> reload(datetime)
Traceback (most recent call last):
File "", line 1, in
ImportError: No module named datetime |
|
from
fromはimportの基点を指定します。import文には基点からのモジュールパスを指定すれば良いため余計なキータイプを減らすことができます。「as」でも同じようなことは可能ですが、fromで指定した基点モジュールパスにはアクセスできません(でも実行はされる)。
| a/__init__.py |
a/aa.py |
1
2
3
| print(__name__)
test = 1
_test = 2 |
|
|
| __main__ |
>>> from a import aa
a
a.aa
# fromに指定したモジュールにはアクセスできない
>>> a
Traceback (most recent call last):
File "", line 1, in
NameError: name 'a' is not defined |
|
ほかにもfromを指定したimportは単体のimportとは異なる点がいくつかあります。
モジュールの属性をimportできる
単体のimportによって指定できるのはモジュールのみですが、fromを指定することでモジュールの属性をimportすることができます。属性名と同じ名前のモジュールが存在する場合は属性名が優先されます。importというよりは定義と言ったほうが正しいかもしれません。定義された属性は元のモジュールとの関係が断ち切られるためreloadによって値の更新がされません。
| a/b/__init__.py |
a/b/c/__init__.py |
a/b/c/e.py |
1
2
| print(__name__)
test = 3 |
|
1
2
3
4
5
| print(__name__)
c = 'c'
d = 'd'
e = 'e' |
|
|
| __main__ |
>>> from a.b.c import e
a
a.b
a.b.c
>>> # e.pyよりもc.e属性が優先される
>>> print(e)
e
>>> import a.b.c as c
>>> print(c.e)
e
>>> # ここでc.eの値を更新(ee)
>>> reload(c)
>>> print(c.e)
ee
# eはcを更新しても変更されない
>>> print(e)
e |
|
また、「import *」とすることで変数名が”_”で始まらないすべての属性を取り込むこともできます。ただ、どの属性が取り込まれたかわかりにくいので明確な目的なく多用するべきではありません。
>>> from a import *
>>> print(test)
1
>>> print(_test)
Traceback (most recent call last):
File "", line 1, in
NameError: name '_test' is not defined |
相対インポートができる
相対インポートは同一パッケージを基点としたモジュールのインポートです。Python2系では同一パッケージのモジュールが最優先されるため、同一パッケージを対象としたインポートではメリットがありませんが、3系では同一パッケージのモジュールは読み込まれないため相対インポートを利用する必要があります。互換性を意識するのであれば2系でも明示的に相対インポート指定をするべきでしょう。
| callee.py |
a/b/callee.py |
a/b/caller.py |
|
|
|
1
2
3
4
5
| from callee import test
print(test)
from .callee import test
print(test) |
|
| python2 console __main__ |
>>> import a.b.caller
a
a.b
a.b.callee
a.b.callee |
|
| python3 console __main__ |
>>> import a.b.caller
a
a.b
callee
a.b.callee |
|
上記の結果を見てわかるとおり、同一パッケージに重複した名前のモジュールが存在すると意図したモジュールをimportできないという問題があります。Python3系ではデフォルトで同一パッケージは参照されないため問題は発生しません。この参照ルールをPython2系に適用したいときabsolute_importを使います。
下記をa/b/caller.pyの先頭に記述
1
| from __future__ import absolute_import |
再度Python2系にてimportしてみる
>>> # python2 console
>>> import a.b.caller
a
a.b
callee
a.b.callee |
3系と同じ結果となりましたね。
相対インポートの2,3系に共通するメリットとして上位パッケージを参照できるという点です。1つ上のパッケージに遡るときは”..”、2つ上の時は”…”と言った具合に”.”の数を増やすことで複数のパッケージを遡ることができます。ただし、カレントディレクトリ以上を参照することはできません。
| a/b/back.py |
1
2
3
4
5
6
| # coding: utf-8
# a.__init__.testを参照する(1)
from .. import test
print(test)
# カレントディレクトリ以上は参照できないためエラーとなる
from ... import test |
|
| __main__ |
>>> import a.b.back
a
a.b
1
Traceback (most recent call last):
File "", line 1, in
File "a/b/back.py", line 4, in
from ... import test
ValueError: Attempted relative import beyond toplevel package |
|
相対インポートが使用できるのはパッケージ内のモジュールのみです。単体のスクリプトファイルとして実行したり、インタラクティブモードにて使用するとパッケージに所属していることにならないためエラーが発生します。
>>> from . import *
Traceback (most recent call last):
File "", line 1, in
ValueError: Attempted relative import in non-package |
モジュールパスを指定できない
簡単に言うとimport文に”.”を含めることができないということです。
>>> from a.b import c.d
File "", line 1
from a.b import c.d
^
SyntaxError: invalid syntax |
__import__
通常モジュールはimport文によって読み込まれますが、読み込むモジュールを動的に変えたいこともあります。対象のモジュールが少ないうちはif文だけで何とかなりますがあまり現実的ではありません。__import__関数はインポートしたいモジュールのパスを文字列として受け取るため、柔軟な指定ができます。読み込んだモジュールオブジェクトを返却します。自動的に宣言されないため注意しましょう。
>>> a = 'sys'
# モジュールオブジェクトが返却される
>>> sys1 = __import__(a)
# importしたのと同じ
>>> import sys as sys2
>>> print(sys1 is sys2)
True |
循環インポート
複数のモジュールがお互いのモジュールを必要とし合っている状態を循環インポートと呼びます。読み込んだタイミングで必要としている属性が宣言されていなければエラーが発生します。
| x.py |
y.py |
1
2
3
4
5
6
7
| print(0)
test1 = 1
import y
test2 = 2 |
|
1
2
3
4
| import x
print(x.test1) # test1は宣言されているが
print(x.test2) # test2は未宣言のためAttributeErrorとなる |
|
| __main__ |
>>> import x
0
1
Traceback (most recent call last):
File "", line 1, in
File "x.py", line 5, in
import y
File "y.py", line 4, in
print(x.test2)
AttributeError: 'module' object has no attribute 'test2' |
|