関数を書き始めたころ、外で作った変数を関数の中で1つ増やしたいと思ったことはありませんか?ところが実行してみると、UnboundLocalErrorという見慣れないエラーで止まってしまいます。
変数にはちゃんと0が入っているのに、なぜ値がないと言われるのでしょうか。この疑問は、Pythonが変数を探すルールを知ると、すっきり解けます。
この記事では、そのルールと、外の変数を書き換えるためのglobal文とnonlocal文を順番に見ていきます。サンプルの実行結果は、手元のPython 3.11で確認したものです。
まずはエラーを再現してみよう¶
最初に、多くの人がつまずく場面をコードで再現してみます。次の例は、ボタンが押された回数を数える関数をイメージしたものです。
count = 0
def press():
count += 1
print(count)
press()
これを実行すると、次のエラーが表示されます。count は関数の外でちゃんと0になっているのに、です。
UnboundLocalError: cannot access local variable 'count' where it is not associated with a value
エラー文を訳すと、ローカル変数のcountにアクセスしようとしたけれど、まだ値が結びついていない、という意味です。ポイントは、Pythonがこのcountをローカル変数だと判断しているところにあります。
ちなみに、Python 3.10以前では local variable 'count' referenced before assignment という文面でした。バージョンによって言い回しは違いますが、言っている中身は同じです。
エラー文の読み方そのものに自信がない方は、先にこちらを読んでおくと理解が早くなります。 【関連記事】Pythonのトレースバックの読み方
変数のスコープとは何か¶
エラーの理由を知るには、スコープという考え方が欠かせません。スコープとは、ある名前がどこから見えるかという範囲のことです。
関数の中で作った変数は、その関数の中でしか使えません。これをローカルスコープと呼び、ファイルの一番外側で作った変数はグローバルスコープに属します。
Pythonが名前を探す順番(LEGB)¶
Pythonは、変数の名前を見つけると決まった順番で中身を探しに行きます。その順番は、頭文字をとってLEGBと呼ばれています。
| 順番 | 名前 | 意味 | 例 |
|---|---|---|---|
| 1 | Local | いま実行している関数の中 | 関数内で代入した変数 |
| 2 | Enclosing | 外側にある関数の中 | 入れ子になった関数の外側の変数 |
| 3 | Global | そのファイル(モジュール)の一番外側 | ファイルの先頭で作った変数 |
| 4 | Built-in | Pythonにはじめから入っている名前 | print や len |
内側から外側へ、見つかった時点で探すのをやめる仕組みです。だからこそ、関数の中では外の変数をそのまま読むことができます。
名前空間という仕組みとあわせて理解すると、さらに見通しがよくなります。 【関連記事】Pythonの名前空間を理解するとコードがスッキリ!
代入した瞬間にローカル変数になる¶
では、なぜ先ほどのコードは失敗したのでしょうか。理由は、Pythonが関数を実行する前に、関数全体を見渡してローカル変数を決めているからです。
関数の中のどこかで count = ... や count += 1 のような代入があると、その名前は関数全体を通してローカル変数として扱われます。代入より前の行でも、後ろの行でも同じです。
count += 1 は、count = count + 1 と同じ意味です。右側でcountを読もうとした時点では、ローカルのcountにはまだ何も入っていません。
これが、値があるのに値がないと言われる正体です。Python公式のFAQにも、まさにこの疑問がそのまま項目として載っています。
global文で外の変数を書き換える¶
関数の中から、ファイルの一番外側にある変数を書き換えたいときに使うのがglobal文です。関数の先頭で、この名前はグローバル変数のことだと宣言します。
先ほどのコードにglobal文を1行足すと、次のように動くようになります。
count = 0
def press():
global count
count += 1
print(count)
press() # 1
press() # 2
print(count) # 2
global count と書いたことで、関数の中のcountはファイル全体で共有されるcountを指すようになりました。関数を呼ぶたびに、外の値が直接増えていきます。
global文の注意点¶
global文には、守らなければいけない決まりがいくつかあります。代表的なのは、宣言より前でその名前を使ってはいけないというものです。
たとえば print(count) のあとに global count と書くと、実行する前の段階でSyntaxErrorになります。宣言は関数の先頭にまとめて書く、と覚えておけば安心です。
もうひとつ大切なのは、読むだけならglobal文は要らないという点です。値を表示したり、計算に使ったりするだけなら、LEGBの順番で自然に見つけてもらえます。
nonlocal文で外側の関数の変数を書き換える¶
関数の中に関数を書くと、内側の関数から外側の関数の変数を書き換えたくなる場面が出てきます。そこで使うのがnonlocal文です。
global文がファイルの一番外側を指すのに対して、nonlocal文はすぐ外側の関数の変数を指します。次のコードは、呼ぶたびに数が増えるカウンターを作る例です。
def make_counter():
count = 0
def counter():
nonlocal count
count += 1
return count
return counter
c = make_counter()
print(c()) # 1
print(c()) # 2
d = make_counter()
print(d()) # 1
cとdは、それぞれ別のcountを持っています。globalで1つの変数を共有するのと違い、カウンターを作るたびに独立した値が用意されるのが特徴です。
このように、関数が外側の変数を覚えたまま持ち歩く仕組みをクロージャと呼びます。仕組みをもっと深く知りたい方は、こちらの記事が参考になります。 【関連記事】Pythonのクロージャとは?関数が外の変数を覚えたまま持ち歩く仕組み
nonlocal文にも決まりがあります。指定する名前は、外側の関数の中にすでに存在していなければなりません。
外側の関数にその変数がないと、no binding for nonlocal 'x' found というSyntaxErrorになります。ファイルの一番外側の変数はnonlocalの対象にならない点も、覚えておきましょう。
globalとnonlocalを表で比べる¶
ここまでの内容を、一度表で整理しておきます。似た見た目の2つですが、指している場所がまったく違います。
| 項目 | global | nonlocal |
|---|---|---|
| 書き換えられる場所 | ファイルの一番外側の変数 | 外側にある関数の変数 |
| 変数がまだないとき | 関数の中で代入すれば新しく作られる | SyntaxErrorになる |
| 主な使いどころ | 設定値やフラグを更新する小さなスクリプト | クロージャやデコレーターの中の状態 |
| 使いすぎたときの影響 | どこで値が変わったか追いにくい | 入れ子が深くなり読みにくい |
使いどころの行に出てきたデコレーターでも、nonlocalはよく登場します。関数の呼び出し回数を数えるデコレーターなどが典型例です。
デコレーターの基本を押さえておきたい方は、あわせて読んでみてください。 【関連記事】Pythonのデコレーターとは?
書き換えではなく中身を変える場合¶
ここで、少し混乱しやすい例を見てみましょう。リストに要素を足すだけなら、global文を書かなくてもエラーになりません。
items = []
def add_item(name):
items.append(name)
add_item("りんご")
print(items) # ['りんご']
なぜこれは動くのでしょうか。理由は、items という名前に新しい値を代入しているわけではないからです。
items.append() は、itemsが指しているリストの中身を変えているだけです。名前の付け替えが起きていないので、itemsはローカル変数になりません。
反対に、関数の中で items = items + [name] と書くと、代入になるので先ほどと同じUnboundLocalErrorが出ます。名前に代入するのか、中身を変えるのか。この違いを意識できるようになると、スコープのエラーはぐっと減ります。
実務でglobalを使いすぎるとどうなるか¶
global文は便利ですが、実務のコードではあまり歓迎されません。どの関数がいつ値を変えたのか、あとから追いかけるのが難しくなるからです。
私は10年ほどエンジニアとして開発に関わってきましたが、以前、集計スクリプトの数字が日によってずれる不具合を調べたことがあります。原因は、複数の関数がglobalで同じ合計値を書き換えていて、呼ぶ順番しだいで結果が変わっていたことでした。
その調査には丸1日かかりました。それ以来、関数の外の値を変えたくなったら、まず引数と戻り値で書けないかを考えるようにしています。
globalに頼らない書き方¶
globalを使わなくても、多くの場合はもっとわかりやすく書けます。代表的な書き換えの方針を表にまとめました。
| やりたいこと | globalを使う書き方 | おすすめの書き方 |
|---|---|---|
| 値を更新したい | 関数内でglobal宣言して代入 | 新しい値をreturnで返し、呼び出し側で代入 |
| 複数の値を一緒に管理したい | グローバル変数を何個も用意 | クラスにまとめて属性として持つ |
| 呼び出しごとの状態を覚えたい | グローバルなカウンター | クロージャとnonlocal、またはクラス |
一番シンプルなのは、引数で受け取って戻り値で返す形です。先ほどのカウンターなら、次のように書けます。
def press(count):
return count + 1
count = 0
count = press(count)
count = press(count)
print(count) # 2
関数の中で外の世界を変えないので、テストもしやすくなります。同じ引数を渡せば同じ結果が返る関数は、それだけで安心して使えるのです。
とはいえ、数十行の使い捨てスクリプトでglobalを1つ使うくらいなら、目くじらを立てる必要はありません。大切なのは、値を変える場所を少なく保つことだと私は考えています。
よくある疑問¶
最後に、学習中によく聞かれる疑問をいくつか取り上げておきます。どれも、ここまでのルールで説明がつくものばかりです。
for文の変数は関数の外でも残るの?¶
関数の中でfor文を使った場合、ループの変数はその関数のローカル変数になります。関数の外からは見えません。
一方、ファイルの一番外側でfor文を回すと、ループの変数はグローバル変数として残ります。if文やfor文はスコープを作らない、というのがPythonのルールです。
リスト内包表記の変数はどうなる?¶
リスト内包表記の中で使った変数は、内包表記の外には漏れ出しません。Python 3では、内包表記が独自のスコープを持つように扱われるからです。
for文とは振る舞いが違うので、最初は少し戸惑うかもしれません。外で同じ名前を使っていても上書きされない、と覚えておけば十分です。
関数の中でグローバル変数と同じ名前を使ってもいい?¶
使うことはできますが、おすすめはしません。関数の中で代入すると、その関数の中だけの別の変数になり、外の値は変わらないからです。
読む人が、どちらの変数のことかを毎回考えなければならなくなります。名前を分けておくほうが、自分にも周りにも親切です。
まとめ¶
今回は、関数の中から外の変数を扱うためのglobal文とnonlocal文を見てきました。最後にポイントを振り返っておきましょう。
Pythonは、関数の中で代入した名前をその関数のローカル変数として扱います。そのため、外の変数に += で足そうとすると、UnboundLocalErrorが出てしまいます。
ファイルの一番外側の変数を書き換えたいときはglobal、外側の関数の変数を書き換えたいときはnonlocalを使います。読むだけなら、どちらも必要ありません。
そして、リストのappend()のように中身を変えるだけなら宣言は不要です。迷ったら、代入しているかどうかを確かめてみてください。
実務では、globalに頼るより引数と戻り値で値を受け渡すほうが安全です。まずは手元のコードで、global文を使っている関数をひとつ戻り値の形に書き直してみましょう。
ここまでお読みいただきありがとうございました。