0
0

More than 1 year has passed since last update.

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

Posted at

1. はじめに

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

menu = None

def get_menu():
    if menu is not None:
        return menu
    menu = <メニューを読み込む重い処理がここに入る>
    return menu

しかし、get_menu()を呼び出すと

>>> 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宣言が必要になるらしい。

var = 0

def func_1():
    print(var)

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

def func_3():
    global var # グローバル宣言
    var = 3
    print(var)

上記のように3種類の関数を定義し、挙動を見てみると

>>> 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()関数だと、

menu = <メニューを読み込む重い処理がここに入る>

でglobal宣言なしにmenuに代入しているため、このmenuはグローバル変数ではなく、新たに定義されたローカル変数menuということになる。

しかし、

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. 解決策

menu = None

def get_menu():
    global menu
    if menu is not None:
        return menu
    menu = <メニューを読み込む重い処理がここに入る>
    return menu

のようにglobal宣言を追加すればよい。

>>> get_menu() # 初回実行時のみメニューを読み込むので時間がかかる
"メニューの内容"
>>> get_menu() # 2回目以降はすぐに実行完了する
"メニューの内容"

という期待通りの動作をしてくれる。

4. まとめ

global宣言を行うとその変数をグローバル変数として解釈させ、関数内で代入させることができる。
一方、nonlocal宣言というものもあり、こちらは一つ外側のスコープの変数として解釈する宣言らしい。

nonlocal 文は、列挙された識別子がグローバルを除く一つ外側のスコープで先に束縛された変数を参照するようにします。(公式ドキュメント)

global宣言とnonlocal宣言の挙動については、次の記事がわかりやすかったので、globalとnonlocalの動きを具体例で知りたい!という方は是非。

また、スコープの基礎的な話は次の記事が勉強になりました。

5. 参考

Programming FAQ — Python 3.11.4 documentation
プログラミング FAQ — Python 3.11.4 ドキュメント
7. 単純文 (simple statement) — Python 3.11.4 ドキュメント

0
0
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0