1
0

More than 3 years have passed since last update.

【Python】モジュールとパッケージを抽象的に理解してみる

Last updated at Posted at 2020-05-03

はじめに

Python のインポートシステムについて、

  • モジュールは .py ファイル
  • パッケージは __init__.py を置いたディレクトリ

などと理解してもよいが、
物理ファイルから離れ、少し抽象化して考えることによって、モジュール名、相対インポート、初期化タイミング、名前の可視性など、システム全体の見通しがよくなると思っている。

なお、本記事では 名前空間パッケージ1 は解説の対象外としている (私が理解していないため)。

参考文献

いずれも v3.8.2 版を閲覧した。

環境

Python v3.8.2 で動作確認した。pytest のテストケースとして動作を検証する。

pip install pytest

pytest 自体の動作確認については付録参照のこと。

概要

  • モジュールとは名前空間の単位であり、インポートできるものである。
  • すべてのコードは、あるモジュール (名前空間) の中で動いている。トップレベルモジュール名は __main__ である。
  • モジュールをインポートすると、モジュールとその名前空間を、現在の名前空間から参照することができる。
  • パッケージとはサブモジュールを持つことのできるモジュールのことである。
  • 一度インポートされたモジュールはグローバルにキャッシュされている。

属性の簡単なまとめ:

__name__ __path__ __package__
モジュール: モジュール名 なし 親パッケージ名
パッケージ: パッケージ名 パッケージのパス パッケージ名

名前空間 (namespace) とは

これはプログラミング言語で一般に使われる概念で、
プログラムの相異なる場所で、変数名 x などの「名前」を、衝突を恐れずに違う意味で使えるための機能である。
たとえば builtins.openos.open はどちらも関数名だが、所属する名前空間が異なるために、
違うものとして定義・使用することができる。
次に説明する「モジュール」は、この 名前空間の単位 になっている。

なお、Python ドキュメントには用語集があって、「namespace」の項目がある: https://docs.python.org/ja/3/glossary.html#term-namespace

モジュールとは・インポートとは

概要でも述べたように、モジュールとは 名前空間の単位であり、import できるもの のことである。

名前空間の単位

まず、モジュールは名前空間の単位なので、モジュール mn に対し、m 内の名前 x (= m.x) と n 内の名前 x (= n.x) は区別される。

コードはすべて、ある名前空間で動作する

次に、あまり意識することがないが、すべてのコードはあるモジュールの中で、つまりある名前空間の中で動いている。
コードが名前を宣言、定義すると、その名前空間に登録される。
Python インタプリタが起動された直後は、__main__ モジュールの名前空間 でコードが動いている。
これがよくある、ある .py ファイルが直接起動されたのかインポートされたのか区別する方法 に使われている。

モジュールのインポート

次に、モジュールはインポートすることができる。モジュールをインポートするとは、

  • モジュールを探してキャッシュし、
  • ロードおよび初期化して、
  • 現在の名前空間にモジュールを束縛する

ことである。ドキュメントには2ステップと書かれているが、3ステップだと思うと処理単位がわかりやすい。
簡単に言うと、モジュール mimport m としてインポートすることにより、

  • モジュール msys.path から検索して、モジュールオブジェクトをグローバルキャッシュ sys.modules に登録する。
  • モジュール m のロードおよび初期化により、その名前空間に名前を登録する。例えば m.fm.C など。
  • 現在の名前空間に 名前 m を登録して、これがモジュール m を指すようにする。

Python インタプリタを対話モードで起動して確かめてみる。(見やすさのため一部整形している)

[.../module-experiments]$ python
Python 3.8.2 (default, Apr 18 2020, 18:08:23) 
[Clang 10.0.1 (clang-1001.0.46.4)] on darwin
Type "help", "copyright", "credits" or "license" for more information.

>>> print(__name__)
__main__

>>> import m
>>> sys.modules['m']
<module 'm' from '.../module-experiments/m.py'>
>>> m
<module 'm' from '.../module-experiments/m.py'>
>>> m.f
<function f at 0x108452b80>
>>> m.C
<class 'm.C'>

開始直後のモジュールが __main__ であることが確認できる。
また、m のインポートにより、mm.f および m.C にアクセスできることがわかる。

なお、インポートされたモジュールはローカルな名前空間にキャッシュされる。
したがって、他の名前と同様に、関数内でインポートしたモジュールは他の関数からはアクセスできない。
ただしキャッシュはグローバルなのでアクセスできる。

パッケージとは

パッケージとは単純に、その中でサブモジュールを検索、インポートできること以外はモジュールと全く同じである。
パッケージを使うことにより、モジュールの階層構造を作ることができる。

簡単に言うと

このとき、モジュール (またはパッケージ) の名前はドットで区切られた a.b.c のようなものになる。
モジュール a.b.cimport a.b.c でインポートすることにより、

  • モジュール (パッケージ) a をインポートする
  • モジュール a の名前空間において、サブモジュール (パッケージ) b (a.b) をインポートする
  • モジュール a.b の名前空間において、サブモジュール c (a.b.c) をインポートする

ことにより、モジュール a.b.ca.b.c でアクセスできるようになる。
注意: c でアクセスできるようになるわけではない。

また、このルールにより、パッケージでもモジュールでも同じようにインポートできることがわかる。

a.b.c をインポートした直後の状態においては、当然 a.da.d.e にはアクセスできない。
ここで import a.d.e を行うと、キャッシュに a が存在することから、

  • モジュール a の名前空間において、サブモジュール (パッケージ) d (a.d) をインポートする
  • モジュール a.d の名前空間において、サブモジュール e (a.d.e) をインポートする

だけが起こるので、これらの時点でモジュールの初期化が行われる。

もう少し詳しく

テクニカルには、パッケージとは、__path__ 属性を持つモジュールのことである。
この属性は、サブパッケージを探す際に用いられる。
つまり、import a.b.c においては、

  • asys.path から探してインポートする
  • モジュール a の名前空間において、a.ba.__path__ から探してインポートする
  • モジュール a.b の名前空間において、a.b.ca.b.__path__ から探してインポートする

ということが起こっている。

なお、パッケージを含むモジュールは __name__ 属性を持っている。
インポートされたモジュールについては、この値はモジュールの完全修飾名 (a.b.c みたいな) である。

パッケージのインポート

パッケージはモジュールなので、モジュール同様に import 文でインポートできる。

import ... as ...

import 文に as 節がある場合、インポートされたモジュール自身が as 以下の名前に束縛される。
例えば、 import a.b as b とすると、名前 b で直接モジュール a.b にアクセスできる。

このとき、モジュール a のロードと初期化は行われているため、モジュール a (と、当然 a.b も) に関する情報はキャッシュされている。しかし、名前空間に a は束縛されていない。

>>> import sys
>>> import a.b as b
['/Users/shinsa/git/python-experiments/module-experiments/a']
a

>>> sys.modules['a']
<module 'a' from '/Users/shinsa/git/python-experiments/module-experiments/a/__init__.py'>
>>> sys.modules['a.b']
<module 'a.b' from '/Users/shinsa/git/python-experiments/module-experiments/a/b/__init__.py'>

>>> b
<module 'a.b' from '/Users/shinsa/git/python-experiments/module-experiments/a/b/__init__.py'>

>>> a
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
NameError: name 'a' is not defined

モジュール名と名前は異なる

import の直後に来るのはあくまでもモジュール名であり、この場所に、定義された名前を使うことはできない。
したがって、import a.b as b としたからといって、import a.b.cimport b.c と書くことはできない。

from 節を持つインポート

この形式のインポートは from a.b import c のケースを取る。c はモジュールでもパッケージでも名前でもよい。
以下の処理が行われる。

  • a.b をインポートする。ただし名前の束縛を行わない。
  • c の束縛を試みる。
    • ca.b で定義された名前である場合、a.b.cc という名前に束縛する。
    • c がモジュールまたはパッケージである場合、a.b.c をインポートして (ただし名前は束縛しない)、a.b.cc という名前に束縛する。

もし as 節が指定されている場合は、上記の例で c を束縛する代わりに、as 節で指定された名前に束縛する。

from ... import *

インポートする名前が *、つまり from M import * の場合には、モジュール M の中で

  • __all__ が定義されている場合にはそれにより指定される名前
  • そうでない場合には M で定義されている名前で _ で始まらないもの

がすべて現在の名前空間で束縛される。

相対インポート

from ... import ... 文では、from 節に相対パッケージ表現を使用できる。
たとえば、パッケージ a.b の初期化スクリプト、もしくはモジュール a.b.c のスクリプトにおいて、

  • from .from a.b を、
  • from ..from a を、
  • from .dfrom a.d を、

それぞれ表す。ドットはもっとたくさんついてもよい。

PEP 366

相対パッケージの計算は __name__ 属性に基づいて行われていた。
すると、例えば python -m a.b.c でインタプリタを起動した時、a/b/c.py__main__ モジュールとして動作する。
これが原因となり、例えば a.b.d を相対インポートができないという問題があった。

新しいバージョンの Python では、__package__ 属性に基づいて相対インポートが計算されるようになり、上記の呼び出し時の問題は解決した (PEP 366)。 ただし python <path>/a/b/c.py で起動した場合は、手動で属性をセットする必要がある。

デフォルト実装: ファイルシステムに対するインポートシステム

さて、ここまで、モジュール、パッケージ、__path__ については、抽象的に説明してきた。
つまり、具体的なファイルシステムでの話を全くしていない。
最近のバージョンでは、Python のインポートシステムは高度に抽象化されたパワフルなシステムになっている。

デフォルトでは、インポートシステムはファイルシステムを前提として動作する。
デフォルトではモジュールは .py ファイル、パッケージはディレクトリによって表現されている。
詳しくはこうなっており、これまでの抽象的なデータ構造にきっちりフィットすることがわかる。

  • sys.path の内容はファイルパスであるとみなして検索を行う。import m により sys.path に含まれるディレクトリが順に検索されて、いずれかのディレクトリにある m.py がインポートされる。
  • パッケージ a.b のロード、初期化は a/b/__init__.py の実行により行う。
  • モジュール a.b.c のロード、初期化は a/b/c.py の実行により行う。
  • パッケージ a.b__path__ 属性値は、b のディレクトリパスである。

逆に言うと、例えば URI ベースのインポートシステムを実装することができ、サービスディスカバリの仕組みを Python のインポートシステムを使って実現することができる。

さいごに: ちょっと不満

Python のドキュメントは、いろいろなところに情報が散らばっていてわかりにくい。
情報を重複させないためにそうしているならまだ理解できるが、
そうではなくて、各情報が微妙に重複しているのに散らばっている。さらに一部の情報は古かったりする。

1
0
3

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0