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は、最も広く知られおいるナヌスケヌスです。

Source: https://habr.com/ru/post/J208378/


All Articles