Pythonのモジュールについて復習&まとめてみました。
基本
Pythonでは拡張子が「py,pyc,pyo」のファイルをモジュールとして読み込むことができます。
モジュールはimportによって読み込まれた時点で実行され、モジュールオブジェクトとしてアクセスできるようになります。
Pythonにおけるグローバルスコープはモジュールに限定されており、 意図的に書き換えない限り実行されたコードが他のモジュールの値に影響を及ぼすことはありません。
また、グローバルスコープに宣言されたオブジェクトはモジュールオブジェクトの属性としてアクセスすることができます。 これはグローバル変数やモジュール変数と言ったりします。
このあたりについて詳しく知りたい方は以下をを参照ください。
Pythonのスコープについてhttps://note.crohaco.net/2017/python-scope/
Pythonでは名前空間という言葉はあまり聞きませんが、モジュールはその一端を担っていると言えるでしょう。
__name__
という変数にモジュールパスが入り、名前空間を識別できます。
- info
- 対話モードなどでこの変数を参照すると
'__main__'
が入っていますよね。 if __name__ == '__main__':
という記述をよく見ますが、 これは「スクリプトファイルとして実行した場合」つまり「モジュールとして実行しなかった場合」を判定します。- 主に、テストコードやコマンドを書いたりするのに使われます。
- 対話モードなどでこの変数を参照すると
ここで mod1.py, mod2.py を用意します。
- mod1.py
- mod2.py
test = 1
test = 2 raise Exception('some exception')
>>> 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
- info
- あまり必要ありませんが少しだけバイトコンパイルについて話しておきます。
- pycは初回import時にバイトコンパイルされ、同じディレクトリに生成されます。
- モジュールファイルの更新時刻に変更がなければ次回以降はpycが読み込まれます。
- 勘違いされることが多いのですが、バイトコンパイルにより高速化されるのは読込速度であり実行速度ではありません。
- バイトコンパイルされたファイルが残っていることにより意図しない挙動を引き起こすことがあるので、 リポジトリには入れないほうがいいです。
-B
オプションを指定して python を実行するか、環境変数PYTHONDONTWRITEBYTECODE=1
が指定されているとpyc
ファイルが作られません。- .pycファイルを作成させない方法メモ
パッケージ
パッケージはディレクトリを使ってモジュールを構造化する仕組みです。
パッケージはモジュールの入れ物であると同時に自身もモジュールなのです。
しかしディレクトリに処理を書くことはできないため代わりに __init__.py
に記述します。
単純に構造化するだけなら内容は空でも構いませんが __init__.py
はパッケージモジュールの実体です。
Python2系では存在しないとそのディレクトリはパッケージとして認識されませんでした。
valid_package.py
valid_package/__init__.py
valid_package/mod.py
print('valid_package.py file')
print(__name__)
print(__name__)
>>> # モジュールパスに記述したすべてのモジュールが実行される >>> 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
print(__name__) test = 1
import callee
>>> import callee callee >>> # 2回目以降はキャッシュが使用される >>> import callee >>> # 別モジュールからの読み
reload
- warning
- Python3 では ビルトインスコープの
reload
がなくなり、importlib
ライブラリに移動しました。 (impライブラリは3.3まで)
- Python3 では ビルトインスコープの
reload
はキャッシュ済みのモジュールを再読込します。
import
は文ですが reload
は関数のため引数にはモジュールオブジェクトを指定する必要があります。
変数名は関係ないということです。
キャッシュを管理しているのは sys.modules
というモジュールパス(文字列)をキーとする辞書オブジェクトです。
お察しの通り、 sys.modules
から該当モジュールパスを削除することで擬似的なreloadを実現できます。
しかし、このやり方は reload
と違いオブジェクトを新たに生成する(アドレスが変わる)ため、同一性が崩れます。
複数箇所からモジュールが参照されている場合には注意が必要です。
>>> # python2系ではインポート不要 >>> from importlib 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 >>> # リロードすると属性値が更新される >>> 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
Search path
モジュールはカレントディレクトリ、PYTHONPATHの順に検索されます。
PYTHONPATH
カレントディレクトリ以外に参照されるディレクトリを列挙したのが
PYTHONPATH
という環境変数です。
ビルトインモジュールやサードパーティモジュールはPYTHONPATHに登録されたディレクトリに配置されています。
PYTHONPATHは sys.path
で参照できます。
- datetime.py
- /tmp/datetime.py
print('[current] dummy %s' % __name__)
print('[tmp] dummy %s' % __name__)
>>> 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) >>> # /tmp/のモジュールを最優先にしてみる >>> sys.path.insert(0, '/tmp/') >>> reload(datetime) [tmp] dummy datetime >>> # PYTHONPATHを空にすると >>> sys.path = [] >>> reload(datetime) Traceback (most recent call last): File "", line 1, in ImportError: No module named datetime
from import
from は import の起点を指定する予約語です。 import文には基点からのモジュールパスを指定すれば良いため余計なキータイプを減らすことができます。
as
でも同じようなことは可能ですが、
fromで指定した基点モジュールパスにはアクセスできません(でも実行はされる)。
a/__init__.py
a/aa.py
print(__name__)
print(__name__)
>>> 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とは異なる点がいくつかあります。
モジュール変数
単体の(fromを使わない) importによって指定できるのはモジュールのみですが、 from を指定することで モジュール変数 (グローバルスコープに属する変数) を import することができます。
属性名と同じ名前のモジュールが存在する場合は属性名が優先されます。
import
というよりは定義と言ったほうが正しいかもしれません。
定義された属性は元のモジュールとの関係が断ち切られるため reload によって値の更新がされません。
a/b/__init__.py
a/b/c/__init__.py
a/b/c/e.py
print(__name__) test = 3 _test = 4
print(__name__) d = 'd' e = 'e'
print(__name__)
>>> 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に *
を指定することでグローバルスコープに属するオブジェクトを取り込むことができます。
通称 star import とか言ったりします。
ただ、どのオブジェクトが取り込まれたかわかりにくいのでむやみに利用すべきではありません。
たとえば datetime モジュールに対して利用してみるとこんな感じになります。
>>> from datetime import * >>> date <class 'datetime.date'> >>> datetime <class 'datetime.datetime'> >>> timedelta <class 'datetime.timedelta'>
このように明示的に指定していないオブジェクトが勝手に定義されてしまうのであまり推奨されていません。 エディタの補完も効きづらくなります。
- info
- グローバルオブジェクトを star import の対象にしたくない場合は以下のような回避策があります。
__all__ = ["target_object"]
をインポート される モジュールのグローバルに記述します。- 対象オブジェクトの変数名を
_
(アンダースコア)から開始する
- グローバルオブジェクトを star import の対象にしたくない場合は以下のような回避策があります。
私が star import を利用する主な箇所は、Django の設定ファイルを環境ごとに分割する場合です。 共通する設定をbase.pyに記述し、環境ごとのファイルで base.py を star import します。
- settings/base.py
- settings/production.py
- settings/local.py
import os import hashlib # Build paths inside the project like this: os.path.join(BASE_DIR, ...) BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) PROJ_DIR = os.path.dirname(BASE_DIR) # Quick-start development settings - unsuitable for production # See https://docs.djangoproject.com/en/2.0/howto/deployment/checklist/ # SECURITY WARNING: keep the secret key used in production secret! SECRET_KEY = '8+m$zw1^mk&zwx0bmqe#qlba8w*k9m0u-uh=bg$t@^d2)^#@$%' # SECURITY WARNING: don't run with debug turned on in production! DEBUG = False ALLOWED_HOSTS = ['*'] AUTH_USER_MODEL = 'accounts.User' # Application definition INSTALLED_APPS = [ 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', # 3rd party 'rest_framework', 'rest_framework.authtoken', 'djoser', 'django_filters', # my applications 'accounts', 'tasks', 'share', ] # 後略
from .base import * # NOQA DEBUG = False INSTALLED_APPS += [ "storages" ] DATABASES = { 'default': { 'ENGINE': 'django.db.backends.postgresql', 'NAME': 'honban', 'USER': 'honbanyu-za', 'PASSWORD': 'honbanpasuwa-do', 'HOST': 'honban-db.example.com', 'PORT': '5432', 'ATOMIC_REQUESTS': True, } }
from .base import * # NOQA DEBUG = True INSTALLED_APPS += [ 'debug_toolbar', ] MIDDLEWARE += [ 'debug_toolbar.middleware.DebugToolbarMiddleware', ] DEBUG_TOOLBAR_CONFIG = { 'SHOW_TOOLBAR_CALLBACK': 'settings.debug_toolbar.allow_all', } DATABASES = { 'default': { 'ENGINE': 'django.db.backends.postgresql', 'NAME': 'postgres', 'USER': 'postgres', 'PASSWORD': '', 'HOST': 'db', 'PORT': '5432', # 'ATOMIC_REQUESTS': True, } }
相対インポート
相対インポートは同一パッケージを基点としたモジュールのインポートです。
相対インポートの2,3系に共通するメリットとして上位パッケージを参照できるという点です。
1つ上のパッケージに遡るときは ..
、2つ上の時は ...
と言った具合に
.
の数を増やすことで複数のパッケージを遡ることができます。
ただし、カレントディレクトリ以上を参照することはできません。
- a/b/back.py
# coding: utf-8 # a.__init__.testを参照する(1) from .. import test print(test) # カレントディレクトリ以上は参照できないためエラーとなる from ... import test
- console
>>> 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
- info
Python2系と3系のインポートルールの違いについて(興味のない方は読み飛ばしてください)
Python2系では同一パッケージのモジュールが最優先されるため、 同一パッケージを対象としたインポートではメリットがありませんが、3系では同一パッケージのモジュールは読み込まれないため相対インポートを利用する必要があります。
互換性を意識するのであれば2系でも明示的に相対インポート指定をするべきでしょう。
callee.py
a/b/callee.py
a/b/caller.py
test = __name__
test = __name__
from callee import test print(test) from .callee import test print(test)
- python2 console
- python3 console
>>> import a.b.caller a a.b a.b.callee a.b.callee
# python3 console >>> import a.b.caller a a.b callee a.b.callee
上記の結果を見てわかるとおり、同一パッケージに重複した名前のモジュールが存在すると python2では意図したモジュールをimportできないという問題があります。
Python3系以降ではデフォルトで同一パッケージは参照されないため問題は発生しません。
この参照ルールをPython2系に適用したいとき
absolute_import
を使います。下記をa/b/caller.pyの先頭に記述
from __future__ import absolute_import
再度Python2系にてimportしてみる
>>> # python2 console >>> import a.b.caller a a.b callee a.b.callee
3系と同じ結果となりましたね。
from import の構文規制
from を使った import には モジュールパス を指定できません。
簡単に言うとimport以降には .
を含めることができないということです。
>>> from a.b import c.d File "", line 1 from a.b import c.d ^ SyntaxError: invalid syntax
通常モジュールはimport文によって読み込まれますが、読み込むモジュールを動的に変えたいこともあります。 対象のモジュールが少ないうちはif文だけで何とかなりますがあまり現実的ではありません。
__import__
関数はインポートしたいモジュールのパスを文字列として受け取るため、柔軟な指定ができます。
読み込んだモジュールオブジェクトを返却します。自動的に宣言されないため注意しましょう。
>>> a = 'sys' # モジュールオブジェクトが返却される >>> sys1 = __import__(a) # importしたのと同じ >>> import sys as sys2 >>> print(sys1 is sys2) True
- info
- python3 では importlib.import_module を使うことが推奨されています。
循環インポート
複数のモジュールがお互いのモジュールを必要とし合っている状態を
循環インポート
や循環参照と言います。
Golangなどそもそもそのような構造を許していない言語もあります。 Pythonの場合は、読み込んだタイミングで必要としている属性が宣言されていなければエラーとなります。
- x.py
- y.py
print(0) test1 = 1 import y test2 = 2
import x print(x.test1) # test1は宣言されているが print(x.test2) # test2は未宣言のためAttributeError
>>> 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: partially initialized module 'x' has no attribute 'test2' (most likely due to a circular import)
3系以降のエラーでは循環インポートでは?と言ってくれるみたいでエラーメッセージが親切ですね。
(2系のときは AttributeError: 'module' object has no attribute 'test2'
でした)
さて、対応方法としてはインポートが1方向になるようにファイルを整理することが望ましいです。 どうしてもそれが適わない場合には、インポートを必要とする関数の中で行うことで回避できる場合があります。
先程の y.py
の内容を 関数に分離して yy.py
に書き直してみます。
- xx.py
- yy.py
print(0) test1 = 1 import yy test2 = 2
def main(): import xx print(xx.test1) print(xx.test2)
>>> import xx 0 >>> import yy >>> yy.main() 1 2 >>>
今回はエラーになりませんでした。
インポートを関数内で行う(=グローバルでインポートしない)ことで 相互に参照するタイミングがずれインポートが解決できるというわけですね。
ただし、関数内でインポートしたライブラリは関数スコープに属するので他のモジュールから参照できない点にご注意ください。
以上です。Pythonのモジュールって結構かんたんですよね。