Python の「パッケージ」と「モジュール」の違いについて説明してみます。
本題に入る前に数点お断りです。
- この記事は長文です。
- 記事作成時の Python の最新バージョンは Python 3.6 です。 Python 3.6 の頃の認識にもとづいて書かれています。
- この記事はある程度調査・確認をした上で書いていますが、私は Python の仕様や Python そのものの開発のプロではありません。あくまでも Python のいちユーザの認識であり間違っている可能性があります(とはいえ、なるべく正確に書こうというモチベーションで書いているので、詳しい方で間違いに気づいた方はご指摘いただけますと幸いです)。
Python の「パッケージ」と「モジュール」の違い
Python のパッケージとモジュールの概念は少し複雑なので、ひとことでかんたんに説明することができません。
次の 2 通りの方法をするのがよいのかなと思います。
- a) 正確ではないがシンプルな説明
- b) シンプルではないが(わりと)正確な説明
2 つの説明を持ち出す理由は、数学で「円周率 = 3.14 」と考えることに似ています。
円周率は本来無理数なので「円周率 = 3.14 」という説明は厳密には間違いですが、 3.14 としておいても実用上は問題のない状況がほとんどです。また、より厳密な定義を理解するには無理数等の概念を先に理解するなどの準備が必要です。そのため、円周率については次の 2 通りの説明ができます。
- e-a) 円周率は 3.14 である
- e-b) 円周率は 3.14159265358979... と続く無理数である
今回 2 通りの説明をする理由はこれと同じで、 a) の説明は「正確ではないがほとんどの場合はその理解で問題ない」、 b) の説明は「わかりづらいがより正確」、といった違いがあります。
順番に説明していきます。
a) 正確ではないがシンプルな説明
先に、正確ではないがシンプルな説明をしてみます。
Python におけるモジュールとパッケージの説明はそれぞれ次のとおりです。
- モジュール = ファイル。拡張子が
.py
(あるいは.pyc
等)の Python ファイルのこと。 - パッケージ = ディレクトリ。
__init__.py
というファイルを格納したディレクトリのこと。
次のように import
文で読み込むことができる点はパッケージもモジュールも共通です。
それぞれ以下の内容のファイルを準備します。
sample_module.py
:
print('これはモジュールです。')
sample_package/__init__.py
:
print('これはパッケージです。')
その上でこれらを python -c
コマンドで実行します。
$ python -c 'import sample_module'
これはモジュールです。
$ python -c 'import sample_package'
これはパッケージです。
この a) の説明において、パッケージとモジュールの違いは「 モジュールは単一のファイルで、パッケージはディレクトリである 」ということになります。
パッケージはディレクトリなので、他のモジュール(=ファイル)やパッケージ(=ディレクトリ)を中に格納することができます。一方のモジュールはファイルなので、他のモジュールを格納することができません。
$ # パッケージは他のパッケージを格納することができる
$ tree .
└── songohan
├── __init__.py
└── songoku
├── __init__.py
├── songoten.py
└── songohan.py
# パッケージの中のパッケージを import することができる
import songohan.songoku
import songohan.songoku.songoten
import songohan.songoku.songohan
a) の説明は以上です。 b) の説明に入る前に、 a) について少し補足をします。
a) の補足: __init__.py
を中に持たないディレクトリの場合
Python 3.3 以降では __init__.py
ファイルを中に持たないディレクトリも Python パッケージとして認識されるようになりました。
$ # trunks.py は `import vegeta.trunks` でインポートできる
$ tree .
└── vegeta
└── trunks.py
__init__.py
の無いディレクトリは、パッケージはパッケージだけど少し特殊な「 ネームスペースパッケージ 」(名前空間パッケージ)として扱われます。
ネームスペースパッケージは、異なるパスに置かれた複数のモジュールやパッケージを共通の親パッケージでまとめることができるものです 。例えば、次のように 2 つの異なる場所に chuoku
というディレクトリがあった場合、それぞれの親ディレクトリをモジュールの検索パスに追加すれば、これらを共通のネームスペースパッケージとして利用できるようになります。
$ tree /tmp/tokyo/ /tmp/osaka/osaka/
/tmp/tokyo/
└── chuoku
└── sample1.py
/tmp/osaka/osaka/
└── chuoku
└── sample2.py
2 directories, 2 files
このことは、次の Python コードで検証することができます。
import sys
# モジュール検索パスに 2 つの chuoku ディレクトリの親ディレクトリを追加する
sys.path.append('/tmp/tokyo')
sys.path.append('/tmp/osaka/osaka')
# ネームスペースパッケージ `chuoku` の下にあるモジュール `sample1` `sample2` を
# どちらも問題なく import することができる
import chuoku.sample1
import chuoku.sample2
逆に、上の chuoku
ディレクトリのうちどちらか一方でも __init__.py
ファイルを中に持っていれば、それはネームスペースパッケージではなく通常のパッケージとなります。例えば、 /tmp/tokyo/chuoku
の方に __init__.py
があればこれは通常のパッケージとなるので、もう一方の /tmp/osaka/osaka/chuoku
はパッケージとして読み込めなくなってしまいます。逆もまた然りです。
ちなみに Python の公式ドキュメントでは、ネームスペースパッケージではない通常のパッケージをネームスペースパッケージと区別するために、通常のパッケージを「レギュラーパッケージ」あるいは「トラディショナルパッケージ」ということばで呼んでいます。
この __init__.py
を持たないディレクトリが自動的にネームスペースパッケージになると何がうれしいのかと言うと、 規模の大きなパッケージのサブパッケージを別々のディストリビューションパッケージとして配布するのがやりやすくなります 。
ここで「ディストリビューションパッケージ」というのは配布用にまとめられた単位のパッケージのことを指していて、通常は pip install
コマンドの引数として指定するものです。例えば、 Django REST framework というライブラリは、 pip
でインストールするときには djangorestframework
、 Python コード内で利用するときは rest_framework
という名前でそれぞれ参照・指定します。
pip
コマンド:
$ pip install djangorestframework
Python コード内:
from django.contrib.auth.models import User
from rest_framework import serializers
class UserSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = User
fields = ('url', 'username', 'email', 'is_staff')
この Django REST framework の場合、パッケージ名は rest_framework
で、ディストリビューションパッケージ名は djangorestframework
、と言うことができます。
さらにちなみに、 Python 3.3 でこの「 __init__.py
が無いディレクトリが自動的にネームスペースパッケージになる」という仕組みが導入される前は、ネームスペースパッケージの仕組みそのものは存在していました。しかし、ネームスペースパッケージ化するには __init__.py
を用意してその中に次のようなコードを書く必要があり、これがなかなか面倒臭かったようです。
__path__ = __import__('pkgutil').extend_path(__path__, __name__)
__import__('pkg_resources').declare_namespace(__name__)
このあたりが Python 3.3 で簡単化されて「 __init__.py
が無いディレクトリは自動的にネームスペースパッケージになる」という仕組みが導入されました。該当する PEP は PEP 420: Implicit Namespace Packages です。細部に興味のある方は PEP 420 のページを読んでみてください(私は読んでもよくわかりませんが・・・)。
a) の補足は以上です。続いて b) の説明を見ていきましょう。
b) シンプルではないが(わりと)正確な説明
b) における Python のモジュールとパッケージの説明は次のとおりです。
- モジュール =
import
文でインポートすることができるもの 。 - パッケージ = モジュールのうち他のモジュールをサブモジュールとして格納したもの 。
もう少し長めに言い換えます。
- すべてのパッケージはモジュールである 。数学的な感じで書けば「 Gp ⊂ Gm 」。
- パッケージとモジュールの共通点は「
import
文でインポートできる 」こと。 - パッケージとモジュールの違いは「 パッケージには
__path__
アトリビュートがありそれを通じてサブモジュールを提供している一方で、モジュールには__path__
が無くサブモジュールを提供していない 」こと。
つまり、パッケージとモジュールは基本的には同じもので、異なるのは「パッケージには __path__
アトリビュートがありそれを通じてサブモジュール(この「サブモジュール」には「サブパッケージ」を含む)を提供していて、モジュールは __path__
アトリビュートが無くサブモジュールを提供していない点」だけです。
この「 __path__
というアトリビュートが重要な仕事をしている」という点は、次のような検証用コードで確認することができます。
2 つのパターンを試してみましょう。ひとつめは「パッケージとして動作する Python ファイル(≒モジュール)」のパターンです。
weird_module.py
:
import os
__path__ = (os.path.dirname(__file__), )
weird_module.py
ファイルがあるディレクトリに移動して、 Python インタラクティブシェルを起動して次のコードを試してみます。
# どの import 文も問題なく実行できる
import weird_module
import weird_module.weird_module
import weird_module.weird_module.weird_module
weird_module.py
は単一の Python ファイルですが、 __path__
アトリビュートを持っており、その値のタプルの要素のひとつとして自分自身の親ディレクトリのパスを持っています。結果として、 weird_module
は実体は単一のファイルでありながら、自分自身をサブモジュールとして再帰的に提供できるパッケージとして動作します(あくまでも検証用なので、実用性はありません)。
もう 1 つのパターンを見てみましょう。これは先ほどとは逆で、「 __init__.py
ファイルを格納したディレクトリで、本来パッケージとして扱われるはずなのに、中にある .py
ファイルをサブモジュールとして提供しないパッケージ」のパターンです。
次のファイル構造を用意しましょう。
$ tree weird_package
weird_package
├── __init__.py
└── child.py
0 directories, 2 files
続いて、 weird_package
ディレクトリの下の __init__.py
ファイルに次の 1 行のコードを書き込みます。
weird_package/__init__.py
:
del __path__
準備ができたら、 weird_package
の親ディレクトリに移動し、 Python インタラクティブシェルを起動して次のコードを試してみます。
# weird_package 自体は問題なく import できる
import weird_package
# weird_package/child.py は import できない
import weird_package.child
# =>
# Traceback (most recent call last):
# File "<stdin>", line 1, in <module>
# ModuleNotFoundError: No module named 'weird_package.child'; 'weird_package' is not a package
weird_package
のサブモジュールであるはずの child
を import
することができません。 weird_package
ディレクトリは __init__.py
ファイルを持っているにもかかわらず、 __path__
が適切に定義されていないので、サブモジュール child
を提供していません。
この検証用サンプルでは他にもいろんなことを確認することができますが、これ以上踏み込むことはよしておきます。
b) の説明は以上です。続いて、 a) b) 2 つの説明を比較して、ポイントを見ていきましょう。
a) b) 2 つの説明を比較して
繰り返しになりますが、 b) の説明を読むと a) の「モジュール = ファイル、パッケージ = ディレクトリ」という説明は一部正しいところがあるものの厳密には正しくないということがわかります。
ただ、 a) の説明が完全に間違いかというとそんなことはなくて、実用上は a) の形で理解していても何ら問題がないことがほとんどです。
ちなみに、(私はこのあたりに詳しくはないのですが)、更に厳密に言うなら、 Python のインポートシステムはデフォルトで次の 3 つのインポート用クラスを提供しています。
BuiltinImporter
: 組み込みモジュールをインポートするためのもの。FrozenImporter
: コンパイルされた frozen モジュールをインポートするためのもの。PathFinder
: モジュール検索パス上にあるモジュールをインポートするためのもの。
a) の説明は、このうちの PathFinder
の挙動の一部を説明したものです。つまり、 a) の説明は PathFinder
以外のインポート用クラスにはあてはまりませんし、さらに PathFinder
の場合でも __path__
に手が加えられた特殊なモジュールにはあてはまりません。
他方の b) の説明は、 PathFinder
を実際の挙動も含めてより厳密に説明したものです。ただ、こちらもあくまでも PathFinder
の説明であり、 BuiltinImporter
や FrozenImporter
にはそのままあてはまりません。
(私の経験上)標準ライブラリ以外のモジュールを利用する場合、ほとんどは PathFinder
を利用する形になるので、実用上は b) のところまで押さえられていれば十分ですが、 b) は他のインポートシステムのパターンはカバーできていないため、厳密に言うならこれも不正確ということになるでしょう( b) よりも厳密な c) の説明ができるはずです)。
さらに、 Python のこのインポートシステム自体拡張することができ、開発者が独自に「パッケージとモジュール」の形を決めて実装することもできるようです。独自に拡張されたインポートシステムのことも含めて考えると、 Python の「パッケージとモジュール」の定義・違いをわかりやすく説明するのはさらに難しくなります。
まとめ
最後に、ここまでの内容をかんたんにまとめてみます。おおよそ次のような感じになるでしょうか。
- シンプルに Python を利用する場合だけなら、 a) の理解で十分
- 規模の大きなパッケージを開発・管理するような人は、 b) までは理解しておいた方がよさそう
- Python そのものの開発に関わりたいような人にとっては、 b) の理解でも(おそらく)不十分
世の中のほとんどの人にとって日常生活上は「 円周率 = 3.14 」以上の理解が必要ないのと同じように(本人が知らなくてもその恩恵を受けているという点については無視するとして)、この「 Python のパッケージとモジュールの定義・違い」についても b) 以上の理解を必要とする人はどちらかというと少数派でしょう。
ですので、多くの Python ユーザにとっては、「 厳密に言うなら a) は間違いだけど、ふだんは a) で覚えておいて差し支えない 」程度に認識しておいて、 b) 以上の理解が必要になったときにはその都度調べて確認できるようにしておく、ぐらいがよいのではないかと思います(もちろん、「とにかく使えたら原理はどうだっていい」という考えの人の場合は、まったく理解する必要はありません)。
ちなみに最後にもうひとつ余談ですが、今回 Wikipedia で「円周率」を引いて驚いたのですが、 Wikipedia によると円周率は 2016 年現在で 22 兆桁(!)まで計算されているそうです。
以上、「 Python のパッケージとモジュールの違い」についての説明でした。「わかりやすかったよー」「役に立ったよー」「間違っているよー」という方はコメント等でお知らせいただけるとうれしいです。
以下、参考ページです。いずれも英語のページですが、 Python のパッケージとモジュールについての理解を深めたい方には一読の価値があります。
参考
-
モジュールとは
-
パッケージとモジュールの違い
-
ネームスペースパッケージ
- PEP 420 – Implicit Namespace Packages | peps.python.org
- Regular package | Glossary — Python 3 documentation
- Namespace package | Glossary — Python 3 documentation
- Packaging namespace packages — Python Packaging User Guide
- sample-namespace-packages/native at master · pypa/sample-namespace-packages · GitHub
- Traps for the Unwary in Python’s Import System — Nick Coghlan's Python Notes 1.0 documentation
- How do I create a namespace package in Python? - Stack Overflow
-
インポートシステム