こんにちは、かつコーチです。
今回は、Pythonで関数を書き始めた人がほぼ全員一度はつまずく「スコープ」について解説します。
スコープとは、変数が参照できる範囲のことです。
「関数の中で定義した変数が、関数の外から見えない」「逆に、関数の外の変数を関数の中で書き換えようとしたらエラーになった」といった経験はないでしょうか。
私自身、駆け出しの頃にこの仕組みを理解しないまま関数を書いていて、UnboundLocalErrorという謎のエラーに何十分も悩まされたことがあります。
この記事を読めば、ローカル変数とグローバル変数の違い、global文の使い方、そして意図せずローカル変数扱いになってしまうハマりどころまで、まとめて理解できます。
スコープとは?ローカル変数とグローバル変数の基本
ローカル変数とグローバル変数の定義
まず、2つの用語を整理します。
- ローカル変数:関数の中で定義され、その関数の中でしか参照できない変数
- グローバル変数:関数の外(モジュールの最上位)で定義され、モジュール全体から参照できる変数
コードで見てみましょう。
message = "こんにちは" # グローバル変数
def greet():
name = "かつコーチ" # ローカル変数
print(message, name)
greet()
print(message)
# print(name) # ここでNameErrorになる
messageは関数の外で定義されているのでグローバル変数、nameは関数の中で定義されているのでローカル変数です。
greet()関数の中からは、グローバル変数のmessageを読み取ることができます。
一方、nameはgreet()関数の中でしか存在しないため、関数の外でprint(name)を実行するとNameErrorになります。
なぜスコープという仕組みが必要なのか
「全部グローバル変数にすれば楽なのでは」と思うかもしれません。
しかし、関数がすべての変数を共有してしまうと、次のような問題が起こります。
- ある関数の中でうっかり変数を上書きしてしまい、別の関数の動作がおかしくなる
- 変数名の衝突を避けるために、常に長くて分かりにくい変数名を使う必要が出てくる
- コードのどこで変数が変更されたのか追跡しづらくなる
ローカル変数の仕組みがあることで、関数の中は関数の中だけで完結するという安心感が生まれます。
これはPythonに限らず、多くのプログラミング言語で採用されている考え方です。
global文の使い方
関数の中からグローバル変数を「書き換える」には
先ほどの例のように、関数の中からグローバル変数を読むだけなら、特別な記述は不要です。
しかし、関数の中でグローバル変数に代入(書き換え)をしようとすると、話が変わってきます。
count = 0
def increment():
count = count + 1 # ここでエラーになる
print(count)
increment()
実行すると、次のようなエラーが出ます。
UnboundLocalError: cannot access local variable 'count' where it is not associated with a value
私が最初にこのエラーを見たときは、「グローバル変数のcountはちゃんと0で定義してあるのに、なぜ『値がない』と言われるのか」と混乱しました。
原因は、関数の中にcount = count + 1という代入文があることです。
Pythonは関数の中に代入文を見つけると、その時点で「この関数の中ではcountはローカル変数として扱う」と決めてしまいます。
その結果、右辺のcountを評価する段階では、まだローカル変数のcountに値が入っていないため、UnboundLocalErrorになるのです。
global文でグローバル変数だと明示する
この問題を解決するのがglobal文です。
global文を使うと、「この変数は関数の外にあるグローバル変数を使う」とPythonに明示できます。
count = 0
def increment():
global count
count = count + 1
print(count)
increment() # 1
increment() # 2
print(count) # 2
global countと宣言することで、関数の中のcountはグローバル変数のcountそのものを指すようになります。
そのため、関数を呼び出すたびにグローバル変数のcountが更新されていきます。
globalを使うときの注意点
global文は関数の先頭付近、変数を使う前に書くのが基本です。
def increment():
count = count + 1 # ❌ global宣言より先に使ってしまっている
global count
このようにglobal宣言より前に変数を使ってしまうと、SyntaxErrorになります。
SyntaxError: name 'count' is used prior to global declaration
global文は必ず、その変数を使い始める前に書くようにしましょう。
よくあるつまずきポイント・エラー対処
つまずき1:関数内で新しい変数のつもりが同名だった
意図せずローカル変数扱いになってしまう、代表的なケースを紹介します。
❌ Before:グローバル変数のつもりが別のローカル変数になっていた
total = 0
def calculate(price, tax_rate):
total = price * (1 + tax_rate) # グローバルのtotalを更新したいつもり
return total
calculate(1000, 0.1)
print(total) # 0のまま(意図と違う)
このコードを書いた本人は「合計金額をグローバル変数のtotalに反映させたい」と思っているかもしれません。
しかし、実際には関数内のtotal = ...によって、関数内だけで完結する新しいローカル変数totalが作られています。
そのため、関数の外のtotalは0のまま変化しません。
実際に私も、複数のファイルにまたがる計算処理で「合計が更新されない」というバグに遭遇し、原因を探ったところ、この「同名だが別物のローカル変数」が原因でした。
✅ After:戻り値で結果を受け取るか、globalを明示する
同じ処理を、意図が明確になるように書き直します。
total = 0
def calculate(price, tax_rate):
result = price * (1 + tax_rate)
return result
total = calculate(1000, 0.1)
print(total) # 1100.0
多くの場合、グローバル変数を書き換えるより、戻り値(return)で結果を受け渡す方が安全です。
戻り値とは、関数の処理結果として呼び出し元に返される値のことです。
どうしてもグローバル変数自体を更新したい場合は、先ほど紹介したglobal文を明示的に使います。
total = 0
def calculate(price, tax_rate):
global total
total = price * (1 + tax_rate)
calculate(1000, 0.1)
print(total) # 1100.0
「グローバル変数を書き換えたいのか、新しいローカル変数を作りたいだけなのか」を意識して書くことが、このバグを避ける一番のポイントです。
つまずき2:リストや辞書は代入と勘違いしやすい
もう1つ、初中級者がよく混乱するのが「変更(ミューテーション)」と「代入」の違いです。
ミューテーションとは、変数の中身(オブジェクト)そのものを変更する操作のことです。
items = []
def add_item(name):
items.append(name) # これは代入ではなくミューテーション
add_item("りんご")
add_item("みかん")
print(items) # ['りんご', 'みかん']
items.append(name)はitems = ...という代入文ではないため、Pythonはitemsをローカル変数だと判断しません。
そのため、global文がなくても、関数の中からグローバル変数のリストに要素を追加できます。
一方、次のように書くと動きが変わります。
items = []
def reset_items():
items = [] # これは代入なので、ローカル変数になる
print(items) # []
reset_items()
print(items) # ['りんご', 'みかん'] のまま変わらない
「関数の中で=を使って代入しているかどうか」が、ローカル変数扱いになるかどうかの境界線になります。
この違いを意識しておくと、リストや辞書がらみのスコープの混乱をかなり減らせます。
応用・一歩先の使い方
nonlocal文:関数の中の関数でスコープを扱う
関数の中に、さらに関数を定義する(入れ子関数)ケースでは、globalではなくnonlocal文を使います。
def make_counter():
count = 0
def increment():
nonlocal count
count += 1
return count
return increment
counter = make_counter()
print(counter()) # 1
print(counter()) # 2
print(counter()) # 3
nonlocalは、「1つ外側の関数のローカル変数」を書き換えたいときに使います。
globalが「モジュール全体のグローバル変数」を対象にするのに対し、nonlocalは「外側の関数のスコープ」を対象にする点が違いです。
このような、外側の変数を覚えている関数のことをクロージャと呼びます。
カウンターやキャッシュの仕組みを作るときによく登場するパターンなので、覚えておくと応用が効きます。
実務では「グローバル変数を極力使わない」設計が基本
ここまでglobal文の使い方を解説してきましたが、実務のコードではグローバル変数の書き換えはできるだけ避けるのがセオリーです。
理由は、グローバル変数を複数の関数から書き換えられるようにすると、「どの関数がいつ値を変えたのか」が追いづらくなり、バグの温床になりやすいからです。
代わりに、関数の引数として値を渡し、戻り値として結果を受け取る設計にすると、処理の流れが追いやすくなります。
# グローバル変数を書き換える設計(避けたい)
total = 0
def add_price(price):
global total
total += price
# 引数と戻り値でやり取りする設計(推奨)
def add_price(total, price):
return total + price
total = 0
total = add_price(total, 1000)
total = add_price(total, 500)
print(total) # 1500
global文の存在自体は知っておく必要がありますが、「使わずに済む設計ができないか」を先に考える習慣をつけましょう。
まとめ
この記事のポイント
- ローカル変数は関数の中だけで有効、グローバル変数はモジュール全体で有効
- 関数の中でグローバル変数に代入すると、
UnboundLocalErrorや意図しないローカル変数化が起こる - グローバル変数を書き換えたいときは
global文を、変数を使う前に宣言する - リストや辞書への
appendなどのミューテーションは代入ではないため、globalなしでも変更できる - 入れ子関数の外側の変数を書き換えたいときは
nonlocal文を使う - 実務ではグローバル変数の書き換えを避け、引数と戻り値でやり取りする設計が基本
次に読むべき記事
スコープの仕組みが分かったら、次は関数の柔軟な引数の受け取り方を押さえていきましょう。
→ 次の記事:Pythonの可変長引数(*args・**kwargs)の使い方
タグ: Python, 中級者向け, 基本文法