ロゴ ロゴ

Pythonのexecで詰まった話と、それに関する一考察

まえがき

皆さん久しぶりです.なかなか暑さも忙しさも去ってくれず、遊びのプログラムもほとんど書けていない今日この頃です.どうもウグイスです.

今回は研究で書いていたプログラムで詰まった話をしようかと思います.研究で書いていたプログラムの内容には触れずに、詰まっていた部分だけで一つネタになるかなと思ったので、久しぶりに筆を執る、もといタイピングしてブログを書こうと思ったわけです.

まあ私は研究でちょっとPythonでプログラムを書いているだけのにわかプログラマなので、Pythonわかる人からしたら大したことではないとは思いますが、そういう人はネタ話として納めていただければと思います.

問題提起

Q1.このコードは動きますか

import numpy

def test():
    return numpy.mean([1,2,3,4])

ret_val = test()
print(ret_val)

A.動きます

結果は2.5と出力されます.


Q2.このコードは動きますか?

code = "import numpy\ndef test():\n\treturn numpy.mean([1,2,3,4])\nret_val = test()\nprint(ret_val)"
exec(code)

A.動きます

同じく2.5と出力されます.

ここではexec()という関数が出てきました.この関数は組み込み関数の一つです.第1引数に渡された文字列をPythonコードとして実行してくれます.厳密には文字列の代わりにコードオブジェクトを渡してもいいことになってますが、ここでは気にしないことにします.

codeにはQ1で動いたコードから余分な改行を削ったものを代入しています.

当然ですが、動きます.


Q3.このコードは動きますか?

code = "import numpy\ndef test():\n\treturn numpy.mean([1,2,3,4])\nret_val = test()\nprint(ret_val)"

def main():
    exec(code)

main()

A.動きません

これは残念ながら動きません.エラー文は以下のようになります(ユーザ名は変えてます)

Traceback (most recent call last):
  File "c:/Users/user/pro/python_worker/exec_test2.py", line 6, in <module>
    main()
  File "c:/Users/user/pro/python_worker/exec_test2.py", line 4, in main
    exec(code)
  File "<string>", line 4, in <module>
  File "<string>", line 3, in test
NameError: name 'numpy' is not defined

なんかよくわからんけどnumpyが定義されてないといわれています.

こんなに単純なコードではなかったのですが、この問題が研究で使っていたコードで発生したため原因究明した話となります.

ここまで読んだ時点で原因が分かった人は、なんか頑張っているなと思って見てください.

原因究明

さてまずはexec()について調べてみましょう.

ドキュメントには以下のような記述があります.

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

exec()には第2第3引数として辞書を与えることができて、それぞれグローバル変数と、ローカル変数として扱われるようです.

そして注釈の中に、globals()とlocals()という組み込み関数について触れられています.

globals()

現在のグローバルシンボルテーブルを表す辞書を返します。これは常に現在のモジュール (関数やメソッドの中では、それを呼び出したモジュールではなく、それを定義しているモジュール) の辞書です。

locals()

現在のローカルシンボルテーブルを表す辞書を更新して返します。関数ブロックで locals() を呼び出した場合自由変数が返されます、クラスブロックでは返されません。


まあそれぞれ動かしてみた方がわかりやすいですかね.適当にコードを書いてみました.

import numpy

def test():
    b = 11
    print(locals())

a = 12

test()

print(globals())

結果

{'b': 11}
{'__name__': '__main__', '__doc__': None, '__package__': None, '__loader__': <_frozen_importlib_external.SourceFileLoader object at 0x000002A38F153DC0>, '__spec__': None, '__annotations__': {}, '__builtins__': <module 'builtins' (built-in)>, '__file__': 'c:/Users/user/pro/python_worker/exec_test2.py', '__cached__': None, 'numpy': <module 'numpy' from 'C:\\Users\\user\\AppData\\Local\\Programs\\Python\\Python38\\lib\\site-packages\\numpy\\__init__.py'>, 'test': <function test at 0x000002A38F19D1F0>, 'a': 12}

上の辞書がlocals()の結果.test関数内で宣言したbが辞書に登録されています.

下の辞書がglobals()の結果、importしたnumpyと、test関数、宣言したaが辞書に登録されているのと、おまじないで有名なnameとかもありますね.

割とこれだけでもいろいろあっていじれそうな要素が多そうですが、今回は原因究明を優先したいので、さっそく先ほどのQ3のソースコードで実験してみましょう.

というわけで、先ほどエラーになったQ3のコードに差し込んで実行してみます.明らかにtest()を呼んだときにエラーになるのはわかるので、コードを分割してexec()し、順次実行でエラー直前の変数の状態を確認することにします.

code1 = "import numpy\ndef test():\n\treturn numpy.mean([1,2,3,4])"
code2 = "ret_val = test()\nprint(ret_val)"

def main():
    global_vars = globals().copy()
    local_vars = locals()
    exec(code1, global_vars, local_vars)
    print(global_vars)
    print(local_vars)
    exec(code2, global_vars, local_vars)

main()
{'__name__': '__main__', '__doc__': None, '__package__': None, '__loader__': <_frozen_importlib_external.SourceFileLoader object at 0x0000015F8D693DC0>, '__spec__': None, '__annotations__': {}, '__builtins__': <module 'builtins' (built-in)>, '__file__': 'c:/Users/user/pro/python_worker/exec_test2.py', '__cached__': None, 'code1': 'import numpy\ndef test():\n\treturn numpy.mean([1,2,3,4])', 'code2': 'ret_val = test()\nprint(ret_val)', 'main': <function main at 0x0000015F8D6DD1F0>}
{'global_vars': {'__name__': '__main__', '__doc__': None, '__package__': None, '__loader__': <_frozen_importlib_external.SourceFileLoader object at 0x0000015F8D693DC0>, '__spec__': None, '__annotations__': {}, '__builtins__': <module 'builtins' (built-in)>, '__file__': 'c:/Users/user/pro/python_worker/exec_test2.py', '__cached__': None, 'code1': 'import numpy\ndef test():\n\treturn numpy.mean([1,2,3,4])', 'code2': 'ret_val = test()\nprint(ret_val)', 'main': <function main at 0x0000015F8D6DD1F0>}, 'numpy': <module 'numpy' from 'C:\\Users\\user\\AppData\\Local\\Programs\\Python\\Python38\\lib\\site-packages\\numpy\\__init__.py'>, 'test': <function test at 0x0000015F8DD0F700>}
Traceback (most recent call last):

ちょっと見づらいのですが、上がglobal辞書、下がlocal辞書です.

ここで大きな違いとして出てくるのは、local辞書にはnumpyが存在していますが、global辞書には存在していないという点です.

ここまで来たら、なんとなくわかってきますかね.exec()内で実行されているコードではimport numpyを呼んでいるのはグローバルな空間です.つまりnumpy.mean()が呼ばれた時にはグローバルな名前空間を参照しているのだと考えました.なのでそれを確かめるために、以下の二つのコードを実行してみます.

Q4.このコードは動きますか?

code = "def test():\n\timport numpy\n\treturn numpy.mean([1,2,3,4])\nret_val = test()\nprint(ret_val)"

def main():
    exec(code)

main()

A.動きます

違いとしてはimportの位置を変えています.これによってimportを呼ぶ位置によってどこの変数空間に参照しに行くかが変わることがわかります.

前と同じく2.5と表示されます.

Q5.このコードは動きますか?

import numpy
np = globals()['numpy']
del numpy

code = "import numpy\ndef test():\n\treturn numpy.mean([1,2,3,4])\nret_val = test()\nprint(ret_val)"

def main():
    global_vars = globals().copy()
    local_vars = locals()
    global_vars['numpy'] = np
    exec(code, global_vars, local_vars)

main()

A.動きます

同じく2.5と表示されます.

global_vars['numpy'] = npの行がなければ動かないことも確認しています.

これによってglobal変数にnumpyが存在していれば参照できる.つまりglobal変数にあれば動くということが確定しました.

考察

ここまでの実行結果から、exec()のソースコード内で呼ばれたimport文はたとえグローバルな空間で宣言された場合でもlocal変数として登録される.しかしexec内でグローバルな空間で宣言されたモジュールは参照しようとした場合、グローバル変数から参照しようとする.

以上の2点から、当然グローバル変数にはモジュールは登録されていないので、定義されてねーよ、xxxみたいな感じでPython君が文句を言ってくる.なのかなと思ってます.

実際グローバル変数にnumpyを追加したQ5のコードは動いているわけですし.

対策

さて、おそらくの原因はわかりましたが、どう解決するかという問題が残されています.

Q4とQ5の方法ではそれぞれ、制限がかかります.Q4では関数内でimportをすることをコード文字列を書く人に強制することになります.Q5では、何が追加インポートされているのかをどう取得するかが問題になります.

ここで思い出してほしいのはexec()は第2第3引数にそれぞれグローバル変数の辞書とローカル変数の辞書を渡せるということです.渡した辞書はexec実行後に実行内容によって変化します.

つまりimportや関数の定義が終わった時点でいったんグローバル辞書にローカル辞書の内容を追加して、実行時に渡せば行けるって寸法です.

Q6.このコードは動きますか?

code1 = "import numpy\ndef test():\n\treturn numpy.mean([1,2,3,4])"
code2 = "ret_val = test()\nprint(ret_val)"

def main():
    global_vars = globals().copy()
    local_vars = locals()
    exec(code1, global_vars, local_vars)
    global_vars.update(local_vars)
    exec(code2, global_vars, local_vars)

main()

A.動きます

まあこれも定義部分と実行部分を分ける必要があるので自然な方法ではありませんが、私の研究で使用するプログラム的にはこれで問題ないのでこの方法で妥協しました.

あとがき

今回は研究で使うプログラムで出たエラーを単純化して、ブログに起こしてみました.なんかデバッグしているときにすごく楽しかったのでブログにしてみたのですが、全部終わってからまとめると、なんか私が楽しいと思っていたものが伝わってない内容になった気がします.

やはり私には文才はないようですね.

ここまでご精読ありがとうございます.また次回があれば会いましょう.ウグイスでした.

コメント入力

関連サイト