Python の dis モジュールでさまざまな import 文を disassemble する (2)

2008/09/25 5:39pm

前回の記事への追記が長くなりそうなので、新しい記事にまとめました。

import と import as

import <module>import <module> as <name> では、どのようなバイトコードにコンパイルされるか、つまり、実行時の振る舞いが微妙に異なることに注意しよう。

>>> dis.disassemble(compile('import os.path', '', 'exec'))
  1           0 LOAD_CONST               0 (-1)
              3 LOAD_CONST               1 (None)
              6 IMPORT_NAME              0 (os.path)
              9 STORE_NAME               1 (os)
             12 LOAD_CONST               1 (None)
             15 RETURN_VALUE
>>> dis.disassemble(compile('import os.path as os_path', '', 'exec'))
  1           0 LOAD_CONST               0 (-1)
              3 LOAD_CONST               1 (None)
              6 IMPORT_NAME              0 (os.path)
              9 LOAD_ATTR                1 (path)
             12 STORE_NAME               2 (os_path)
             15 LOAD_CONST               1 (None)
             18 RETURN_VALUE

前者では IMPORT_NAME os.path でロードされたモジュールを os という名前でシンボルテーブルに登録する。では、IMPORT_NAME os.path でロードされるモジュールは何かといえば、これは os モジュールである。os.path ではない。

そして、os モジュール内部では os.path モジュールを path という名前でインポートしている(実際には、プラットフォームごとに異なるモジュールをインポートする)。そのため、os モジュールを os という名前でシンボルテーブルに登録しておけば、os.pathos.path モジュールを参照できるわけだ(このへんは次節で更に掘り下げる)。

それにたいして、後者の import os.path as os_path では IMPORT_NAME os.path でロードされたモジュールから LOAD_ATTR path で取り出した os.path モジュールを os_path という名前でシンボルテーブルに登録する。このように、import <module>import <module> as <name> では、実行時の振る舞いが微妙に異なっている。

import A と import A.B

ここからはバイトコードを離れて、import の動作をみていこう。まずは前節のimport os.path as os_path を振り返る。

  1. IMPORT_NAME os.pathos モジュールがロードされる
  2. LOAD_ATTR pathos モジュールから os.path モジュールを取り出す
  3. 取り出した os.path モジュールを os_path という名前でシンボルテーブルに登録する

前節でも書いたように、os モジュール内部では os.path モジュールを path という名前でインポートしているので、2. の手順がうまくいくのは納得できる。しかし、それ以外のモジュール(パッケージ)ではどうだろうか?

たとえば、A.B.C というパッケージ構造があるとする。

% python
>>> import A
>>> A
<module 'A' from 'A/__init__.pyc'>
>>> dir(A)
['__builtins__', '__doc__', '__file__', '__name__', '__path__']

このように、dir() で調べても、A モジュールに B という属性はない。

>>> A.B
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'module' object has no attribute 'B'

ということは、このパッケージでは import A.B as A_B のようなインポートは失敗しそうだ。何故なら A モジュールに B という属性はないのだから、上の手順では 2. で失敗するはずである。

まずは念のため、バイトコードも確認しておこう。

>>> dis.disassemble(compile('import A.B as A_B', '', 'exec'))
  1           0 LOAD_CONST               0 (-1)
              3 LOAD_CONST               1 (None)
              6 IMPORT_NAME              0 (A.B)
              9 LOAD_ATTR                1 (B)
             12 STORE_NAME               2 (A_B)
             15 LOAD_CONST               1 (None)
             18 RETURN_VALUE

前節でみたものと同じだ。やはり、LOAD_ATTRB を取り出そうとしている。では、実行してみる。

>>> import A.B as A_B
>>>

おかしい。成功してしまった。

>>> A_B
<module 'A.B' from 'A/B/__init__.pyc'>

ちゃんとインポートもできているようだ。どういうことだろうか? 一旦インタプリタを終了して、もう一度、今度は import A.B を試してみよう。

% python
>>> import A.B
>>> A.B
<module 'A.B' from 'A/B/__init__.pyc'>

なんと、A.B でモジュールにアクセスできている。更に import A して A モジュールを調べてみると、

>>> import A
>>> A
<module 'A' from 'A/__init__.pyc'>
>>> dir(A)
['B', '__builtins__', '__doc__', '__file__', '__name__', '__path__']

A モジュールに B が増えていることが分かる。

これで os 以外のモジュールでも import がうまく動作する理由が分かった。おそらくは IMPORT_NAME が実行されるときに、内部的にこのような下準備が行われていたわけだ。