2018/05/15

Python にまつわるアイデア: Python のパッケージとモジュールの違い

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 '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 です。

さらにちなみに、この「 __init__.py が無いディレクトリが自動的にネームスペースパッケージになる」という仕組みが Python 3.3 で導入される前は、ネームスペースパッケージの仕組みそのものは存在していたのですが、ネームスペースパッケージ化するには __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 の説明であり、 BuiltinImporterFrozenImporter にはそのままあてはまりません。

(私の経験上)標準ライブラリ以外のモジュールを利用する場合、そのほとんどは PathFinder を利用する形になるので、実用上は b) のところまで押さえられていれば十二分ですが、 b) は他のインポートシステムのパターンはカバーできていないため、より厳密に言うならこれも不正確ということになるでしょう。

さらに、 Python のこのインポートシステム自体拡張することができるようなので、開発者が独自に「パッケージとモジュール」の形を決めて実装することもできるようです。独自に拡張されたインポートシステムのことも含めて考えると、 Python の「パッケージとモジュール」の定義・違いをわかりやすく説明するのはさらに困難になります。

まとめ


最後に、ここまでの内容をかんたんにまとめてみます。おおよそ次のような感じになるでしょうか。

  • シンプルに Python を利用する場合だけなら、 a) の理解で十分
  • 規模の大きなパッケージを開発・管理するような人は、 b) までは理解しておいた方がよさそう
  • Python そのものの開発に関わりたいような人にとっては、 b) の理解でも(おそらく)不十分

世の中のほとんどの人にとって「 円周率 = 3.14 」以上の説明が必要ないのと同じように(本人が知らなくてもその恩恵を受けているという点については無視するとして)、この「 Python のパッケージとモジュールの定義・違い」についても b) 以上の理解を必要とする人はどちらかというと少数派でしょう。

ですので、多くの Python ユーザにとっては、「 厳密に言うなら a) は間違いだけど、ふだんは a) で覚えておいて差し支えない 」程度に認識しておいて、 b) 以上の理解が必要になったときにはその都度調べて確認できるようにしておく、ぐらいがちょうどよいのではないかと思います(もちろん「とにかく使えさえしたら原理はどうだっていい」という人の場合は、理解する必要はありません)。

ちなみに最後にもうひとつ余談ですが、今回 Wikipedia で「円周率」を引いて驚いたのですが、 Wikipedia によると円周率は 2016 年現在で 22 兆桁(!)まで計算されているそうです。


以上、「 Python のパッケージとモジュールの違い」についての説明でした。「わかりやすかったよー」「役に立ったよー」「間違っているよー」という方はコメント等でお知らせいただけるとうれしいです。

以下、参考ページです。いずれも英語のページですが、 Python のパッケージとモジュールについての理解を深めたい方には一読の価値があります。


参考