3.0のexecの挙動

以下がPython 3.0で面白い挙動を示していたので調べてみた。なお2.5.4でテストしたらエラーが発生しなかった。

>>> def f11():
... exec('x = 666')
... print(locals()['x'])
... x = 777
...
>>> f11()
Traceback (most recent call last):
File "", line 1, in
File "", line 3, in f11
KeyError: 'x'

まずdis.dis(f11)でバイトコードを調べてみたが、exec関数の中身の挙動が問題になっている模様でよく分からなかった。また、挙動としては、「グローバル変数とローカル変数」もしくは「ネスト変数とローカル変数」を1つの名前空間上に混在させた時に起こる問題と酷似していたので以下を実験してみた。

>>> def foo():
...   x=666
...   def f11():
...     print(x)
...     x=777
...   f11()
...
>>> foo()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 6, in foo
  File "<stdin>", line 4, in f11
UnboundLocalError: local variable 'x' referenced before assignment

似たようなエラーが発生した。ここで、さらに以下のようなテストをしてみた。

>>> def foo():
...   def f11():
...     exec('nonlocal x; x=666')
...     print(locals()['x'])
...     x=777
...   f11()
...
>>> foo()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 6, in foo
  File "<stdin>", line 3, in f11
SyntaxError: nonlocal declaration not allowed at module level

execの中でnonlocal文を実行してみた。すると、モジュールレベルではnonlocal文は実行できませんよというエラーが発生した。ここで、exec内では(第2引数を省略すると)モジュールレベルの名前空間で実行していると推測できる。さらに、以下をテストする。

>>> def f11():
...   exec('x=666')
...   print(locals())
...
>>> f11()
{'x': 666}

ここで分かるのは、モジュールの名前空間に入っていたものが、ローカルの名前空間に移行されている。というのも、以下のように名前が見えるところに変数があっても、locals()で参照できないからだ。

>>> def foo():
...   x=666
...   def f11():
...     print(locals())
...   f11()
...
>>> foo()
{}

最後に、もう1つ見てみる。

>>> def f11():
...   x=777
...   exec('x=666')
...   print(locals())
...
>>> f11()
{'x': 777}

挙動から推測すると、モジュールスコープからローカルスコープに移行する際に、既にローカルスコープが存在すると移行しないということになると思う。というか、もしかしたらバグなのかもしれない。