発端
下記の記事を見て、変数の動的定義ってexec
関数を使わなくても出来ることに気づいたのが発端。
[解決!Python]exec関数やglobals関数を使って変数を動的に定義するには
exec
関数を使わない変数の動的定義
試したコードは以下。globals
関数の返り値の辞書型変数に間接的または直接的に要素を追加することで、新たな変数を定義出来ることが確認出来た。
x = 1
gl = globals()
print(gl["x"]) # --> 1
gl["y"] = 2
print(y) # --> 2
globals()["z"] = 3
print(z) # --> 3
この方法の利点
動的に作りたい変数の名前が変数n
に、値が変数v
に代入されている時にexec
関数を使った場合とglobals
関数を使った場合を以下に示すが、後者の方がシンプルです。
n = "NewVar"
v = 99
# define with exec()
exec(n + "= " + str(v))
# define with globals()
globals()[n] = v
この方法が可能だと知らないと、理解するのにちょっとかかりますが、分かってしまえば、シンプルな分だけやりたいことが明確な記述になっています。
ローカル変数を同じ方法で定義してみた
以下の様にほぼ同様の記述を関数内で行ってみました。
def func():
a = 1
L = locals()
print(L["a"])
L["b"] = 2
print(b)
func() # --> NameError: name 'b' is not defined
すると、動的に定義したつもりの変数を知らないとエラーになりました。
変数L
の内容をprintする文を加えて確認すると、要素bは確かに追加されています。
exec
関数を使った以下のコードでも同様にエラーになります。
def func():
a = 1
exec("b = 2")
print(b)
func() # --> NameError: name 'b' is not defined
pythonの処理系がローカル変数が動的に追加されても認識できないということです。
理由をChatGPTに聞いてみた
この様になる理由を聞いてみた質問と回答が以下です。
質問
globals()["Z"] = 3 とすると、変数Zが参照可能な状態になりますが、関数内でlocals()["c"] = 3とした場合、変数cは参照可能な状態になりません。これはどうしてでしょうか。回答
globals()["Z"] = 3とすると、Zという名前でグローバル変数を作成または更新し、プログラムのどこからでもこの変数にアクセスできるようになります。これは、globals()が返す辞書がグローバル名前空間を直接反映しているためです。変更した内容はプログラムの実行中、継続して反映されます。一方で、関数内でlocals()["c"] = 3とした場合、期待通りにcという名前のローカル変数が作成されるわけではありません。これはPythonの実装の詳細に由来するものですが、一般的な理由として以下の点が挙げられます。
Pythonの関数とスコープ
Pythonでは、関数の実行時にローカル変数は特定の方法で扱われます。関数のコードがコンパイルされるとき、使用される変数の名前がローカル変数として記録されます。このプロセスでは、関数の本体で直接的に名前が割り当てられた変数のみがローカル変数として認識されます。locals()の限界
locals()は現在のローカルスコープの内容を反映した辞書を返しますが、この辞書を変更しても、元のローカルスコープには影響しません。これは、Pythonの関数内部でローカル変数が扱われる方式に起因します。具体的には、関数のスコープ内でローカル変数がコンパイル時に決定されるため、実行時にlocals()を介して新たなローカル変数を追加することはできません。実装の詳細
実際には、Pythonの関数のローカル変数は内部的には高速アクセスのための固定サイズの配列で管理されており、変数名はインデックスにマッピングされます。このため、実行時にlocals()を変更しても、この内部的なマッピングや配列の内容を変更することはできず、結果として新たに変数を追加することができないのです。まとめ
globals()["Z"] = 3によってグローバル変数Zを作成・更新できるのに対し、locals()["c"] = 3ではローカル変数cを期待通りに作成・更新できないのは、Pythonの関数のスコープと変数の管理方法、およびglobals()とlocals()が返す辞書の挙動の違いによるものです。この挙動はPythonの言語仕様と実装の詳細に基づいています。
代替手段
ローカル変数の使用目的が、変数のスコープを制限したいという場合には、代替手段が実はあります。exec
関数の第2引数として辞書型変数を渡すことで、グローバル変数の定義される場所を変更すれば良いのです。
FreeCADなどのユーザーが入力したpythonコードの実行機能を持つソフトでは、システム側で使っている変数名と同じ名前の変数をユーザーが使うことによる問題を避けるために、この手法を用いているだろうと推測されます。
結論
pythonの実装上の制約により、動的に作れる変数はグローバル変数だけということです。
また、ローカル変数が動的に作れないことに伴う問題は、exec
関数の第2引数を指定することで大抵は回避可能と思われます。
なお、確認はpythonのVer. 3.12で行いました。(将来のバージョンではこの実装上の制約も変更されているかも知れません。)