Q1次のコードを実行したときの出力はどれですか?stock = 100
def f():
stock = 50
f()
print(stock)
内部関数とクロージャ — global / nonlocal でスコープを操る
Python の内部関数とクロージャ、global / nonlocal によるスコープ操作を図解で解説します。一通り押さえて関数の応用に進めます。
前回までで、関数の引数や戻り値の扱い方を見てきました。今回は関数まわりの応用として、関数の中と外で変数がどう区別されるか(スコープ)、関数の中で関数を定義する内部関数、そして外側の変数を覚えた関数を返すクロージャを整理します。あわせて、外側の変数を関数の中から書き換える global と nonlocal の使いどころも見ていきます。
グローバル変数とローカル変数 — スコープが分かれる
関数の外で定義した変数をグローバル変数、関数の中で定義した変数をローカル変数と呼びます。関数の中からグローバル変数を読むことはできますが、関数の中で同じ名前に = 値 で代入すると、Python は新しいローカル変数を作るため、外の変数とは別物になります。
変数のメモリ上の場所を id() で見ると、関数の外と中で別の場所を指していることが確認できます。
stock = 100 # グローバル変数
def show_stock():
print(f"関数内: {stock}") # 100 — 外を参照できる
def try_change():
stock = 50 # ここで作られるのは新しいローカル変数
print(f"関数内: {stock}") # 50
show_stock() # 関数内: 100
try_change() # 関数内: 50
print(f"関数外: {stock}") # 関数外: 100 ← 外は変わらない
- stock = 100 — グローバル変数
- 関数の中からは読むことだけできる
print(stock)→ 外側の 100 を参照- ローカル変数は作っていない
stock = 50— 関数内で新しいローカルを作成- 外の
stockには影響しない
global キーワードで外の変数を書き換える
外のグローバル変数を関数の中から書き換えたいときは、関数の冒頭で global 変数名 と宣言します。「これは外のあのグローバル変数のこと」と Python に伝える宣言で、これがないまま count += 1 のように代入すると、参照と代入が同居して UnboundLocalError(参照前に代入された)というエラーになります。
visit_count = 0
# × global なしだと UnboundLocalError
# def increment():
# visit_count += 1
# increment() ← UnboundLocalError
# ○ global で外のグローバル変数を書き換える
def increment():
global visit_count
visit_count += 1
increment()
increment()
increment()
print(visit_count) # 3
global は最小限に
global を使うと、関数の中から外の状態が黙って書き換わるコードになります。1 か所での書き換えが遠くのロジックを壊しやすく、規模が大きくなるほど追跡が難しくなります。
基本は 値を return で呼び出し元に返し、外側で代入する書き方を選び、状態を持たせたい処理は後で学ぶクラスにまとめましょう。
関数と変数のメモリ上の扱い — 名前と実体
Python は内部的に名前空間(namespace)と呼ばれる「名前 → 実体」の対応表を持っています。x = 5 は「メモリ上に整数 5 を作り、名前 x がそこを指す」という登録です。
def f(): ... も同じ仕組みで、関数オブジェクト(関数本体)をメモリに作り、名前 f がそれを指します。プログラム全体で 1 つあるこの対応表がグローバル名前空間で、モジュールの起動時に用意され、終了まで残ります。
- x = 5 — 名前
xが整数 5 を指す - def f — 名前
fが関数オブジェクトを指す - プログラムの終了まで残る
- 引数とローカル変数を保持
- return 後にフレームごと破棄
- 1 回目とは完全に別物
- ローカル変数は独立
内部関数 — 関数の中で関数を定義する
関数の中でさらに def を書けば、関数の中だけで使える関数(内部関数)を定義できます。長い関数の中で意味のあるかたまりに名前を付ける目的で使うと、本体の流れが読みやすくなります。
内部関数は外側の関数からしか呼び出せないため、外に出したくない処理を隠す目的にも向きます。
validateを直接呼ぶと NameError- 外からは存在が見えない
- 引数
name/ageを保持 - validate を中から呼べる
- 外側の
name/ageを参照 - 外には公開されない
def process_user(name, age):
def validate():
if not name or not isinstance(age, int) or age < 0:
raise ValueError("無効な入力です")
validate() # 内部関数を呼び出し
print(f"{name}({age})の処理を実行しました")
process_user("太郎", 25)
# 太郎(25)の処理を実行しました
# process_user の外から validate は呼べない
# validate() ← NameError
長い関数を分けたいときに有効
1 つの関数が 50 行・100 行と長くなってきたら、かたまりごとに process_name() / process_age() のような内部関数に切り出すと、外側の関数本体が「ステップの一覧」として読めるようになります。外で再利用したくなったら、内部関数を外に出して通常の関数にするのは簡単です。
クロージャ — 外の変数を覚えた関数を返す
内部関数は、外側の関数の引数や変数を読むことができます。さらに、外側の関数で内部関数自体を return で返すと、外側の値を覚えたまま動く関数を作れます。これをクロージャと呼びます。
例えば「3 倍する関数」「5 倍する関数」のように、設定値だけが違う似た関数を量産したいときに便利です。設定値(factor)を外側の関数の引数で受け取り、内部関数の中で参照すれば、make_multiplier(3) の結果は「3 を覚えた multiply」、make_multiplier(5) は「5 を覚えた multiply」になります。
def make_multiplier(factor):
def multiply(x):
return x * factor # 外側の factor を参照
return multiply
times3 = make_multiplier(3) # factor=3 を覚えた関数
times5 = make_multiplier(5) # factor=5 を覚えた別の関数
print(times3(10)) # 30
print(times5(10)) # 50
print(times3(7)) # 21
クロージャは関数を「設定済み」で配るしくみ
「税率 10%」「税率 8%」のように、設定値だけ違う似た計算をたくさん作りたいときにクロージャが役立ちます。引数で毎回設定値を渡す代わりに、設定済みの関数を 1 つ渡せば済むため、呼び出し側の見通しがよくなります。
nonlocal — 外側の関数の変数を書き換える
クロージャは外側の変数を読むことはできますが、global のときと同じく count += 1 のように書き換えようとすると UnboundLocalError になります。これを許可するキーワードが nonlocal です。global がモジュール直下を対象にするのに対し、nonlocal は1 つ外側の関数のローカル変数を対象にします。
状態を関数オブジェクトの中に閉じ込められる
nonlocal は、呼び出すたびに値が増えていくカウンターを内部に閉じ込めたいときによく使う書き方です。
global と違って状態を特定の関数オブジェクトの中に閉じ込められるので、副作用が広がりにくく、global よりも安全に状態を持たせられます。
def create_counter():
x = 0
def increment():
nonlocal x # 外側 create_counter の x を更新する宣言
x += 1
return x
return increment
counter = create_counter()
print(counter()) # 1
print(counter()) # 2
print(counter()) # 3
# 別のカウンターを作ると x は独立
counter2 = create_counter()
print(counter2()) # 1 — counter とは別物
counter = create_counter()— increment を取得- 外側からは
xには直接触れない
- x = 0 — 1 度だけ作られるカウンター
- increment が
nonlocalで参照する対象
nonlocal x— 1 つ外側のxを指すx += 1で外側の x を更新
理解度チェック
まずは1問ずつ答えてみましょう。
Q2次のコードがエラーになる理由として最も適切なものはどれですか?count = 0
def inc():
count += 1
inc()
Q3次のコードで print(times3(10)) の出力はどれですか?def make_multiplier(factor):
def multiply(x):
return x * factor
return multiply
times3 = make_multiplier(3)