😸

eval() で関数を作るときのコツ

2021/09/16に公開

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() 時のグローバルスコープに入れてやるか、関数実行時のスコープに入れてやる必要がある。

背景知識

参考文献

意外なところに書かれていたりする。

eval()

eval は文字列で書かれた式を評価する組み込み関数である。このとき、評価に使用する global scope と local scope を指定することができる。

eval1.py
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() は変数のスコープがどのように解決されるか表示してくれる。例えば:

eval2.py
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 に次の行を追加する:

eval3.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 を入れてやると正常に実行できる:

eval4.py
# 略: eval2.py と同じ

w = 9
assert 'w' in globals()  # global scope に入っているか確認
f(2, 3)

なお、local scope に入れても動かないので注意。

本題: lambda 式を eval する

さきほどの eval2.py において lambda を作る部分を eval() に置き換えてみる。
同じスコープで評価されるはずなので同じ結果になるはず、と思うかもしれない。

eval5.py
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 を実行時の「グローバルスコープ」として扱う。
これはこのようにして確認できる。

eval6.py
# 略: eval5.py と同じ

f = define_g()
assert f.__globals__ is globals()

なので、実行時の global scope に z (と w) を入れてやれば動く:

eval7.py
# 略: eval5.py と同じ

f = define_g()
z = 5
w = 9
f(2, 3)

でも明示的にグローバルスコープに入れる作業は自動化しにくい。よって、次のやり方がおすすめである。

たぶんベストな策

ローカルスコープの z を、eval() 時のグローバルスコープに入れてやればいい。

eval8.py
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