静かなる名辞

pythonとプログラミングのこと


【python】execを使って変数名を動的に変える方法についての考察

はじめに

 pythonでどうしても変数名を動的に変えたい場合、execを使うことになる。

 実用的には無意味というかやるべきではないのだけど(他の方法でもっと合理的なコードが書ける)、やった場合の挙動でちょっと気になる点があったので、検証して記事にまとめておく。

スポンサーリンク


やり方としてはexecでやれば良い

 まずやり方から。

exec("hoge = 'ほげ'")  # hoge = 'ほげ'と書いたのと同じ
print(hoge) # => ほげ

 stringを引数に渡せるので、なんでもし放題です。でも、こういうことをすると何かと大変なことになるので、やめておいた方が無難。

もっといいやり方

 もっとマシなやり方も、念の為紹介しておく。

d = dict()
d["hoge"] = "ほげ"
print(d["hoge"]) # => ほげ

 識別子がほしいときは辞書などのコレクション型などに入れよう。それで普通は事足りる。

 あるいは、連番の変数を作りたがる人も一定数いる。

# こんな奴
string1 = "ほげ"
string2 = "ふが"
string3 = "ぴよ"

# 動的に生成するとしたら
for i, s in enumerate(["'ほげ'","'ふが'", "'ぴよ'"]):
    exec(f"string{i+1} = {s}")

 リストに入れれば事足りる。

strings = ["ほげ", "ふが", "ぴよ"]

# indexが0からなのは仕方ない。プログラミングをやるなら慣れた方が良い。
print(strings[0]) # => ほげ

素朴な疑問

 ここからが本編で、この記事で考察したいことである。

 考察したいこと:
  スコープどうなってるの?

hoge = 10
def f():
    exec("hoge = 'ほげ'")
    print(hoge)
f() # => 10

 理由がわからない。仕方ないのでドキュメントを見に行く。

exec(object[, globals[, locals]])(原文)
この関数は Python コードの動的な実行をサポートします。 object は文字列かコードオブジェクトでなければなりません。文字列なら、その文字列は一連の Python 文として解析され、そして (構文エラーが生じない限り) 実行されます。 [1] コードオブジェクトなら、それは単純に実行されます。どの場合でも、実行されるコードはファイル入力として有効であることが期待されます (リファレンスマニュアルの節 "file-input" を参照)。なお、 return および yield 文は、 exec() 関数に渡されたコードの文脈中においてさえ、関数定義の外では使えません。返り値は None です。

いずれの場合でも、オプションの部分が省略されると、コードは現在のスコープ内で実行されます。globals だけが与えられたなら、辞書でなくてはならず、グローバル変数とローカル変数の両方に使われます。globals と locals が与えられたなら、それぞれグローバル変数とローカル変数として使われます。locals を指定する場合は何らかのマップ型オブジェクトでなければなりません。モジュールレベルでは、グローバルとローカルは同じ辞書です。exec が globals と locals として別のオブジェクトを取った場合、コードはクラス定義に埋め込まれたかのように実行されます。

globals 辞書がキー __builtins__ に対する値を含まなければ、そのキーに対して、組み込みモジュール builtins の辞書への参照が挿入されます。ですから、実行されるコードを exec() に渡す前に、 globals に自作の __builtins__ 辞書を挿入することで、コードがどの組み込みを利用できるか制御できます。

注釈 組み込み関数 globals() および locals() は、それぞれ現在のグローバルおよびローカルの辞書を返すので、それらを exec() の第二、第三引数にそのまま渡して使うと便利なことがあります。
注釈 標準では locals は後に述べる関数 locals() のように動作します: 標準の locals 辞書に対する変更を試みてはいけません。 exec() の呼び出しが返る時にコードが locals に与える影響を知りたいなら、明示的に locals 辞書を渡してください。

2. 組み込み関数 — Python 3.6.5 ドキュメント


 ダメだ、よくわからん。落ち着いて考え直してみる。

 文が実行されていれば、少なくともglobals辞書かlocals辞書のどちらかには追加されるはずだ。

hoge = 10
def f():
    exec("hoge = 'ほげ'")
    print(globals())
    print(locals())
f()
""" =>
{'__cached__': None, '__package__': None, '__loader__': <_frozen_importlib_external.SourceFileLoader object at 0x7f5989241b00>, '__doc__': None, 'hoge': 10, '__builtins__': <module 'builtins' (built-in)>, '__name__': '__main__', 'f': <function f at 0x7f5989277f28>, '__spec__': None, '__file__': 'exec_scope_test.py'}
{'hoge': 'ほげ'}
"""

 しっかりローカル変数に追加されている。ということは、文は実行されている。

 そうか、わかった。スコープは関数定義時に決まるんだった。定義時にはexecは実行されないので、関数内から見えるhogeはグローバルに定義したhogeになる。あとで実行時にローカル変数が増えようが、そんなのはお構いなしにグローバルのhogeを見続けるということか。

 じゃあ、こうするとどうなるの?

def f():
    exec("hoge = 'ほげ'")
    print(hoge)
f()
Traceback (most recent call last):
  File "exec_scope_test.py", line 4, in <module>
    f()
  File "exec_scope_test.py", line 3, in f
    print(hoge)
NameError: name 'hoge' is not defined

 こちらも同様の事情である。関数定義時に、関数のスコープの中にはhogeが見つからないので、pythonインタプリタはhogeがグローバル変数だと*1勝手に認識する。あとはずっとそう認識しっぱなしなので、「hogeなんてグローバル変数は見つからなかった」という結果になってエラーを吐く。

 この例について考えてみると、わかりやすい。

>>> def f():
...     print(i)
... 
>>> f()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 2, in f
NameError: name 'i' is not defined
>>> i = 0
>>> f()
0

 そしてこれをなんとか意図通り動かすのは、おそらく無理じゃないかな。ちょっと思いつかない。

 やりたいことからしたら無意味なんだろうけど、ローカル変数を先に作っておけばできるのだろうか。

def f():
    hoge = None
    exec("hoge = 'ほげ'")
    print(hoge)
    print(locals())
f()
""" =>
None
{'hoge': None}
"""

 できないのかよ。これは何事かと思ってググったら、出てきた。

python - How does exec work with locals? - Stack Overflow

 ややこしい話だから読みたい人は勝手に読んでいただけば良いと思う。とりあえず、こうすると回避はできる。

def f():
    hoge = None
    ldict = {}
    exec("hoge = 'ほげ'", globals(), ldict)
    print(ldict["hoge"])
f() # => ほげ

 この結果を喜んではいけない。「もっといいやり方」で紹介した辞書を使う方法を、とても回りくどく実行しているだけだからだ。

 あと、上のリンクにも書いてあるけど、python2だと普通(?)にできるんだってさ。

>>> def f():
...     hoge = None
...     exec "hoge = 'ほげ'"
...     print hoge
... 
>>> f()
ほげ

 嫌になっちゃうね。

まとめと結論

 execを使うとスコープの概念がぶっ壊れてほんとうにつらいことになる。

 すべてをグローバルな空間で完結させればこの記事に書いたような問題には引っかからないだろうけど、ある程度の規模のプログラムをまともな規模で書くことはできなくなるので、本当に使いみちがない。

 やはり、やらないでおいた方が良い。

*1:厳密には少なくともローカルスコープではないと認識して実行時に上位スコープをたどる・・・んだっけ?