[Python] 関数内でグローバル変数の参照と代入をしようとして未定義エラーにハマった話

# 1. はじめに
変数のスコープをあまり理解していなかったせいでPythonに怒られた。
まず、読み込みの重い処理で、初回のみ読み込むようにしたくて書いたのが以下のようなコードだった。
```Python
menu = None

def get_menu():
    if menu is not None:
        return menu
    menu = <メニューを読み込む重い処理がここに入る>
    return menu
```
しかし、get_menu()を呼び出すと
```Python
>>> get_menu()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 2, in get_menu
UnboundLocalError: local variable 'menu' referenced before assignment
```
というエラーが返ってきた。`menu`はグローバル変数なはずなのにローカル変数として扱われている。何故...
# 2. global宣言
大前提として、関数内で単純に`<変数名> = <代入内容>`という形でグローバル変数に代入することはできず、global宣言が必要になるらしい。
```Python
var = 0

def func_1():
    print(var)

def func_2():
    var = 2
    print(var)

def func_3():
    global var # グローバル宣言
    var = 3
    print(var)
```
上記のように3種類の関数を定義し、挙動を見てみると
```Python
>>> var
0
>>> func_1()
0 # ここで出力されているvarはグローバル変数
>>> func_2()
2 # ここで出力されているvarはfunc_2内のローカル変数
>>> var
0 # グローバル変数のvarはfunc_2によって変更されていない
>>> func_3()
3 # ここで出力されているvarはグローバル変数
>>> var
3 # グローバル変数のvarがfunc_3によって変更された
```
つまり、
- `func_1`のように関数内からグローバル変数は当然参照できる
- しかし、`func_2`のようにglobal宣言なしに代入しようとすると、`var`はグローバル変数とは異なる、新たに定義されたローカル変数となる
- `func_3`のようにglobal宣言を行うと、関数内でもグローバル変数に代入できる

ということになる。

最初の`get_menu()`関数だと、
```Python
menu = <メニューを読み込む重い処理がここに入る>
```
でglobal宣言なしに`menu`に代入しているため、この`menu`はグローバル変数ではなく、新たに定義されたローカル変数`menu`ということになる。

しかし、
```Python
if menu is not None:
```
の`menu`はローカル変数`menu`への代入前に参照しているので、グローバル変数なのでは？という疑問が生じた。そこで、公式ドキュメントを訪ねると
> In Python, variables that are only referenced inside a function are implicitly global. If a variable is assigned a value anywhere within the function’s body, it’s assumed to be a local unless explicitly declared as global.（Python では、関数内で参照されるだけの変数は暗黙的にグローバルとなります。 関数の本体のどこかで値が変数に代入されたなら、それは明示的にグローバルであると宣言されない限り、ローカルであるとみなされます。）（順に公式ドキュメント英語版・日本語版）

と書いてあった。関数内のどこかにグローバル変数と同名のローカル変数への代入を行っている箇所があれば、たとえ代入前の行であったとしてもその名前の変数はローカル変数とみなされるらしい。
# 3. 解決策
```Python
menu = None

def get_menu():
    global menu
    if menu is not None:
        return menu
    menu = <メニューを読み込む重い処理がここに入る>
    return menu
```
のようにglobal宣言を追加すればよい。
```Python
>>> get_menu() # 初回実行時のみメニューを読み込むので時間がかかる
"メニューの内容"
>>> get_menu() # 2回目以降はすぐに実行完了する
"メニューの内容"
```
という期待通りの動作をしてくれる。
# 4. まとめ
global宣言を行うとその変数をグローバル変数として解釈させ、関数内で代入させることができる。
一方、nonlocal宣言というものもあり、こちらは一つ外側のスコープの変数として解釈する宣言らしい。
> nonlocal 文は、列挙された識別子がグローバルを除く一つ外側のスコープで先に束縛された変数を参照するようにします。（公式ドキュメント）

global宣言とnonlocal宣言の挙動については、次の記事がわかりやすかったので、globalとnonlocalの動きを具体例で知りたい！という方は是非。
- [globalとnonlocalの宣言に関係する疑問点を検証してみました | Snow Tree in June](https://snowtree-injune.com/2018/12/13/global-nonlocal/)

また、スコープの基礎的な話は次の記事が勉強になりました。
- [［Python入門］関数のローカル変数とスコープ：Python入門（1/3 ページ） - ＠IT](https://snowtree-injune.com/2018/12/13/global-nonlocal/)

# 5. 参考
[Programming FAQ — Python 3.11.4 documentation](https://docs.python.org/3/faq/programming.html#what-are-the-rules-for-local-and-global-variables-in-python)
[プログラミング FAQ — Python 3.11.4 ドキュメント](https://docs.python.org/ja/3/faq/programming.html#what-are-the-rules-for-local-and-global-variables-in-python)
[7. 単純文 (simple statement) — Python 3.11.4 ドキュメント](https://docs.python.org/ja/3/reference/simple_stmts.html#the-nonlocal-statement)