Pythonのモジュールインポートのしくみ

importってなにをやっているんだろう?

tags:python, tips, experiment
created:2006-03-24T12:48:35

importってなにをやっているんだろう? 超基本の「import」意外と奥が深いです。

インポートする時

hoge.pyというモジュールをインポートする時、 hoge.pycファイルがなければコンパイル後のキャッシュをhoge.pycとして保存するようです。

パッケージってなんなの?

Pythonではフォルダに「__init__.py」があれば、そのフォルダを「パッケージ」として扱えるようになっています。

論理的なツリーを構築するためにもつかえますし、このフォルダ内に複数のモジュールを入れておき、あたかも巨大なひとつのモジュールのように振舞わせることが出来ます。

「from」や「as」ってなんなの?

たくさんのモジュールを扱うプログラムになってくると、

from A import B

といった記述を見たりするでしょう。

Aにはパッケージやモジュール名を指定し、Bには取り込みたいパッケージやモジュール名またはクラスや関数名、変数名なども指定できます。

この記述により、取り込みたいオブジェクトの親を明示することが出来ます。

ただし、from側にクラスや関数名、変数名を指定する事は出来ません。

import A.B

でも親を明示した取り込みが出来ますが、Bにアクセスするのに「A.B」と毎度記述しなければなりません。

fromを使った方法なら、「B」という記述でアクセスできます。

「as」は別名を名づけするためのもので、以下のような使われ方です。

import A.B as C
from D.E.F import G.H.I as J

こうすると、複雑なパッケージに含まれるオブジェクトにシンプルな名前でアクセスできます。

別名をつけるのは混乱を招きやすいので 互換モジュールを指定する時などを除き やたらと使うのはやめたほうがいいと思います。

ファイル検索の順番

  1. 実行中のファイルと同じフォルダ
  2. カレントフォルダ
  3. 環境変数「PYTHONPATH」に列挙したフォルダ
  4. sys.pathに登録してあるフォルダ

以上の順番に、最初に条件にマッチするファイルをインポートします。

モジュールオブジェクトはシングルトン

import sys

って書きますよね。すると、「sys」という名前のモジュールオブジェクトを参照できるようになります。

このモジュールオブジェクトはシングルトンになっています!

テストしてみましょう

test.py
print 'import test!'

というファイルを作成しておいて、 以下のようにインタラクティブシェルで2回インポートしてみると良くわかります。

>>> import test
import test!
>>> import test
>>>

1度目はprint文が実行されてメッセージが表示されますが、 2度目は表示されません。

これは、モジュールオブジェクトインスタンスをPython内部で管理していて、 同じファイルパスにあるモジュールがインスタンス化されていることがわかったら 再度そのファイルをパース(コンパイル)してインスタンス化することはせずに、 すでにインスタンス化しているオブジェクトをインポート先の名前空間に対応付けします。

この仕組みのおかげで、インポートするたびに何度もパースすることなく 効率的にモジュールの利用が出来るということです。

C/C++言語では、自前のマクロで「インクルードガード」することが多々ありますが、 Pythonではimport文の内部処理でそれをやってくれているということです。

PythonやDelphiなどは、ファイルごとに名前空間が独立しています。 したがって、インポートするモジュールの内部でいくらいろんなモジュールをインポートしていても インポートする側の名前空間からそれらのモジュールを直接アクセスする事は出来ません。

また、インポートする側の 名前空間を暗黙のうちに上書きされる こともありません。

巨大なプロジェクトになると後者はすごいメリットになります。

目の前のソースひとつ眺めれば動作は誤解なく実行されるのです。

注釈

C/C++ではファイルをオープンした後にガード成立かどうかを判定します。 また、ネストされたヘッダも効果を発揮する仕様なので、 結果として瞬間的に同時に何百ものファイルを開く場合が発生する。 最近の言語ではファイルを開く前にガードするかしないかを判定します。 この差は、大きなプロジェクトに取り組むときに大きく実感させられます。

C++の巨大プロジェクトになると2GHzPCでビルドに数時間かかるものとかもあります。 同じくらいのソースを最近の言語でビルドさせたらきっとビルド時間は半減するでしょう。

リロード

以下のように「reload」を使うと再度パースしなおしさせることが出来ます。

>>> reload(test)
import test!
<module 'test' from 'test.py'>
>>>

importは文で値を返さず、reloadは関数なので値を返します。

>>> test = reload(test)
import test!
>>>

とすると「test」がリロードした 最新のモジュールオブジェクトインスタンスに更新されるでしょう。

アスタリスクのリスク

一応、以下のように記述をすることが出来るようになっています。

from A import *

このときA内の定義をすべて現時点の名前空間に上書きします。

アスタリスクを使ったインポートはC、C++でいうところのincludeのようにあたかもそのモジュールの内容をコピーして貼り付けたかのような挙動になります。

from math import *
a = sin(0)
b = cos(0)

といったように使えますので、一見便利ですよね。

しかし、あえて以下の書き方を推奨します。

from math import sin,cos
a = sin(0)
b = cos(0)

または

import math
a = math.sin(0)
b = math.cos(0)

こういった明示した書き方は複雑なコードを作成しだした時に有難みがわかります。

整頓され長く使われているモジュールを使っているうちは アスタリスクを使った書き方で問題になる事は少ないですが、 自作モジュールをインポートするのにアスタリスクを使うのは 使い捨てスクリプトでなければやめておきましょう。 (モジュールの形態にする時点で再利用を考慮しているわけだから 使い捨てではありませんね?)

アスタリスクを用いるリスクとは、暗黙のうちに名前空間が汚されるということです。

>>> dir()

とやってみると、現時点の名前空間における名前のリストが得られます。

アスタリスクを用いたインポートを多用していると、あずかり知らぬ名前がわんさかこのリストに現れることになります。

ものすごく一般的な名前がこのリストに現れるようになったら、それはもう何が起こるのか連想できなくなります。

>>> dir()
[..., 'SetValue', ...]

なんて出てきてもSetValueしたらなにが起こるか連想する事は出来ません。

他人のソースレビューをする時、どのソースをたどればいいのか一目でわかるようになっていないと非常に時間がかかったり、誤解したりします。

つまり「明示されている」という事は非常に重要なことなのです。

注釈

他人と共同作業なんてしてないって?先月書いたあなたのソースコードも他人のソースコードみたいなものですよ!

C++のnamespaceとの比較

C++はCにおける常にグローバルな唯一の名前空間にしか定義できない仕様を継承してしまっています。 そして名前のコンフリクトを避けるために モジュール単位での名前空間を別途定義できるようにしました。

当然、名前空間の定義は再利用を検討するモジュールでは必須になります。 (逆に名前空間定義のないC++ライブラリは本当に運用できているのか信用できません。)

定義せずに再利用を開始してしまうとあとの保守に大変な手間が発生します。

PythonやDelphiでは、とりあえずモジュール名がそのまま名前空間名として働くので モジュール開発段階でも意識せずに再利用を開始できます。

所感

Pythonとしばらく付き合わないと実感する事はできませんが、

Pythonの仕様やライブラリのソースを眺めていると、 「Pythonic」とよばれる思想みたいなものを感じ取ることが出来ます。

そのひとつのポリシーとして「明示する」ということが言えると思います。

そう考えると、selfを何度も打たされるのにも多少納得していただけるのではないかと・・・。