pythonで動的に作れる変数はグローバル変数だけと気付いた

# 発端
下記の記事を見て、変数の動的定義って`exec`関数を使わなくても出来ることに気づいたのが発端。
[［解決！Python］exec関数やglobals関数を使って変数を動的に定義するには](https://atmarkit.itmedia.co.jp/ait/articles/2404/09/news026.html)

# ```exec```関数を使わない変数の動的定義
試したコードは以下。`globals`関数の返り値の辞書型変数に間接的または直接的に要素を追加することで、新たな変数を定義出来ることが確認出来た。
```python
x = 1
gl = globals()
print(gl["x"]) # --> 1
gl["y"] = 2
print(y) # --> 2
globals()["z"] = 3
print(z) # --> 3
```
## この方法の利点
動的に作りたい変数の名前が変数`n`に、値が変数`v`に代入されている時に`exec`関数を使った場合と`globals`関数を使った場合を以下に示すが、後者の方がシンプルです。
```python
n = "NewVar"
v = 99
# define with exec()
exec(n + "= " + str(v))

# define with globals()
globals()[n] = v
```
この方法が可能だと知らないと、理解するのにちょっとかかりますが、分かってしまえば、シンプルな分だけやりたいことが明確な記述になっています。

# ローカル変数を同じ方法で定義してみた
以下の様にほぼ同様の記述を関数内で行ってみました。

```python
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`関数を使った以下のコードでも同様にエラーになります。
```python
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コードの実行機能を持つソフトでは、システム側で使っている変数名と同じ名前の変数をユーザーが使うことによる問題を避けるために、この手法を用いているだろうと推測されます。

# `exec`関数の第3引数locals
上の代替手段で、`exec`関数の第2引数を指定する方法を書きましたが、exec関数の第3引数が`locals`といかにもローカル変数を動的に使えそうな名前です。
ですが、これをを指定しても、関数内のローカル変数を動的に作ることも出来ません。

以下のコードは、モジュールレベルでは動作します。
```python
x = 10
exec("z = x + 5", globals(), locals())
print(z)  # 15
```
これを以下の様に関数内において、ローカル変数`z`を動的に作ろうとするとどうでしょう。
```python
def locals_test():
    x = 10
    exec("z = x + 5", globals(), locals())
    print(z)  # 15
```
これは、グローバル変数として変数`z`が定義されていない限り、`NameError: name 'z' is not defined`とエラーになります。
pythonの公式ドキュメント中の`exec`関数のところに書かれていますが、モジュールレベルでは`globals()`と`locals()`の返すものは同じだそうです。先の例のモジュールレベルで実行したコードはグローバル変数(モジュールレベルのスコープの変数)として変数`z`を生成しただけでした。

## ローカル変数の定義のされ方
下記のコードは動作しそうに見えますが、エラーになります。
```python
Z=9
def test_local_scope():
    print(Z)
    Z = 0
test_local_scope()
```
これは「関数内で代入された変数は、`global`文か`nonlocal`文で指定しない限りローカル変数」というルールが適用されるためです。このルールは、変数への代入が変数の参照より後であっても適用されます。
つまり、ローカル変数はコードを解釈する途中で、ローカル変数として区別されているのです。この辺の仕組みがローカル変数を動的に作れないことの原因と思われます。

### 閑話：宣言文的な要素
pythonにおいては、関数定義やクラス定義を「関数オブジェクトなどを生成する命令」と考えると、これらも即時に実行されていると考えることができます。
私は以前はperl使いでしたが、perlではコードは実行前に2パスで解釈されている感じで、宣言文的に処理されるものがありました。それと比べて、pythonは宣言文的に評価順序が優先される要素が少ないと思います。
その少ない中で、宣言文的な要素が、`global`文と`nonlocal`文、及びローカル変数の定義となる代入文です。(私は今のところ、これ以外にpythonで評価順序が優先される要素を見つけていません。)

# 結論
pythonの実装上の制約により、動的に作れる変数はグローバル変数だけということです。
また、ローカル変数が動的に作れないことに伴う問題は、`exec`関数の第2引数を指定することで大抵は回避可能(目的を果たすことは可能)と思われます。

なお、確認はpythonのVer. 3.12で行いました。(将来のバージョンではこの実装上の制約も変更されているかも知れません。)