Pythonのモジュールについてまとめてみたよ

理解が曖昧だと感じていたためPythonのモジュールについて復習&まとめてみました。当たり前のことも多いですが、中には知らないこともあるかもしれませんよ!
※この記事ではバージョンの指定がなければ2系のことを指します

基本

Pythonでは拡張子が「py,pyc,pyo」のファイルをモジュールとして読み込むことができます。pyc(pyo)は初回import時にバイトコンパイルされ、同じディレクトリに生成されます。モジュールファイルの更新時刻に変更がなければ次回以降はpyc(pyo)が読み込まれます。バイトコンパイルにより高速化されるのは読込速度であり実行速度ではありません。

モジュールはimportによって読み込まれた時点で実行され、モジュールオブジェクトとしてアクセスできるようになります。ここが他言語のincludeとは違いますね。

Pythonにおけるグローバルスコープはモジュールに限定されており、意図的に書き換えない限り実行されたコードが他のモジュールの値に影響を及ぼすことはありません。また、グローバルスコープに宣言されたオブジェクトはモジュールオブジェクトの属性としてアクセスすることができます。これをモジュール変数といいます。

Pythonでは名前空間という言葉はあまり聞きませんが、モジュールがその役割果たしていると言えるでしょう。「__name__」という変数にモジュールパスが入り、名前空間を識別できます。
対話モードなどでこの変数を参照すると「’__main__’」が入っていますよね。「if __name__ == ‘__main__':」という記述をよく見ますが、これは「スクリプトファイルとして実行した場合」つまり「モジュールとして実行しなかった場合」を判定します。
主に、テストコードやコマンドを書いたりするのに使われます。
この記事で「__main__」としている箇所はPythonコンソールで実行していることを表しています。

mod1 mod2
1
test = 1
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はパッケージモジュールの実体であるため、存在しないとそのディレクトリはパッケージとして認識されません(Python2系以下)。

valid_package.py valid_package/__init__.py valid_package/mod.py
1
print('valid_package.py file')
1
print(__name__)
1
print(__name__)
__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
1
import callee
__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
 
>>> # このタイミングでcallee.pyファイルの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]
>>> # 通常のdatetimeが読まれるようになった
>>> 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
print(__name__)
1
print(__name__)
__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
3
print(__name__)
test = 3
_test = 4
1
2
3
print(__name__)
d = 'd'
e = 'e'
1
print(__name__)
__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

fromによる属性のimportは、モジュールの属性を代入分によって再定義していると考えるとわかりやすいかもしれません。はじぱいにも同じような説明がありますが、「from mod import name1, name2」は以下と等価です。

>>> import mod
>>> name1 = mod.name1
>>> name2 = mod.name2
>>> del mod

また、「import *」とすることで変数名が”_”で始まらないすべての属性を取り込むこともできます。ただ、どの属性が取り込まれたかわかりにくいのでむやみに利用すべきではありません。

>>> from a.b import *
>>> print(test)
3
>>> 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
test = __name__
1
test = __name__
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'