Pythonのcompile()関数を徹底解説!文字列コードを効率的に実行する


 

Pythonでプログラミングをしていると、文字列として書かれたPythonコードを動的に実行したいという場面に出くわすことがあります。例えば、ユーザーが入力したスクリプトを実行したり、テンプレートエンジンで動的にコードを生成したりする場合です。このようなとき、これまでに解説したeval()exec()関数が役立ちますが、同じコードを何度も実行する必要がある場合、毎回文字列からコードに変換するのは非効率です。

ここで登場するのが、Pythonの組み込み関数である**compile()関数**です。compile()は、文字列のPythonコードを、Pythonインタプリタが直接理解できる「コードオブジェクト」に前もって変換(コンパイル)します。これにより、同じコードを複数回実行する際のパフォーマンスが向上します。

この記事では、compile()関数の基本的な使い方から、その役割、eval()exec()との連携、そして特に注意すべき潜在的なセキュリティリスクまでを初心者にもわかりやすく解説しますします。

 

compile()関数とは?文字列コードを「事前準備」する

 

Pythonのcompile()関数は、Pythonコードを含む文字列やバイト列を、Pythonが実行できる**「コードオブジェクト(code object)」**に変換する組み込み関数です。一度コードオブジェクトにコンパイルしてしまえば、それをeval()exec()に渡すことで、繰り返し効率的に実行できます。

 

基本的な使い方

 

compile()関数は、少なくとも3つの必須引数を取ります。

  1. source (必須): コンパイルしたいPythonコードを含む文字列、バイト列、またはASTオブジェクトです。

  2. filename (必須): コードの出典を示す文字列です。これはデバッグ時のスタックトレースなどで表示されるファイル名のようなもので、実際のファイルが存在する必要はありません。慣例として<string><stdin>などが使われます。

  3. mode (必須): コンパイルするコードの種類を示す文字列です。以下のいずれかを指定します。

    • 'eval': 1つの単一の式(expression)をコンパイルします。eval()関数で実行するために使われます。

    • 'exec': 任意のPythonの文(statement)のシーケンスをコンパイルします。exec()関数で実行するために使われます。

    • 'single': 1つの単一の対話型文をコンパイルします。結果がNoneでない場合に、その結果を出力します。主にPython対話型シェルで使われます。

Python
 
# 'eval' モードの例(式をコンパイル)
expression_string = "10 + 20 * 3"
compiled_expression = compile(expression_string, '<string>', 'eval')
print(f"コンパイルされた式の型: {type(compiled_expression)}") # 出力: コンパイルされた式の型: <class 'code'>
print(f"コンパイルされた式の評価結果: {eval(compiled_expression)}") # 出力: コンパイルされた式の評価結果: 70

# 'exec' モードの例(文をコンパイル)
statement_string = """
x = 5
y = 7
print(f"x * y = {x * y}")
"""
compiled_statements = compile(statement_string, '<script>', 'exec')
print(f"コンパイルされた文の型: {type(compiled_statements)}") # 出力: コンパイルされた文の型: <class 'code'>
exec(compiled_statements) # 出力: x * y = 35

# 'single' モードの例(インタラクティブな文をコンパイル)
single_statement = "2 + 2"
compiled_single = compile(single_statement, '<stdin>', 'single')
exec(compiled_single) # 出力: 4 (対話型シェルでの実行結果と同様に、結果が自動的に出力される)

コンパイルされたコードオブジェクトは、その後何度でもeval()exec()に渡して実行できます。

 

compile()関数の役割と活用事例

 

compile()関数を使う最大の理由は、パフォーマンスの向上コードの管理です。

 

1. パフォーマンスの向上:繰り返し実行される動的コード

 

同じ文字列のPythonコードを何度もeval()exec()で実行する場合、Pythonインタプリタはその都度、文字列を解析し、バイトコードに変換する(コンパイルする)必要があります。このコンパイルのオーバーヘッドは、短いコードであれば気になりませんが、長いコードや頻繁に実行されるコードでは無視できません。

compile()を使って一度コードオブジェクトに変換しておけば、2回目以降の実行ではこのコンパイルのステップが不要になり、実行速度が向上します。

Python
 
import time

# 文字列のまま100万回実行
start_time = time.time()
for _ in range(1_000_000):
    eval("100 * 200 + 300 / 400")
end_time = time.time()
print(f"文字列のまま実行: {end_time - start_time:.4f}秒")

# compile()で一度コンパイルしてから100万回実行
compiled_code = compile("100 * 200 + 300 / 400", '<string>', 'eval')
start_time = time.time()
for _ in range(1_000_000):
    eval(compiled_code)
end_time = time.time()
print(f"コンパイルしてから実行: {end_time - start_time:.4f}秒")
# 出力例:
# 文字列のまま実行: 0.1500秒
# コンパイルしてから実行: 0.0500秒
# (環境により差はありますが、コンパイル済みのほうが速くなることがわかります)

 

2. コードのキャッシュと再利用

 

動的に生成されるコードが、ある程度決まったパターンを持つ場合、そのパターンをcompile()でコンパイルしてキャッシュしておくことで、システムの起動時間やレスポンスタイムを改善できます。

 

3. プラグインシステムやコードジェネレーター

 

より高度なプラグインシステムや、プログラムがプログラムを生成するような「コードジェネレーター」を開発する際に、生成された文字列コードをcompile()で準備し、安全な環境で実行するために利用されることがあります。

 

compile()関数とセキュリティリスク:eval()/exec()と同様の注意点

 

compile()関数自体はコードを実行しませんが、その出力であるコードオブジェクトがeval()exec()に渡されることを考えると、eval()exec()と同様に深刻なセキュリティリスクを伴います。

信頼できないソース(例: ユーザーからの入力、外部ファイル)から取得した文字列をcompile()に渡し、その結果をeval()exec()で実行することは、絶対に避けてください。

 

危険性の再確認

 

もし悪意のあるコードが含まれた文字列をcompile()し、それを実行した場合、システムに致命的な損害を与える可能性があります。例えば、ファイルの削除、機密情報の窃取、ネットワークへの不正アクセスなど、あらゆる操作が実行されてしまいます。

Python
 
# 例: ユーザー入力からコンパイルする(非常に危険な例!)
# user_input_code = input("実行したいPythonコードを入力してください: ")
# compiled_malicious = compile(user_input_code, '<user_input>', 'exec')
# exec(compiled_malicious) # <-- 絶対にやってはいけない!

 

安全な利用のための原則

 

eval()exec()と同様に、compile()でコンパイルしたコードを実行する場合は、以下の原則を厳守してください。

  1. 信頼できるソースからの入力のみ: compile()に渡す文字列は、完全に信頼できるソース(自分で書いたコードの一部、信頼できる設定ファイルなど)からのみ取得してください。

  2. eval()/exec()実行時の環境制限: eval()exec()globalslocals引数を渡し、__builtins__を制限することで、悪意のあるコードがアクセスできる範囲を最小限に抑えます。これはcompile()の引数ではなく、**実行時(eval/execの呼び出し時)**に設定します。

    Python
     
    # 安全な環境の構築例(再掲)
    safe_builtins = {'print': print, 'sum': sum, 'len': len, 'int': int, 'float': float}
    safe_globals = {'__builtins__': safe_builtins}
    
    user_code = "my_var = 10 + len('abc'); print(my_var)"
    
    try:
        # コンパイルは実行環境を制限しないが、実行時に制限が適用される
        compiled_user_code = compile(user_code, '<user_script>', 'exec')
        exec(compiled_user_code, safe_globals, {})
    except Exception as e:
        print(f"安全な環境でコード実行中にエラー: {e}")
    
    # 悪意のあるコードの試行
    malicious_code = "__import__('os').system('echo malicious')"
    try:
        compiled_malicious = compile(malicious_code, '<user_script>', 'exec')
        exec(compiled_malicious, safe_globals, {})
    except NameError as e: # __import__ が含まれていないため NameError
        print(f"安全な環境で悪意のあるコードをブロック: {e}")
    
  3. より安全な代替手段の検討: ほとんどの場合、compile() + eval()/exec()を使わずに問題を解決できるより安全な方法が存在します。

    • 数式評価には ast.literal_eval()

    • 設定ファイル読み込みには json, yaml, configparser

    • 動的なメソッド呼び出しには getattr()

    • 大規模なスクリプト実行には、サンドボックス環境での分離。

 

まとめ

 

Pythonのcompile()関数は、文字列やバイト列のPythonコードを効率的な「コードオブジェクト」に変換するための組み込み関数です。これにより、同じコードを複数回実行する際のパフォーマンスを向上させることができます。しかし、その強力な機能ゆえに、eval()exec()と同様に、信頼できないソースからの入力を処理する際には重大なセキュリティリスクを伴います。

  • compile(source, filename, mode): ソースコードをコードオブジェクトに変換します。

    • mode'eval', 'exec', 'single' のいずれかを指定します。

  • パフォーマンスの向上(特に繰り返し実行されるコード)、コードのキャッシュと再利用に役立ちます。

  • eval()exec()と組み合わせて使用しますが、セキュリティ対策(実行環境の厳格な制限)が必須です。

  • 信頼できない入力に対しては、より安全な代替手段を優先的に検討すべきです。

この関数を使用する際は、その利点と潜在的な危険性を十分に理解し、厳重なセキュリティ対策を講じるか、より安全な代替手段を検討するようにしてください。


 

■プロンプトだけでオリジナルアプリを開発・公開してみた!!

■AI時代の第一歩!「AI駆動開発コース」はじめました!

テックジム東京本校で先行開始。

■テックジム東京本校

「武田塾」のプログラミング版といえば「テックジム」。
講義動画なし、教科書なし。「進捗管理とコーチング」で効率学習。
より早く、より安く、しかも対面型のプログラミングスクールです。

<短期講習>5日で5万円の「Pythonミニキャンプ」開催中。

<月1開催>放送作家による映像ディレクター養成講座

<オンライン無料>ゼロから始めるPython爆速講座