Python を支える技術: モジュール・ インポートシステム編 / python-module-import-system

Python を支える技術: モジュール・ インポートシステム編 / python-module-import-system

33a639236e5fa989dde2f579202ec18a?s=128

Nozomu Kaneko

September 22, 2016
Tweet

Transcript

  1. Python を支える技術: モジュール・ インポートシステム編 Nozomu KANEKO PyCon JP 2016, 2016-09-22

    早稲田大学西早稲田キャンパス Room 202 #pyconjp_202 1
  2. アジェンダ 1. インポートフック 2. 名前空間パッケージ 3. 相対インポート 2

  3. おまえ、誰よ • @knzm2015 (数字は気が向いたときにインクリメントされます) • 現在は自然言語処理や機械学習の研究開発っぽいことをやってます • その前はスマートフォンアプリのバックエンド開発してました • それ以前は

    Web エンジニア(言語は Python)やってました • 大学時代は自然言語による対話システムの研究をしてました • 趣味は Python 公式ドキュメント翻訳(最近あまりできてない) • 仕事先の PyCon JP スポンサー率がやばい 3
  4. アジェンダ 1. インポートフック 2. 名前空間パッケージ 3. 相対インポート 4

  5. インポートの裏側で起きていること 多くの人が考えるインポートの仕組み: 1. sys.path からモジュールに対応するファイルを検索する 2. ファイルが見つかったら、モジュールをロードする 1. インポートフック 5

  6. インポートの裏側で起きていること (2) 実際に起きていることはもう少し複雑: 1. インポート機構が sys.meta_path を検索して、 PathFinder の find_spec()

    を呼び出す 2. PathFinder が sys.path_hooks を検索して、FileFinder のfind_spec() を呼び出す 3. FileFinder は sys.path からファイルを検索してモジュール仕様を返す 4. モジュール仕様に含まれる SourceFileLoader を使ってモジュールをロードする 1. インポートフック 6
  7. インポートの裏側で起きていること (3) さらに、 • import 文は __import__() 関数呼び出しのシンタックスシュガー • __import__()

    は内部で importlib.import_module() を呼んでいる import foo ↓ __import__(“foo”) ↓ importlib.import_module(“foo”) ↓ 前スライドの 1 へ 1. インポートフック 7
  8. インポートフックと importer プロトコル インポートフックとは、インポート処理の中でモジュールを見つける方法やロードする方 法をカスタマイズするための方法です。現在では、名前空間パッケージの検索やモ ジュールのスクリプト実行 (python -m) などのさまざまな仕組みがインポートフックを 使って実装されています。

    importer プロトコルは、インポートフックの中で使われるオブジェクトが実装すべきメソッ ドを定義したものです。 フックの方式として メタフック と パスフック の2種類があり、そこで中心的な働きをする のが ファインダー と ローダー というオブジェクトです。 1. インポートフック 8
  9. インポートフックと importer プロトコル (2) • メタフック ◦ インポート処理が開始するときのフックポイント。任意の条件でインポート処理 を完全に置き換える目的で使われる •

    パスフック ◦ sys.path からモジュールを探す場合や名前空間パッケージをインポートすると きのフックポイント。ファイルシステムからモジュールをインポートする方法を置 き換える目的で使われる 1. インポートフック 9
  10. インポートフックと importer プロトコル (3) • ファインダー ◦ モジュールを見つける方法を知っているオブジェクト • ローダー

    ◦ モジュールをロードする方法を知っているオブジェクト • インポーター ◦ ファインダーとローダーの両方のメソッドを実装したクラス • モジュール仕様 ◦ ファインダーとローダーの間で情報をやり取りするためのオブジェクト 1. インポートフック 10
  11. メタフック sys.meta_path に登録されているメタパス・ファイ ンダーを使ってモジュール名に対応するモジュー ル仕様を検索します。 モジュール仕様が見つかったら、その情報に基づ いてモジュールのロードが行われます。 メタフックの使用例 (importlib.import_module() の簡略版)

    1. インポートフック 11
  12. パスフック sys.path_hooks に登録されている callable を呼 んでインポートパスに関連したパスエントリ・ファイ ンダーを求め、モジュール名に対応するモジュール 仕様を検索します。 パスフックの使用例 (PathFinder.find_spec()

    の簡略版) 1. インポートフック 12
  13. 組み込みのファインダーとローダー • 組み込みのファインダークラス ◦ PathFinder -- メタパス・ファインダー ◦ FileFinder --

    パスエントリ・ファインダー • 組み込みのローダークラス ◦ SourceFileLoader -- ソースからモジュールをロードする ◦ SourcelessFileLoader -- ソースがないモジュールをロードする ◦ ExtensionFileLoader -- 拡張モジュールをロードする • 組み込みのインポータークラス ◦ BuiltinImporter -- ビルトインモジュール用 ◦ FrozenImporter -- フリーズされたモジュール用 1. インポートフック 13
  14. 組み込みのファインダーとローダー (2) 実際にインポートフックにファインダーが登録されているのをインタラクティブシェルから 確認できます。 1. インポートフック 14 sys.meta_path をクリアするとインポートが正常に動かなくなる!

  15. インポートフック関連の PEP • PEP 302 New Import Hooks • PEP

    420 Implicit Namespace Packages • PEP 451 A ModuleSpec Type for the Import System インポートフックの仕様は、徐々に改良を重ねて今に至っています。 1. インポートフック 15
  16. PEP 302 New Import Hooks リリースバージョン: Python 2.3 それまではインポート処理を拡張するためには __import__()

    関数を置き換えるなどの強引な方法しかなかっ たのが、標準の API を使ってできるようになりました。 この時点では、ファインダーとローダーの定義は以下のようなシンプルなもので、それに加えていくつかのモ ジュール属性を定義したものが importer プロトコルと呼ばれていました。 • ファインダーは find_module(fullname, path=None) を持つオブジェクト。ローダーを返す。 • ローダーは load_module(fullname) を持つオブジェクト。モジュールをロードして返す。 主なユースケースとしては、当初 zip インポートとアプリケーション化 (py2exe, py2app, etc.) が想定されていた ようです。 1. インポートフック 16
  17. リリースバージョン: Python 3.3 暗黙の名前空間パッケージが追加される過程で、ファインダーに以下のような仕様変更がありました。 • finder.find_loader(fullname) メソッドが追加された ◦ find_loader() メソッドが実装されていなければ従来通り

    find_module() が呼ばれる • メタパス・ファインダーとパスエントリ・ファインダーの仕様が追加された ◦ これまでは共通だった sys.meta_path のファインダーと sys.path_hooks のファインダーの API が 区別されるようになった PEP 420 Implicit Namespace Packages 1. インポートフック 17
  18. PEP 451 A ModuleSpec Type for the Import System リリースバージョン:

    Python 3.4 インポートフックに関わるオブジェクトとして、ファインダーとローダーに加えてモジュール仕様が追加されまし た。 • ファインダーの仕様変更 ◦ finder.find_spec(name, path, target) メソッドが追加 ◦ finder.find_module(), finder.find_loader() は deprecated • ローダーの仕様変更 ◦ loader.create_module(spec), loader.exec_module(module) メソッドが追加 ◦ loader.load_module() は deprecated 1. インポートフック 18
  19. アジェンダ 1. インポートフック 2. 名前空間パッケージ 3. 相対インポート 19

  20. Python 3.3 以降の名前空間パッケージ a.k.a. 暗黙の名前空間パッケージ パッケージが複数のディレクトリ(ポーション)から構成される ./ ├── path1/ │

    └── xyz/ │ └── spam.py └── path2/ └── xyz/ └── ham.py ←ポーション ←ポーション 両方をまとめて xyz パッケージとして扱う = xyz.spam と xyz.ham のどちらもインポート可能 2. 名前空間パッケージ 20
  21. • モジュール ◦ .py ファイル (場合によっては .pyc や .so のことも)

    • パッケージ ◦ 通常のパッケージ: __init__.py を含むディレクトリ ◦ Python 3.2 以前の名前空間パッケージ ◦ Python 3.3 以降の暗黙の名前空間パッケージ 用語に関する若干の混乱があることに注意が必要です: • Python コード上でインポートして作られるオブジェクトのことは モジュールオブジェクト と呼ばれますが、 モジュールオブジェクトの実体はパッケージ のこともあります • PyPI からインストールできる、複数のモジュールを集めた egg ファイルや whl ファイルのことを文脈に よってはパッケージと呼ぶこともありますが、名前空間パッケージに関する PEP などではそれらを ディス トリビューション (=配布物) と呼んで区別しています 用語の整理 2. 名前空間パッケージ 21
  22. モジュールオブジェクトの持つ属性 備考: • __path__ はサブモジュールを検索するときに sys.path の代わりに使われる • スクリプトとして実行された場合 __name__

    は “__main__” になる • モジュールの階層とディレクトリ階層は必ずしも一致しないことに注意 : ex) os.path モジュール (csv) パッケージ (json) __name__ ‘csv’ ‘json’ __file__ '/usr/lib/python3.4/csv.py' '/usr/lib/python3.4/json/__init__.py' __path__ × (属性を持たない) ['/usr/lib/python3.4/json'] __package__ “” (以前は None) 'json' 2. 名前空間パッケージ 22
  23. 名前空間パッケージはなぜ必要か 一つの理由は、関連性のある複数の配布物 (= egg ファイルや whl ファイルなど) を上 位のパッケージ名でまとめたいから Perl

    の配布物の例 • Net::Twitter::Lite • Net::Twitter::Queue • Net::Twitter::Stream • … Python の配布物の例 • python-twitter • pytwitter • twitter_cient • TwitterAPI • twitter_api • … 2. 名前空間パッケージ 23
  24. 名前空間パッケージの利用状況 名前空間パッケージを採用しているプロジェクトの例: Zope >>> import zope.interface # pip install zope.interface

    >>> import zope.deprecation # pip install zope.deprecation Sphinx >>> import sphinxcontrib.blockdiag # pip install sphinxcontrib-blockdiag >>> import sphinxcontrib.httpdomain # pip install sphinxcontrib-httpdomain 2. 名前空間パッケージ 24
  25. 名前空間パッケージの利用状況 名前空間パッケージを採用していないプロジェクトの例: Pyramid >>> import pyramid_debugtoolbar # pip install pyramid-debugtoolbar

    >>> import pyramid_mako # pip install pyramid-mako >>> import pyramid_jinja2 # pip install pyramid-jinja2 Django (パッケージ名がフリーダムすぎる) >>> import debug_toolbar # pip install django-debug-toolbar >>> import django_extensions # pip install django-extensions >>> import django_filters # pip install django-filter >>> import django_redis # pip install django-redis >>> import rest_framework # pip install djangorestframework 2. 名前空間パッケージ 25
  26. Python 2 の時代にはいくつかの方式がありました。 • pkgutil.extend_path() : 標準ライブラリ (Python 2.3 〜)

    • pkg_resources.declare_namespace() : setuptools declare_namespace() の方が高機能ですが、 setuptools がインストールされていない環境ではエラーになる ため、次のような合わせ技でよく使われていました : モジュールオブジェクトの __path__ 属性を直接操作することで名前空間パッケージを実現する =明示的な名前空間パッケージ 伝統的な名前空間パッケージ 2. 名前空間パッケージ 26
  27. 伝統的な名前空間パッケージの欠点 • __path__ を操作する方法が標準化されていない ◦ 複数の互換性のない方式が乱立している • __init__.py の存在が無駄 ◦

    すべての__init__.py に同じ内容の定型的なコードを書かないといけない ◦ どのディレクトリの__init__.py が実際に読まれるかは不定なので、 __init__.py に意味のある内容 を書けない • __init__.py の存在が邪魔 ◦ Linux ディストリビューションなどが複数のパッケージを同じディレクトリにインストールしたいときに __init__.py がコンフリクトする 2. 名前空間パッケージ 27
  28. 名前空間パッケージ関連の PEP • PEP 382 Namespace Packages [Rejected] • PEP

    402 Simplified Package Layout and Partitioning [Rejected] • PEP 420 Implicit Namespace Packages 最初の PEP が提案されてから実装まで実に3年かかっています。 2. 名前空間パッケージ 28
  29. PEP 420 Implicit Namespace Packages リリースバージョン: Python 3.3 この PEP

    では、これまでの名前空間パッケージに代わる新しい「暗黙の名前空間パッ ケージ」の仕様を提案しました。 • この名前空間パッケージの仕様では __init__.py は不要になります (というか置い てはいけない) • パッケージの検索は最初にインポートした時だけでなく、毎回のインポート時にも行 われます。ただしパフォーマンスのため、 sys.path や親パッケージの __path__ 属 性が変更されない限り、前回計算した値が使われ続けます。 • 暗黙の名前空間パッケージの __path__ 属性は普通のリストではなく、独自の iterable (_NamespacePath 型) になります。 2. 名前空間パッケージ 29
  30. アジェンダ 1. インポートフック 2. 名前空間パッケージ 3. 相対インポート 30

  31. PEP 328 Imports: Multi-Line and Absolute/Relative リリースバージョン: Python 2.5 A.B

    というモジュールで import X をしたときに、 • X がインポートされるのが絶対インポート • A.X がインポートされるのが(暗黙の)相対インポート です。以前の Python では、 A.X というモジュールがあれば相対インポート、 A.X がなければ絶対インポートさ れていました。 しかし、この挙動は、どのモジュールがインポートされるか分かりにくい、 A.X というモジュールが存在すること で X を隠してしまい容易にインポートできないといった問題を生じるため、望ましくないということになりました。 (X が os や string のような標準ライブラリだった場合を想像してください ) 3. 相対インポート 31
  32. PEP 328 Imports: Multi-Line and Absolute/Relative そのため、 PEP 328 で相対インポートのルールが変わりました。

    • Python 3 では暗黙の相対インポートはなくなり、常に絶対インポートされる • Python 2 でもファイル先頭に from __future__ import absolute_import を書くと 絶対インポート優先になる • 相対インポートがしたい場合は、明示的な相対インポート import .X を使う 3. 相対インポート 32
  33. PEP 338 Executing modules as scripts リリースバージョン: Python 2.5 Python

    2.4 で、コマンドラインからモジュールを直接実行する -m オプションが追加されました。この機能は、当 初はサブモジュールを指定することはできず、単にモジュールファイルパスを指定して実行するのと同じ動作で した。 PEP 338 は -m オプションでサブモジュールも指定できるようにするもので、インポートフックが使われていま す。 具体的な仕組みは、途中まで普通にインポートするのと同じように処理して、最後にモジュールのトップレベル の式を実行するところで専用の関数を呼び出すというものです。 3. 相対インポート 33
  34. PEP 366 Main module explicit relative imports リリースバージョン: Python 2.6

    PEP 328 と PEP 338 の組み合わせで、スクリプトとして実行されたサブモジュールからの相対インポートが動 かなくなりました。これは、以下の仕様の不整合によるものです : • 相対インポートはインポート先の完全修飾モジュール名を解決するために元のモジュールの __name__ 属性を使っている • 実行モジュールの __name__ は必ず “__main__” になる そこで、モジュールオブジェクトに __package__ 属性を追加して、相対インポートのときに __package__ 属性 を使うようにしたのが PEP 366 です。 3. 相対インポート 34
  35. PEP 366 Main module explicit relative imports __name__ と__package__ の関係:

    3. 相対インポート 35
  36. Python インポートシステムの歴史 • 2003年 Python 2.3 リリース ◦ PEP 302

    で提案された「新しいインポートフック」が追加された ◦ pkgutil モジュールが追加された • 2006年 Python 2.5 リリース ◦ PEP 328 で提案された相対インポート記法が追加された (暗黙の相対インポートは廃止 ) • 2008年 Python 3.0 リリース ◦ imputil モジュールが削除された • 2009年 Python 3.1 リリース ◦ importlib モジュールが追加された • 2012年 Python 3.3 リリース ◦ PEP 420 で提案された暗黙的な名前空間パッケージが追加された • 2014年 Python 3.4 リリース ◦ PEP 451 で提案された ModuleSpec が追加された ◦ imp モジュールが廃止された -> importlib モジュールに統合 36
  37. おわりに 「インポートフック」「名前空間パッケージ」「相対インポート」について、 現状とこれまでの歴史を簡単に説明しました 今回の学び: • Python のインポートシステムは、これまでバージョンアップとともに根本的な改良 が何度も加えられてきた • エンドユーザに大きな混乱を与えることなく、後方互換性と根本的な改良をいかに

    両立してきたか • 仕様に関する議論の過程が PEP にまとめられていることで、あとからなぜそのよう な仕様になったのか経緯が分かるのは素晴らしい 37
  38. 参考資料 Python 3.5 言語リファレンス » 5. インポートシステム Python 3.5 標準ライブラリ

    » 31.5. importlib PEP 273 -- Import Modules from Zip Archives PEP 302 -- New Import Hooks PEP 328 -- Imports: Multi-Line and Absolute/Relative PEP 338 -- Executing modules as scripts PEP 366 -- Main module explicit relative imports PEP 382 -- Namespace Packages [Rejected] PEP 395 -- Qualified Names for Modules [Withdrawn] PEP 402 -- Simplified Package Layout and Partitioning [Rejected] PEP 420 -- Implicit Namespace Packages PEP 451 -- A ModuleSpec Type for the Import System PEP 3122 -- Delineation of the main module [Rejected] 38