Pythonでのzipモジュールの作成

背景



アクロニステクノロジーの開発の特定の段階で、製品の一部として独自のアセンブリのpython3言語インタープリターを配布し、これらの製品のインフラストラクチャへのアクセスを提供する独自のモジュールで拡張する可能性を検討することが決定されました。 この投稿は、この方向の研究結果の1つです。



まず、限られたコンパクトな有限再配布可能モジュールのセットが必要でした。 ただし、 python.orgを介して配布される公開pythonアセンブリにはこれがありません。標準ライブラリだけは、言語自体の不可欠な部分であり、1000を超えるpyファイルで構成されています。 そのため、1つまたは複数のモジュールに関連するPythonのソースコードのセット全体がzipアーカイブにパックされ、1つのzipファイルで配布される場合、zipアーカイブにあるモジュールをインポートする機能など、インタープリターの興味深い機能にすぐに注目しました。



振り返ってみると、Pythonでのzipモジュールの操作のサポートは強力で便利なものであると自信を持って言えます。 そして、それは機能し、うまく機能します。 zip-peggingの精神を吹き込んだzipモジュールの一連の実験の後、Python言語の標準ライブラリ全体(そのスクリプト部分)も別のzipファイルにパックされるようになりました。



開始する



まず、できるだけシンプルなテスト環境を作成しますが、同時に、説明した機能のすべての意図された機能を実証するのに十分です。 環境はWindowsになるので、現時点では私にとってより便利であることがわかりました。 ここに挙げたLinuxの例を試してみたい人のために、基本的な違いはないはずです。必要なのは、Linuxディストリビューションのパッケージマネージャー、または古き良きconfigure / make / make installを通じてインストールされたpython3だけです



zipでパックする簡単なデモモジュールは、最初はd:\ habr \ libにあります。



とりわけ、いくつかのモジュールを1つのzipファイルにパックする機能を実証したかったため、ここでは異なるタイプの2つのモジュールを作成しました。最初のsay_hello



モジュールはsay_hello()



関数がsay_hello()



れたsay_hello.py



ファイルで構成され、2番目のmy_sysinfo



モジュールmy_sysinfo



もう少し複雑になります-インポートリストにprint_sysinfo



関数を含む__init__.py



ファイルを含むディレクトリの形式。 今後、 sys.version



などの要約情報の中でも特にこの関数は、zipページングの機能を明らかにするための独自の呼び出しスタックも印刷するとすぐに言います。



すべてがアンパック形式で機能することを確認します。

 c:\Python33\python.exe
      
      





 Python 3.3.2 (v3.3.2:d047928ae3f6, May 16 2013, 00:03:43) [MSC v.1600 32 bit (Intel)] on win32 Type "help", "copyright", "credits" or "license" for more information. >>> import sys >>> sys.path.insert(0,'d:\\habr\\lib') >>> import say_hello >>> say_hello.say_hello() Hello python world. >>> import my_sysinfo >>> my_sysinfo.print_sysinfo() -------------------------------------------------------------------------------- 3.3.2 (v3.3.2:d047928ae3f6, May 16 2013, 00:03:43) [MSC v.1600 32 bit (Intel)] -------------------------------------------------------------------------------- File "<stdin>", line 1, in <module> File "d:\habr\lib\my_sysinfo\sysinfo.py", line 9, in print_sysinfo traceback.print_stack() --------------------------------------------------------------------------------
      
      







ジップパッキング



zip形式のソースpyファイルのパッケージには秘密がありません。 これを行うには、指先で利用できる任意のzipアーカイバを使用するか、標準のzipfile



モジュールの機能を使用してpythonスクリプトで直接パックします。 後で、 mkpyzip.pyという名前を付けてd:\ habr \ toolsフォルダーに入れる単純なパッケージスクリプトのコードを提供します。



このスクリプトを使用して、上記のモジュールをzipファイルd:\ habr \ output \ mybundle.zipにパックします。

 :\Python33\python.exe d:\habr\tools\mkpyzip.py --src d:\habr\lib\my_sysinfo d:\habr\lib\say_hello.py --out d:\habr\output\mybundle.zip ::: d:\habr\lib\my_sysinfo\__init__.py >>> mybundle.zip/my_sysinfo/__init__.py ::: d:\habr\lib\my_sysinfo\sysinfo.py >>> mybundle.zip/my_sysinfo/sysinfo.py ::: d:\habr\lib\say_hello.py >>> mybundle.zip/say_hello.py
      
      



とりわけ、どのファイルおよびどの名前でzipアーカイブにパックされるかについての詳細な結論がこのスクリプトに追加されました。



このようなzipアーカイブにパッケージ化すると、すべてが機能することを確認します。

 c:\Python33\python.exe
      
      





 Python 3.3.2 (v3.3.2:d047928ae3f6, May 16 2013, 00:03:43) [MSC v.1600 32 bit (Intel)] on win32 Type "help", "copyright", "credits" or "license" for more information. >>> import sys >>> sys.path.insert(0, 'd:\\habr\\output\\mybundle.zip') >>> import say_hello >>> say_hello.say_hello() Hello python world. >>> import my_sysinfo >>> my_sysinfo.print_sysinfo() -------------------------------------------------------------------------------- 3.3.2 (v3.3.2:d047928ae3f6, May 16 2013, 00:03:43) [MSC v.1600 32 bit (Intel)] -------------------------------------------------------------------------------- File "<stdin>", line 1, in <module> File "d:\habr\output\mybundle.zip\my_sysinfo\sysinfo.py", line 9, in print_sysinfo traceback.print_stack() --------------------------------------------------------------------------------
      
      



my_sysinfo.print_sysinfo()



から、すべてが期待どおりに動作し、zipアーカイブにパックされていることがmy_sysinfo.print_sysinfo()



ます。特に、関数my_sysinfo.print_sysinfo()



からのスタックのプリントアウトは、呼び出された関数のコードがzipファイル内にあることを示します-d:\ habr \ output \ mybundle .zip \ my_sysinfo \ sysinfo.py



zipでパックするときのバイトコード生成



モジュールのインポート時にバイトコードを生成する、またはインポート時に有効な場合は以前に生成されたバイトコードをロードして実行するなど、インタープリターのよく知られている機能を思い出してください。 zipでパッケージ化されたモジュールの場合、状況は多少異なります。 zipモジュールの場合、バイトコードを事前に生成してzipファイルにパッケージ化する必要があります。そうでない場合、インタープリターは、zipファイルからモジュールをインポートするたびに再起動するたびにメモリにバイトコードを生成します。 さて、 mkpyzip.pyスクリプトでは、 バイトコード生成が既に提供されています--mkpyc



オプションを追加して、zipファイルを再生成するだけです。

 c:\Python33\python.exe d:\habr\tools\mkpyzip.py --mkpyc --src d:\habr\lib\my_sysinfo d:\habr\lib\say_hello.py --out d:\habr\output\mybundle.zip ::: d:\habr\lib\my_sysinfo\__init__.py >>> mybundle.zip/my_sysinfo/__init__.py ::: mkpyc for: d:\habr\lib\my_sysinfo\__init__.py >>> mybundle.zip/my_sysinfo/__init__.pyc ::: d:\habr\lib\my_sysinfo\sysinfo.py >>> mybundle.zip/my_sysinfo/sysinfo.py ::: mkpyc for: d:\habr\lib\my_sysinfo\sysinfo.py >>> mybundle.zip/my_sysinfo/sysinfo.pyc ::: d:\habr\lib\say_hello.py >>> mybundle.zip/say_hello.py ::: mkpyc for: d:\habr\lib\say_hello.py >>> mybundle.zip/say_hello.pyc
      
      





pythonモジュールをzipファイルにパックする基本的な側面が明らかになったので、mkpyzip.pyユーティリティ自体のコードを持ち込むときです。 このスクリプトには特別なものはなく、バイトコードを生成するためのプロトタイプは標準のpython言語ライブラリから借用されていることにすぐに気付きます(このプロトタイプを検索するには、wr_longキーワードを検索してください)。

mkpyzip.py
 import argparse import imp import io import marshal import os import os.path import zipfile def compile_file(filename, codename, out): def wr_long(f, x): f.write(bytes([x & 0xff, (x >> 8) & 0xff, (x >> 16) & 0xff, (x >> 24) & 0xff])) with io.open(filename, mode='rt', encoding='utf8') as f: source = f.read() ast = compile(source, codename, 'exec', optimize=1) st = os.fstat(f.fileno()) timestamp = int(st.st_mtime) size = st.st_size & 0xFFFFFFFF out.write(b'\0\0\0\0') wr_long(out, timestamp) wr_long(out, size) marshal.dump(ast, out) out.flush() out.seek(0, 0) out.write(imp.get_magic()) def compile_in_memory(source, codename): with io.BytesIO() as fc: compile_file(source, codename, fc) return fc.getvalue() def make_module_catalog(src): root_path = os.path.abspath(os.path.normpath(src)) root_arcname = os.path.basename(root_path) if not os.path.isdir(root_path): return [(root_path, root_arcname)] catalog = [] subdirs = [(root_path, root_arcname)] while subdirs: idx = len(subdirs) - 1 subdir_path, subdir_archname = subdirs[idx] del subdirs[idx] for item in sorted(os.listdir(subdir_path)): if item == '__pycache__' or item.endswith('.pyc'): continue item_path = os.path.join(subdir_path, item) item_arcname = '/'.join([subdir_archname, item]) if os.path.isdir(item_path): subdirs.append((item_path, item_arcname)) else: catalog.append((item_path, item_arcname)) return catalog def mk_pyzip(sources, outzip, mkpyc=False): zipfilename = os.path.abspath(os.path.normpath(outzip)) display_zipname = os.path.basename(zipfilename) with zipfile.ZipFile(zipfilename, "w", zipfile.ZIP_DEFLATED) as fzip: for src in sources: catalog = make_module_catalog(src) for entry in catalog: fname, arcname = entry[0], entry[1] fzip.write(fname, arcname) print("::: {} >>> {}/{}".format(fname, display_zipname, arcname)) if mkpyc and arcname.endswith('.py'): bytes = compile_in_memory(fname, arcname) pyc_name = ''.join([os.path.splitext(arcname)[0], '.pyc']) fzip.writestr(pyc_name, bytes) print("::: mkpyc for: {} >>> {}/{}".format(fname, display_zipname, pyc_name)) def main(): parser = argparse.ArgumentParser() parser.add_argument('--src', nargs='+', required=True) parser.add_argument('--out', required=True) parser.add_argument('--mkpyc', action='store_true') args = parser.parse_args() mk_pyzip(args.src, args.out, args.mkpyc) if __name__ == '__main__': main()
      
      







バイトコードの有効性



また、生成したバイトコードが有効であり、モジュールをインポートするときにインタープリターがメモリで新しいバイトコードを再生成しようとせずに正常に取得することを確認する方法についてもいくつか説明します。

これを行うには、 __file__



say_helloモジュールの__file__



属性__file__



出力します。

 c:\Python33\python.exe
      
      





 Python 3.3.2 (v3.3.2:d047928ae3f6, May 16 2013, 00:03:43) [MSC v.1600 32 bit (Intel)] on win32 Type "help", "copyright", "credits" or "license" for more information. >>> import sys >>> sys.path.insert(0,'d:\\habr\\output\\mybundle.zip') >>> import say_hello >>> say_hello.__file__ 'd:\\habr\\output\\mybundle.zip\\say_hello.pyc'
      
      



ロードされたモジュールの__file__



属性__file__



生成したpycファイルを指しているという事実は、バイトコードの有効性の十分な証拠です。



これにより、明確な良心をもって、Python言語でのzipパッキングに関する入門レビューを完了することができます。



驚き



私の同僚の1人は、 Eclipseを取り上げ、有名なアドオンPyDevの助けを借りて、彼が書いたpythonスクリプトをデバッグしようとしました。これは、とりわけ、説明したテクノロジーを使用して圧縮されたpythonモジュールの機能を使用しました。



主な不快な驚きは、PyDevがそのようなモジュールのデビューを完全に拒否したことです。 この問題に強く興味を持ち、問題の原因を探し始めました。 さて、すでに振り返ってみると、PyDevに対する私たちの信念によれば、zipモジュールのデバッグに十分な品質のサポートがないということができます。



それでも、調査の時点では、PyDevでのデバッグの微妙な違いはすぐにレビューから除外されました。 pythonに組み込まれたpdbデバッガーは、非常に疑わしい種類の呼び出しスタックに関する情報も提供しました。 さらに、ソースpyファイルとともに、バイトコードを含むpycファイルもzipアーカイブにある場合にのみ、情報は疑わしかった。 pyファイルのみのzipアーカイブの場合、自動生成されたバイトコードは明らかに異なるものであり、pdbでのデバッグは正しい情報を提供し、苦情は発生しませんでした。 デバッグを除き、すべてが期待どおりに機能しました。 それでも、バイトコードには間違いがありました。 そして、pdbはこれを明らかに私たちに知らせました。



問題の原因が見つかったので、pdbの下でpythonコードをデバッグする詳細に進む気にはなりません。 問題の原因を明確にするために、前述のmy_sysinfoモジュールのprint_sysinfo()関数を使用して、圧縮されたバイトコードから呼び出しスタックを再印刷します。

 c:\Python33\python.exe
      
      





 Python 3.3.2 (v3.3.2:d047928ae3f6, May 16 2013, 00:03:43) [MSC v.1600 32 bit (Intel)] on win32 Type "help", "copyright", "credits" or "license" for more information. >>> import sys >>> sys.path.insert(0,'d:\\habr\\output\\mybundle.zip') >>> import my_sysinfo >>> my_sysinfo.print_sysinfo() -------------------------------------------------------------------------------- 3.3.2 (v3.3.2:d047928ae3f6, May 16 2013, 00:03:43) [MSC v.1600 32 bit (Intel)] -------------------------------------------------------------------------------- File "<stdin>", line 1, in <module> File "my_sysinfo/sysinfo.py", line 9, in print_sysinfo traceback.print_stack() --------------------------------------------------------------------------------
      
      





では、この出力を以前に受け取っ出力と比較して、独自のバイトコードの圧縮を開始する前にしましょう。 ここでの主な違いは、スタックフレーム内のファイルパスです。



zipファイルにバイトコードがない場合、次の形式の出力がありました。

「モジュール」のファイル「stdin」、1行目

ファイル " d:\ habr \ output \ mybundle.zip \ my_sysinfo \ sysinfo.py "、print_sysinfoの9行目

traceback.print_stack()



そして、バイトコードを追加した後、次の形式を取りました。

「モジュール」のファイル「stdin」、1行目

print_sysinfoの 9行my_sysinfo / sysinfo.py 」ファイル

traceback.print_stack()



出力から、コールスタックのzipアーカイブにバイトコードを追加すると、絶対パスからファイルへのパスが、さらにzipアーカイブのルートに対する相対パスに変わることが明らかになります。 ここで、注意深い読者は、 mkpyzip.pyユーティリティでcompile



れた組み込み関数へのこの相対パスを送信することにより、私たち自身がそのようなバイトコードを生成したことに直ちに反対するかもしれません。 しかし、これについてもう少し深く考えると、この場合の完全なパスは決して適切ではないことが明らかになります。なぜなら、最終的な目標は、あるマシンでzipアーカイブを収集し、別のマシンで、おそらくは別のオペレーティングシステム。



当時、私たちは誰もzipモジュールをインタープリターにロードする実装に精通していなかったため、問題の根本的な原因は何かという質問に明確な答えを出すことは不可能でした。 pythonのzipモジュールローダーがロード時に正しく動作しないかどうか。



最終的に、python -dev@python.orgを通じてpython開発者自身のアドバイスを求めることが決定されました 。 当時彼らが私たちにアドバイスしたのは、このトピックのバグを取得して、説明された問題のコンテキストが失われないようにすることだけでした。 バグbugs.python.org/issue18307を作成し、待ち始めました。 約1か月待機して他の同様に差し迫った問題に取り組んだ後、忍耐は静かに終わり、python33.dllはデバッガに入りました。



その結果、疑いを確認し、バイトコードのロード時に正しく動作しないのは、PythonのzipモジュールローダーのC-shnaya実装であると断言できます。 より正確には、zipファイルからロードされたときにバイトコード内のパスの自動正規化を必要とするここで説明されているケースは、単に実装されていません。 その結果、同じバグの一部として、この問題を修正し、コールスタック内のファイルパスを絶対形式に導くパッチを提案しました。



約半年後、 bugs.python.orgのこのバグは公開されたままです。 どうやらpythonのzipモジュールは機能ですが、強力ですが、めったに使用されないため、特にzipアーカイブ内のバイトコードの場合はそうです。 それにもかかわらず、Pythonソースを含む独自のリポジトリ(公開元にできるだけ近づけるようにしています)を使用して、このパッチに専念しました。



おわりに



zipアーカイブにパッケージ化されたPythonのモジュールは、展開された形式と同様に機能します。 準備が必要なのは、パッケージ化後、Eclipse + PyDevと他のIDEの両方でデバッグするのが難しい場合があることです。デバッグもPyDevに基づいています。 それにもかかわらず、特定の状況では、IDEでPythonコードを簡単にデバッグするよりも、バイナリの生産モジュールのコンパクトなセットを持つ能力の方が重要になる場合があります。



PS setuptools / eggsを発明しましたか? いや



PythonのZipモジュールは完全に独立した自給自足の機能であり、言語インタープリター自体のコアに組み込まれています。 setuptools / eggsは、最も広く知られているユースケースです。



All Articles