2018-09-04

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

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まで)

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

モジュールはカレントディレクトリ、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 を利用する主な箇所は、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

循環インポート

複数のモジュールがお互いのモジュールを必要とし合っている状態を 循環インポート や循環参照と言います。

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のモジュールって結構かんたんですよね。