eval() で関数を作るときのコツ
Overview
注意: 知らない文字列を eval()
または exec()
することは危険であることをご承知おきください
- とある事情により、
eval()
を使ってlambda x, y: x + y
のようなラムダ式の文字列から関数を生成したい。 - 作られた関数は、
eval()
が実行される時点でのスコープを使って評価してほしい。
たとえば、lambda l: is_sorted(l)
を評価する時点でis_sorted
が解決されていれば、実行時にも正しく解決されてほしい (当たり前に思えるが、意外に難しい)
意外と手こずったので、関連する仕様を調べつつ解決策を模索した。
動作環境
以下、Python 3.9.5 で動作を確認した。ドキュメントは 3.9.4 日本語版を参照している。
Summary
-
eval()
で関数を定義する場合、それにローカルスコープを渡しても、それは定義される関数から見ると enclosing scope として扱われるので、eval()
の仕様によりスコープ解決の対象にならず未解決変数として扱われれる。 - したがって、そのような変数は
eval()
時のグローバルスコープに入れてやるか、関数実行時のスコープに入れてやる必要がある。
背景知識
参考文献
意外なところに書かれていたりする。
- Python チュートリアル > 9. クラス > 9.2 Python のスコープと名前空間 https://docs.python.org/ja/3/tutorial/classes.html#python-scopes-and-namespaces
- Python 言語リファレンス
- 3.データモデル > 3.2. 標準型の階層 (の「ユーザ定義関数」のところ) https://docs.python.org/ja/3/reference/datamodel.html#the-standard-type-hierarchy
- 4. 実行モデル > 4.2. 名前付けと束縛 https://docs.python.org/ja/3/reference/executionmodel.html#naming-and-binding
- Python 標準ライブラリ
- 組み込み関数 >
eval()
https://docs.python.org/ja/3/library/functions.html#eval - Python ランタイムサービス > inspect --- 活動中のオブジェクトの情報を取得する https://docs.python.org/ja/3/library/inspect.html
- 組み込み関数 >
eval()
eval
は文字列で書かれた式を評価する組み込み関数である。このとき、評価に使用する global scope と local scope を指定することができる。
g = {'x': 1, 'y': 3}
l = {'x': 2, 'z': 4}
# x はどちらの scope でも定義されているので、local scope が優先して解決に使われる。
# y は指定した global scope で解決される。
# z は指定した local scope で解決される。
# よって、実行結果は 2+3+4=9 で 9 が表示される。
print(eval('x + y + z', g, l))
なお、スコープには以下の4種類がある。内側から順にこの通り:
- local scope: 現在実行中のコードを含むスコープ。最内のスコープ。
- enclosing (= nonlocal) scope: これは local と global の間にある、複数のネストしたスコープである。
- global (or module) scope: これはモジュール、すなわちファイルごとに独立して定まる。
- builtin scope: 組み込み関数などのスコープ。最外のスコープ。
ここでのポイントは、eval()
の際に enclosing scope が考慮されないことである。
(なお、builtin scope は自動的に挿入される)。
これは公式ドキュメントの https://docs.python.org/ja/3/library/functions.html#eval に、さりげなく書かれている。
eval() は、それが実行される環境の ネストされたスコープ (非ローカルのオブジェクト) を参照できないことに注意してください。
inspect.getclosurevars()
による、クロージャー変数の取得
inspect.getclosurevars()
は変数のスコープがどのように解決されるか表示してくれる。例えば:
from inspect import getclosurevars
v = 1 # global scope
def define_g():
z = 5 # enclosing (= nonlocal) scope of scope of `g`
g = lambda x, y: v + w + x + y + z # body is in local scope of `g`
return g
f = define_g()
print(getclosurevars(f))
実行結果はこうなる:
ClosureVars(nonlocals={'z': 5}, globals={'v': 1}, builtins={}, unbound={'w'})
ここでは、define_g()
はその中で関数 g
を定義し、それを返す。その返り値を f
に代入している。
スコープの解決の大部分は関数の (実行時ではなくて) 定義される時に行われる。
その結果、
- (出力には現れないが)
x, y
が local scope で解決されること -
z
が enclosing (= nonlocal) scope にあること -
v
が global scope にあること -
w
が未解決 (未束縛) であること
がインタプリタにはわかっている。g
は定義時に未解決の変数があるが、エラーになることはなく g
を定義できる。
ただし、f
(つまり g
) を実行するには、実行時の global scope に w
が含まれる必要がある。
eval2.py
に次の行を追加する:
# 略: eval2.py と同じ
f(2, 3)
すると w
の解決ができないのでエラーが表示される:
Traceback (most recent call last):
File ".../eval3.py", line 18, in <module>
f(2, 3)
File ".../eval3.py", line 8, in <lambda>
g = lambda x, y: v + w + x + y + z # body is in local scope of `g`
NameError: name 'w' is not defined
一方で、global scope に w
を入れてやると正常に実行できる:
# 略: eval2.py と同じ
w = 9
assert 'w' in globals() # global scope に入っているか確認
f(2, 3)
なお、local scope に入れても動かないので注意。
lambda
式を eval
する
本題: さきほどの eval2.py
において lambda
を作る部分を eval()
に置き換えてみる。
同じスコープで評価されるはずなので同じ結果になるはず、と思うかもしれない。
from inspect import getclosurevars
v = 1 # global scope
def define_g():
z = 5 # enclosing (= nonlocal) scope of scope of `g`
g = eval('lambda x, y: v + w + x + y + z') # body is in local scope of `g`
return g
f = define_g()
print(getclosurevars(f))
が、実行結果はこうなる:
ClosureVars(nonlocals={}, globals={'v': 1}, builtins={}, unbound={'z', 'w'})
w
に加えて、z
が未解決変数になっている。 この理由を考えてみる。
思ったように動かない理由
z
は、g
の本体のスコープ (local scope) から見ると enclosing scope (= nonlocal scope) にある。
これは eval()
の仕様により、名前解決にあたって考慮されない。よって未解決変数とされてしまう。
Python の仕様を勉強したあとだと明らかなのだが、漫然としか理解していなかった私には、以前は理由が理解できなかった。
解決策
次善策
eval()
によって定義された関数は、eval()
で指定された global scope を実行時の「グローバルスコープ」として扱う。
これはこのようにして確認できる。
# 略: eval5.py と同じ
f = define_g()
assert f.__globals__ is globals()
なので、実行時の global scope に z
(と w
) を入れてやれば動く:
# 略: eval5.py と同じ
f = define_g()
z = 5
w = 9
f(2, 3)
でも明示的にグローバルスコープに入れる作業は自動化しにくい。よって、次のやり方がおすすめである。
たぶんベストな策
ローカルスコープの z
を、eval()
時のグローバルスコープに入れてやればいい。
from inspect import getclosurevars
v = 1 # global scope
def define_g():
z = 5 # enclosing (= nonlocal) scope of scope of `g`
custom_globals = globals() | locals()
g = eval('lambda x, y: v + w + x + y + z', custom_globals) # body is in local scope of `g`
return (g, custom_globals)
(f, custom_globals) = define_g()
print(getclosurevars(f))
custom_globals['w'] = 9
f(2, 3)
実行結果はこう:
ClosureVars(nonlocals={}, globals={'v': 1, 'z': 5}, builtins={}, unbound={'w'})
eval()
直前の global scope と local scope をあわせたカスタムな custom_globals を作り、それを eval()
の実行環境として指定すると、作成された関数を実行する際の「グローバルスコープ」はそれになる。
注意するべきは、w
の値もその custom_global に追加する必要があること。一般にはスコープとして辞書オブジェクトを使うので、普通にキーと値を追加すればいい。
成果と限界
- これによって、ある関数内の local scope (正確には
locals()
値) と global scope を使って実行されるような (ラムダ) 関数をeval
で作れるようになる。ライブラリとかデコレータの実装には便利だと思われる。 - ただし、さらに
define_g
がある関数の内部にある場合などはこの方法では限界がある。define_g
の enclosing scope が必要になるかもしれないからである。その場合は実行フレームをたどってスコープを構成する必要がありそう。
Discussion