カテゴリー
Uncategorized

[Python] compile()

compile() によってソースコードをコードオブジェクト (types.CodeType) か AST (ast.AST) に変換できる。AST は再度 compile() することによってコードオブジェクトに変換できる。

最も身近なコードオブジェクトは関数オブジェクト (types.FunctionType) の __code__ 属性*1である。モジュールオブジェクト (types.ModuleType) からはコードオブジェクトを取得できないが、後記する方法で実行時に取得できる。

ソースコードの情報を取得したいときや改変したいときは AST に変換するとよい。AST については ast Abstract Syntax Trees と ast モジュールのソースコードを読むこと。

永続化

AST は pickle できる。また、コードオブジェクトは marshal できる。標準の状態でコードオブジェクトを永続化できることは重要である。.pyc ファイルの構造は次の関数 make_pyc のようになる。

import marshal, imp, os, struct, time
def make_pyc(filename):
"""see http://hg.python.org/cpython/file/2.7/Python/import.c"""
suffix, mode, _ = \
[i for i in imp.get_suffixes() if i[2] == imp.PY_COMPILED][0]
with open(os.path.splitext(filename)[0] + suffix, 'wb') as output:
output.write(imp.get_magic())
output.write(struct.pack('I', os.stat(filename).st_mtime))
with open(filename, 'U') as input:
code = compile(input.read(), filename, 'exec', 0, True)
marshal.dump(code, output)

行番号 (lineno)

ユーザーから入力されたソースコードを compile() するとき、それがただのスクリプトファイルとして扱える状態なら単に compile() すればよいが、テンプレートのように複数回 compile() を実行するときは位置の設定が必要となる。

  1. AST に変換し、ast.increment_lineno() を呼ぶ。
  2. ソースコードの先頭に ‘\n’ * N を結合する。
  3. 一度コードオブジェクトに変換し、types.CodeType(argcount, nlocals, stacksize, flags, codestring, constants, names, varnames, filename, name, firstlineno, lnotab[, freevars[, cellvars) の引数 firstlineno を任意の値に設定し、コードオブジェクトを生成する。

また次のようにすれば埋め込み文字列の行番号を実際の位置にあわせることができる。文字列中の行の継続 (末尾に \) はエスケープしなくても SyntaxError にならないため、必ずエスケープするように注意する。

import sys
source = '\n' * (sys._getframe(0).f_lineno - 1) + """if 1:
    ...
    """
code = compile(source, __file__, 'exec')

AST からコードオブジェクトを作成するとき、CPython は lineno が前後で逆転するような AST でも変換するが、PyPy ではエラーになる。

列番号 (col_offset)

AST では列番号が存在し、0 から始まる。しかし、コードオブジェクトに変換するとこの情報は消滅する。SyntaxError のための情報と考えられる。

Traceback (most recent call last): … のカスタマイズ

traceback.c の実装を参照すればわかるが、この表示をカスタマイズすることはできない。コードオブジェクトに変換するときに行番号とファイル名を設定すれば任意のファイルの任意の行を出力できる。

__doc__, __future__ を取得する

__future__ は複雑である。

  • compile() の引数 flags は __future__ の状態を設定できる。
  • compile() の引数 dont_inherit は 呼び出し元の __future__ フラグを継承するか設定できる。
  • compile() に与えるソースコードでも __future__ フラグを設定できる。
  • 連続する from __future__ import feature は許される。これは複数の ast.ImportFrom に変換される。
  • __builtins__.__dict__[‘print’] は常に存在する。
  • 次のコードが動作してしまう。
>>> import __future__, ast
>>> node = ast.parse('print 1')
>>> print ast.dump(node)
Module(body=[Print(dest=None, values=[Num(n=1)], nl=True)])
>>> exec compile(node, '<string>', 'exec', __future__.print_function.compiler_flag)
1

一回の実行中に複数回の compile() の呼び出しがあるときは、簡単にするため次のようなルールを定めた方がよい。

  • compile() の引数 dont_inherit は 常に False を指定する。
  • ユーザーのソースコードとライブラリのソースコードの __future__ フラグは分離する。
  • 最初のユーザーのソースコードの compile() で __future__ フラグを決定し、以降の compile() の flags に指定する。
  • ライブラリから __future__ フラグを追加しない。
    • AST オブジェクトを作成して追加しない。
    • ライブラリのソースコードを compile() するときは __future__ フラグを指定せず、フラグにかかわらず動作するようにする。

AST から __doc__ を取得する

ast.get_docstring() を使う。

AST から __future__ import を取得する

def iter_futureimport(node):
assert isinstance(node, ast.Module)
it = iter(node.body)
try:
i = it.next()
# skip docstring, see ast.get_docstring()
if isinstance(i, ast.Expr) and isinstance(i.value, ast.Str):
i = it.next()
while isinstance(i, ast.ImportFrom) and i.module == '__future__':
yield i
i = it.next()
except StopIteration:
pass

compile() に指定できる flags のビットマスク

コードオブジェクトの co_flags は余計なフラグが設定されていることがある。次の値でマスクする。

import __future__
PyCF_ALL = sum(getattr(__future__, i).compiler_flag
for i in __future__.all_feature_names)

コードオブジェクトをモジュールオブジェクトに変換する

import sys, types
def module_from_code(name, code, filename=__file__):
"""see PyImport_ExecCodeModuleEx
    at http://svn.python.org/view/python/trunk/Python/import.c?view=markup
    """
result = types.ModuleType(name)
result.__file__ = filename
result.__package__ = None
#result.__builtins__ = __import__('__builtin__')
sys.modules[result.__name__] = result
exec code in result.__dict__, result.__dict__
return result

実行中 (自分) のコードオブジェクトを得る

import sys
__code__ = sys._getframe(0).f_code

コードオブジェクトの最終行番号を取得する

import dis
__code__.co_firstlineno, list(dis.findlinestarts(__code__))[-1][1]

*1:PyPy には実装されていない。代わりに func_code を使う

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です