17ec084
@17ec084 (智剛 平田)

Are you sure you want to delete the question?

Leaving a resolved question undeleted may help others!

【python】関数内でexecによる代入を行ったときの、直観に反する動作について

1.環境

Python 3.8.2 (tags/v3.8.2:7b3ab59, Feb 25 2020, 23:03:10) [MSC v.1916 64 bit (AMD64)] on win32

2.実行したこと

対話モード
>>> def f(arg):
...     exec("a=arg")
...     return a
...
>>> f(0)

3. 予想と現実

関数fは引数argとして受け取ったオブジェクトをそのまま返却するだろう。
なぜならば、exec("a=arg")は関数f内で実行されるから、f内の変数aには、引数argが代入されるはずだからだ。

4. 現実

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 3, in f
NameError: name 'a' is not defined

変数aは存在しないという旨のエラーが出た。
line 3 in fにはreturn aというコードが書かれているので、
execの実行時ではなく、returnの実行時にこのエラーは送出されたことといえる。

また、次のような再実験1を行ったことにより、return af内の変数ではなく、fのひとつ外のスコープのaを返却していたことが分かった。

対話モード(再実験1)
>>> a = "外"
>>> def f(arg):
...     exec("a=arg")
...     return a
...
>>> f("内")
'外'

f内で変数aを呼んでいるにも関わらず、fのひとつ外のスコープのaが呼び出されているということは、
f内のローカル変数aが存在しないということを示唆している。すなわち、execは自身が呼ばれたスコープに対し、変数を代入することに失敗している。
このことは、再実験2により確かめることが出来た。

対話モード(再実験2)
>>> a = '外'
>>> def f(arg):
...     a = "内-非exec"
...     exec("a=arg")
...     return a
...
>>> f("内-exec")
'内-非exec'

5.質問

2章あるいは再実験1,2において、execは、どのスコープに対して変数aを代入したのでしょうか。
また、以下に示す再実験3のように、execは、グローバルスコープで呼ばれる限り、自身が呼ばれたスコープに対して変数を代入することが可能ですが、なぜ関数内のローカルスコープでは同じことが可能ではないのでしょうか?
よろしくお願いいたします。

対話モード(再実験3)
>>> a = "外-非exec"
>>> exec("a='外-exec'")
>>> a
'外-exec'
0

2Answer

関数内で exec("a=arg") すると exec() は標準の locals 辞書の変更を試みるからです。標準の locals 辞書は変更してもローカル変数に影響を及ぼしません。

詳しい解説

exec(object[, globals[, locals]]) 関数はオプション引数 globalslocals を受け取ります。

また、

標準では(引用注:オプション引数を省略した場合は) locals は後に述べる関数 locals() のように動作します: 標準の locals 辞書に対する変更を試みてはいけません。 exec() の呼び出しが返る時にコードが locals に与える影響を知りたいなら、明示的に locals 辞書を渡してください。

-- https://docs.python.org/ja/3.8/library/functions.html#exec

とされています。

変更を試みるとどうなるか確認するために、標準の globals 辞書と標準の locals 辞書に対する変更を試みるコードを以下のように実行してみます。

globals()['a'] = 'GLOBAL'
print(a)

def f():
    b = 'LOCAL (DEFAULT)'
    locals()['b'] = 'LOCAL (UPDATED)'
    print(b)

f()

def g():
    locals()['c'] = 'LOCAL (NEW)'
    print(c)

g()
% python3 locals.py

GLOBAL
LOCAL (DEFAULT)
Traceback (most recent call last):
  File "/Users/uasi/locals.py", line 15, in <module>
    g()
  File "/Users/uasi/locals.py", line 13, in g
    print(c)
NameError: name 'c' is not defined

標準の globals 辞書を変更するとグローバル変数が定義されますが、標準の locals 辞書を変更してもローカル変数は定義されず、定義済みローカル変数の値が変わることもないと分かります。

まとめると、ローカル変数を変更するコードをオプション引数なしで exec() することは、標準の locals 辞書の変更を試みることと等しく、実際のローカル変数には何の影響も及ぼさないということです。

なお、引数 locals に独自の辞書を与えた場合に辞書が変更されることは以下のコードで確かめられます。

def h():
    my_globals = {'__builtins__': {}} # __builtins__ キーがないとデフォルト値が挿入されてしまう
    my_locals = {}
    exec('d = "MY LOCAL"', my_globals, my_locals)
    print(my_globals)
    print(my_locals)

h()
% python3 my_locals.py
{'__builtins__': {}}
{'d': 'MY LOCAL'}
3Like

質問の内容とは異なりますが、関数内でも関数のメンバ変数はexec出来るようです。

def f(arg):
	f.a = "内-非exec"
	exec('f.a=arg')
	return f.a

print(f("内-exec"))

#>> 内-exec
3Like

Your answer might help someone💌