Pythonコードの解析とpycファイル

このガイドでは、PythonファイルがGraalVM Pythonランタイムでどのように解析されるかについて詳しく説明します。

パーサーのパフォーマンス

シリアライズされた.pycファイルからコードをロードする方が、ANTLRを使用して.pyファイルを解析するよりも高速です。

Pythonソースの抽象構文ツリー(AST)の作成には、次の2つのフェーズがあります。最初のフェーズでは、単純な構文ツリー(SST)とスコープ・ツリーが作成されます。2番目のフェーズでは、SSTが言語実装フレームワーク・ツリーに変換されます。

変換には、スコープ・ツリーが必要です。スコープ・ツリーには、変数および関数定義のスコープ位置と、スコープに関する情報が含まれています。単純な構文ツリーには、ソースをミラーリングするノードが含まれています。SSTと言語実装フレームワーク・ツリーを比較すると、SSTの方がはるかに小さくなります。これには、単純な方法でソースを表すノードのみが含まれています。通常は、1つのSSTノードが多数の言語実装フレームワーク・ノードに変換されます。

単純な構文ツリーは、ANTLR解析または適切な*.pycファイルからのデシリアライズという2つの方法で作成できます。ソースについて適切な.pycファイルがない場合は、ANTLRを使用してソースが解析されます。Pythonの標準のインポート・ロジックで適切な.pycファイルが見つかった場合は、単にそれからのSSTとスコープ・ツリーのデシリアライズがトリガーされます。

デシリアライズは、ANTLRを使用したソース解析よりもはるかに高速で、所要時間はANTLRが必要とする時間のわずか約30%です。当然ながら、新しいファイルの最初のインポートには少し時間がかかります。ANTLRを使用した解析に加えて、Pythonの標準のライブラリ・インポート・ロジックでは、結果のコード・オブジェクトが.pycファイルにシリアライズされます。つまり、ここでは、SSTとスコープ・ツリーがこのようなファイルにシリアライズされます。

pycファイルの作成および管理

目的の.pyファイルと一致する.pycファイルが見つからないか、無効なファイルが見つかった場合、.pycファイルはGraalVM Pythonランタイムによって自動的に作成されます。

Pythonソース・ファイル(モジュール)が最初の実行中にインポートされたときに、適切な.pycファイルが自動的に作成されます。同じモジュールが再度インポートされたときには、すでに作成されている.pycファイルが使用されます。つまり、まだ実行(インポート)されていないソース・ファイルについては.pycファイルはありません。.pycファイルの作成は完全にFileSystem APIを介して行われ、埋込み担当者はファイルシステム・アクセスを管理できます。

その後、スクリプトを実行するたびに、既存の.pycファイルが再利用されるか、新しいものが生成されます。元のソース・ファイルのタイムスタンプまたはハッシュコードが変更されると、.pycファイルが再生成されます。ハッシュコードは、source.hashCode()をコールすることによってPythonソースに基づいてのみ生成され、java.util.Arrays.hashCode(byte[])で計算された、ソース・ファイル・バイトの配列に対するJDKハッシュコードです。

.pycファイルは、Pythonパーサーのマジック番号が変更された場合にも再生成されます。マジック番号はPythonソースでハードコードされており、ユーザーが変更することはできません(当然ながら、そのユーザーがPythonのバイトコードへのアクセス権を持っている場合を除きます)。

SSTまたはスコープ・ツリーのバイナリ・データの形式が変更されると、GraalVMのPythonランタイムの開発者はマジック番号を変更します。これは実装の詳細であるため、マジック番号はGraalVMのPythonランタイムのバージョンに対応している必要はありません(CPythonの場合と同様)。pycのマジック番号は、実行されている具体的なPythonランタイムJavaコードに応じて変わります。

.pycファイルを使用する場合、バージョンを切り替えるときまたは元のソース・コードを変更するときには少なくともGraalVMのPythonランタイムへの書込みアクセスを許可する必要があります。そうしないと、ソース・ファイルの再生成が失敗し、インポートのたびに、古い.pycファイルへのアクセス、コードの解析、そのシリアライズ、および新しい.pycファイルの記述の試行(および失敗)というオーバーヘッドが発生します。

*.pycファイルは、GraalVMのPythonランタイムによって削除されることはなく、再生成されるのみです。これは、該当するソース・ファイルが変更されたとき(内容の最終変更のタイムスタンプまたはハッシュコード)、またはPython実装パーサーのマジック番号が変更されたときに再生成されます。マジック番号の変更はリリース・ノートで通知されるため、埋込み担当者またはシステム管理者はアップグレード時に古い.pycファイルを削除できます。

.pycファイルについて作成されるフォルダ構造は次のようになります:

top_folder
    __pycache__
         sourceA.graalpython.pyc
         sourceB.graalpython.pyc
    sourceA.py
    sourceB.py
    sub_folder
        __pycache__
            sourceX.graalpython.pyc
        sourceX.py

デフォルトでは、__pycache__ディレクトリはソース・コード・ファイルと同じディレクトリ・レベルに作成され、このディレクトリに、同じディレクトリのすべての.pycファイルが格納されます。このフォルダには、異なるバージョンのPython (CPythonなどを含む)で作成された.pycファイルが格納されることがあるため、ユーザーには、*.cpython3-6.pycなどで終わるファイルが表示される場合があります。

現在の実装では、.pycファイルに元のソース・テキストのコピーも含まれます。これは軽微なパフォーマンス最適化であるため、元のソース・ファイルへのパスを使用してSourceオブジェクトを作成できますが、元の*.pyファイルを読み取る必要はありません。これにより、言語実装フレームワーク・ツリーを取得するプロセスが高速化されます(1つのファイルのみが読み取られます)。.graalpython.pycファイルの構造は次のとおりです:

MAGIC_NUMBER
source text
binary data - scope tree
binary data - simple syntax tree

元のソースを引き続きリカバリできるため、.pycファイルは、ゲスト・コードからPythonライブラリ・ソース・コードを見えなくするための効果的な手段ではありません。ソースが省略されていても、構文ツリーには十分な情報が含まれているため、簡単にソース・コードに逆コンパイルできます。

シリアライズされたSSTおよびスコープ・ツリーは、属性co_code (CPythonではバイトコードを含む)の内容として、Pythonのcodeオブジェクトにも格納されます。たとえば:

>>> def add(x, y):
...   return x+y
...
>>> add.__code__.co_code
b'\x01\x00\x00\x02[]K\xbf\xd1\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00 ...'

.pycファイルは主に、CPythonと互換性のある方法でランタイムによって自動的に管理されます。CPythonの場合と同様に、それらの場所と、それらを記述する必要があるかどうかを指定するオプションがあり、これらのオプションの両方をゲスト・コードで変更できます。

*.pycファイルの作成は、CPythonの場合と同じ方法で制御できます(https://docs.python.org/3/using/cmdline.htmlを参照):

埋込み担当者は、環境変数またはCPythonオプションを使用してこれらのオプションをGraalVMのPythonの実装に通信することはできないため、これらのオプションを次の言語オプションとして使用できます:

Pythonコンテキストでは、.pycファイルの記述がデフォルトで有効になりません。graalpythonランチャによってこれがデフォルトで有効になりますが、埋込みユースケースでこれが必要な場合は、__pycache__の場所が適切に管理され、その場所にあるファイルが導出元のソース.pyファイルと同様に操作に対して保護されるように注意を払う必要があります。

また、アプリケーション・ソースをGraalVM EntepriseのPythonランタイムにアップグレードするには、埋込み担当者が必要に応じて古い.pycファイルを削除する必要があります。

セキュリティに関する考慮事項

SSTおよびスコープ・ツリーのシリアライズは手動で記述され、デシリアライズ中にSSTノード以外のクラスをロードすることはできません。Javaオブジェクトのシリアライズには、Javaシリアライズまたはその他のフレームワークは使用されません。その主な理由はパフォーマンスですが、これには、悪意を持って作成された.pycファイルによってクラスのロードを強制できないという効果があります。

ファイル操作(データおよびタイムスタンプの取得やpycファイルの記述)はすべて、FileSystem APIを介して行われます。埋込み担当者は、カスタム(読取り専用など)のFileSystem実装を使用して、これらの操作すべてを変更できます。また、埋込み担当者は、GraalVMのPythonランタイムについてI/O権限を無効にすることにより、.pycファイルの作成を事実上無効にすることもできます。

.pycファイルが読取り可能でない場合、その場所は書込み可能ではありません。.pycファイルのシリアライズ・データまたはマジック番号がなんらかの形で破損している場合、デシリアライズは失敗し、単に.pyファイルが再度解析されます。これによって、モジュールの解析についてのみ、パフォーマンスにわずかな影響が生じますが、ほとんどのアプリケーションにとって重大ではありません(アプリケーションがPythonコードのロード以外に実際の作業を行う場合)。