Date: 2012-07-24
Tags: python, python3, namespace-package

Python 3.3b1 の名前空間パッケージを試してみた

'python', 'python3', 'namespace-package'

今日会社で @IanMLewis と話していて、Python3の名前空間パッケージの話になった。自分はてっきり名前空間パッケージは今後業界標準から消えていくのかと思っていたんだけど、実際には PEP-0420 で提案されて Python-3.3 から標準化されている、というのを教えてもらった。 Ian++

Python2の名前空間パッケージ

Pythonで名前空間パッケージを使うにはいくつか作法がある。そもそもPythonでディレクトリをimport可能にする(=パッケージにする)には、そのディレクトリに __init__.py を置く必要がある。中身は空でも良い。

/tmp/
   +-- spam/
   |    +-- __init__.py
   +-- ham/

(spam/ham逆だったのを直しました. thx @kyuwabara !)

この状態で以下のようになる:

$ cd /tmp
$ python
Python 2.7.2 ...(略)
>>> import spam
>>> import ham
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ImportError: No module named ham

spam はimportできたけど ham はimport出来ない。

次に名前空間パッケージを作ってみる。名前空間パッケージとは、PYTHONPATHの通っている別々のディレクトリに同じPythonパッケージ構造をもっているもののこと。 parent.child1.oneparent.child2.two が同じディレクトリにあるのは名前空間パッケージとは言わない。

名前空間パッケージではない、普通のパッケージ:

/tmp/
   +-- parent/
        +-- __init__.py
        +-- child1/
        |    +-- __init__.py
        |    +-- one.py
        +-- child2/
             +-- __init__.py
             +-- two.py
$ cd /tmp
$ python
Python 2.7.2 ...(略)
>>> import parent.child1.one
>>> import parent.child2.two

名前空間パッケージ:

/tmp/
   +-- spam/
   |    +-- parent/
   |         +-- __init__.py     ... (1)
   |         +-- child1/
   |              +-- __init__.py
   |              +-- one.py
   +-- ham/
        +-- parent/
             +-- __init__.py     ... (1)
             +-- child2/
                  +-- __init__.py
                  +-- two.py

このとき (1) の__init__.pyには一般的に以下の内容を記載する:

try:
    __import__('pkg_resources').declare_namespace(__name__)
except ImportError:
    __path__ = __import__('pkgutil').extend_path(__path__, __name__)

これで以下のようにspamとhamにPATHを通せば動作する(環境変数PYTHONPATHでもいい):

$ cd /tmp
$ python
Python 2.7.2 ...(略)
>>> import sys
>>> sys.path.insert(0, '/tmp/spam')
>>> sys.path.insert(0, '/tmp/ham')
>>> import parent.child1.one
>>> import parent.child2.two

このとき、parentが名前空間パッケージという、実体が1つのPATHとは限らない特殊なパッケージと呼ばれる。

>>> parent.__path__
['/tmp/spam/parent', '/tmp/ham/parent']

Python3の名前空間パッケージ

Python-3.3(b1)のリリースノート に以下のように記載されている:

Native support for package directories that don’t require __init__.py marker files and can automatically span multiple path segments (inspired by various third party approaches to namespace packages, as described in PEP 420)

Python標準で、パッケージディレクトリに __init__.py マーカーファイルを 置かなくてもパッケージとして使えるようになりました。また、複数のパス に同じパッケージ名が分散している場合に自動的に集約するようになりました (これらは、いくつかのサードパーティーのアプローチにインスパイアされ、 PEP-0420 で採用されました)。

ということで、もはや パッケージディレクトリに__init__.pyは要らない らしい。

さっそく試してみた:

/tmp/
   +-- spam/
   |    +-- parent/
   |         +-- child/
   |              +-- one.py
   +-- ham/
        +-- parent/
             +-- child/
                  +-- two.py

__init__.py は無し。

$ python3.3
Python 3.3.0b1 ...(省略)
>>> import sys
>>> sys.path.append('/tmp/spam')
>>> import parent
>>> parent.__path__                          #(1)
_NamespacePath(['/tmp/spam/parent'])
>>>
>>> sys.path.append('/tmp/ham')
>>> parent.__path__                          #(2)
_NamespacePath(['/tmp/spam/parent'])
>>>
>>> import parent.child
>>> parent.__path__                          #(3)
_NamespacePath(['/tmp/spam/parent', '/tmp/ham/parent'])
>>> parent.child.__path__
_NamespacePath(['/tmp/spam/parent/child', '/tmp/ham/parent/child'])

ちゃんとimportできたし、実体が複数箇所にあることも認識された! (1)の時点でparentがimport済みだったので、pathを追加しただけでは(2)ではparent.__path__は変化していないが、(3)でimportしたあとではparent.__path__が変化した。

次はsiteコマンドを使ってsite-packagesを追加する要領でディレクトリを追加:

>>> import site
>>> site.addsitedir('/tmp/egg')
>>> parent.__path__
_NamespacePath(['/tmp/spam/parent', '/tmp/ham/parent'])
>>>
>>> import parent.child.three
>>> parent.__path__
_NamespacePath(['/tmp/spam/parent', '/tmp/ham/parent', '/tmp/egg/parent'])
>>> parent.child.__path__
_NamespacePath(['/tmp/spam/parent/child', '/tmp/ham/parent/child', '/tmp/egg/parent/child'])

sys.path.appendしたときと同じように、parentもparent.childもimport済みだったためpathを追加しただけでは変わらなかったけど、/tmp/eggにあるモジュールをimportしたらちゃんとparent.__path__が変化した。

すばらしい!これで __init__.py を置くべきか置かないべきかという話は不要になるね。

追記1

$ cd /tmp
$ mkdir foo
$ mkdir bar
$ touch foo/__init__.py
$ python3.3
Python 3.3.0b1 ...(省略)
>>> import foo
>>> import bar
>>> foo
<module 'foo' from './foo/__init__.py'>
>>> bar
<module 'bar' (namespace)>

__init__.py が無い場合 (namespace) と表示されている。

追記2

C:\Users\taka> cd \
C:\> python3.3
Python 3.3.0b1 ...(省略)
>>> import Users.taka.Dropbox.code.python.stdout
>>> Users.taka.Dropbox.code.python.stdout
<module 'Users.taka.Dropbox.code.python.stdout' (namespace)>
>>> Users.taka.Dropbox.code.python.stdout.__path__
_NamespacePath(['.\\Users\\taka\\Dropbox\\code\\python\\stdout'])

なんか気持ち悪いぞw