順番に読み進めながら学べます

内部関数とクロージャ — global / nonlocal でスコープを操る

Python の内部関数とクロージャ、global / nonlocal によるスコープ操作を図解で解説します。一通り押さえて関数の応用に進めます。

前回までで、関数の引数や戻り値の扱い方を見てきました。今回は関数まわりの応用として、関数の中と外で変数がどう区別されるか(スコープ)、関数の中で関数を定義する内部関数、そして外側の変数を覚えた関数を返すクロージャを整理します。あわせて、外側の変数を関数の中から書き換える globalnonlocal の使いどころも見ていきます。

グローバル変数とローカル変数 — スコープが分かれる

関数の外で定義した変数をグローバル変数、関数の中で定義した変数をローカル変数と呼びます。関数の中からグローバル変数を読むことはできますが、関数の中で同じ名前に = 値 で代入すると、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 — グローバル変数
  • 関数の中からは読むことだけできる
show_stock() のローカル名前空間
  • print(stock) → 外側の 100 を参照
  • ローカル変数は作っていない
try_change() のローカル名前空間
  • stock = 50 — 関数内で新しいローカルを作成
  • 外の stock には影響しない
関数のローカル名前空間はモジュールの中に内包される。外側の値は読みに行けるが、関数内の代入は別物のローカルとして閉じ込められる。

在庫数を表すグローバル変数を関数の中から触ってみて、読むだけなら反映されるが、関数内の代入は別物になることを体感します。

① グローバル変数 stock = 100 を宣言してください。

def show_stock(): を定義し、print(f"関数内: {stock}") を表示してください。

def try_change(): を定義し、stock = 50 のあとに print(f"関数内: {stock}") を表示してください。

show_stock()try_change()print(f"関数外: {stock}") の順に呼び出して、関数の外側の値が変わっていないことを確認してください。

(正しく実行できれば解説が表示されます)

Python エディタ

コードを実行してください

global キーワードで外の変数を書き換える

外のグローバル変数を関数の中から書き換えたいときは、関数の冒頭で global 変数名 と宣言します。「これは外のあのグローバル変数のこと」と Python に伝える宣言で、これがないまま count += 1 のように代入すると、参照と代入が同居して UnboundLocalError(参照前に代入された)というエラーになります。

global で「外のあの変数」と宣言
global なしcount += 1UnboundLocalErrorglobal countcount += 1外の count を書き換え失敗成功
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 で呼び出し元に返し、外側で代入する書き方を選び、状態を持たせたい処理は後で学ぶクラスにまとめましょう。

ページ訪問数を数えるカウンターを global で更新します。

visit_count = 0 を関数の外で宣言してください。

def increment_visit(): を定義し、関数の冒頭に global visit_count を書いてから visit_count += 1 してください。

increment_visit() を 3 回呼び出してから、print(f"訪問数: {visit_count}") を表示してください。

Python エディタ

コードを実行してください

関数と変数のメモリ上の扱い — 名前と実体

Python は内部的に名前空間(namespace)と呼ばれる「名前 → 実体」の対応表を持っています。x = 5 は「メモリ上に整数 5 を作り、名前 x がそこを指す」という登録です。

def f(): ... も同じ仕組みで、関数オブジェクト(関数本体)をメモリに作り、名前 f がそれを指します。プログラム全体で 1 つあるこの対応表がグローバル名前空間で、モジュールの起動時に用意され、終了まで残ります。

モジュールと関数フレームのメモリ配置
グローバル名前空間(モジュール)
  • x = 5 — 名前 x が整数 5 を指す
  • def f — 名前 f が関数オブジェクトを指す
  • プログラムの終了まで残る
f() の 1 回目の呼び出しフレーム
  • 引数とローカル変数を保持
  • return 後にフレームごと破棄
f() の 2 回目の呼び出しフレーム
  • 1 回目とは完全に別物
  • ローカル変数は独立
関数を呼ぶたびに専用のローカル名前空間(フレーム)がメモリ上に新しく作られ、抜けると消える。グローバル名前空間はプログラム全体で 1 つ。
def の定義と関数呼び出しのメモリ上の流れ
①def f(): ...を実行関数オブジェクトをグローバル名前空間に登録プログラム終了までグローバルに残る②1 回目: f() を呼ぶローカル名前空間(フレーム)を新規作成return とともにフレームごと破棄③2 回目: f() を呼ぶ新しいフレームを別途作成(1 回目とは独立)return とともにフレームごと破棄実行時呼ぶ終了時また呼ぶ終了時
def を一度実行するとグローバル名前空間に関数が登録され、プログラム終了まで残る。一方、関数を呼ぶたびに専用のローカルフレームが新しく作られ、return とともに破棄される。続く節の内部関数・クロージャ・nonlocal も、いずれもこのフレーム構造の上に成り立っている。

内部関数 — 関数の中で関数を定義する

関数の中でさらに def を書けば、関数の中だけで使える関数(内部関数)を定義できます。長い関数の中で意味のあるかたまりに名前を付ける目的で使うと、本体の流れが読みやすくなります。

内部関数は外側の関数からしか呼び出せないため、外に出したくない処理を隠す目的にも向きます。

内部関数のスコープ — 集合のイメージ
モジュール(グローバル名前空間)
  • validate直接呼ぶと NameError
  • 外からは存在が見えない
process_user() のフレーム
  • 引数 name / age を保持
  • validate を中から呼べる
validate() のフレーム
  • 外側の name / age参照
  • 外には公開されない
validate は process_user の中だけで生きる関数。外側の引数を参照でき、外からは見えない。
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() のような内部関数に切り出すと、外側の関数本体が「ステップの一覧」として読めるようになります。外で再利用したくなったら、内部関数を外に出して通常の関数にするのは簡単です。

注文を処理する関数の中に、入力チェック専用の内部関数を作って構造を整理します。

def process_order(item, quantity): を定義してください。

② その中に def validate(): を作り、item が空文字または quantityint でない、もしくは quantity が 0 以下なら raise ValueError("無効な注文です") してください。

validate() を呼んだあと、print(f"{item} を {quantity} 個受け付けました") を表示してください。

process_order("りんご", 3) を呼び出して結果を確認してください。

Python エディタ

コードを実行してください

クロージャ — 外の変数を覚えた関数を返す

内部関数は、外側の関数の引数や変数を読むことができます。さらに、外側の関数で内部関数自体を 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
クロージャの仕組み — 定義と呼び出しの流れ
①make_multiplier(3)を呼び出す②factor=3 を持つ外側フレームができる④return multiply(factor=3)③def multiply中で factor を参照⑤times3 = make_multiplier(3)⑥times3(10)→ 10 × 3(factor) = 30実行中で定義覚えた関数を返す受け取り呼ぶ
make_multiplier の外側フレームは return で消えるが、その中で作られた multiply は factor=3 を覚えたまま外に渡される。

クロージャは関数を「設定済み」で配るしくみ

「税率 10%」「税率 8%」のように、設定値だけ違う似た計算をたくさん作りたいときにクロージャが役立ちます。引数で毎回設定値を渡す代わりに、設定済みの関数を 1 つ渡せば済むため、呼び出し側の見通しがよくなります。

割引率を覚えた割引適用関数を返す関数を作ります。

def make_discounter(rate): を定義し、その中に def apply(price): を作って return int(price * (1 - rate)) を返してください(applyreturn するのを忘れずに)。

discount_10 = make_discounter(0.1)discount_30 = make_discounter(0.3) で 2 つの関数を用意してください。

print(discount_10(1000))print(discount_30(1000)) を表示し、同じ価格に異なる割引率が適用されることを確認してください。

Python エディタ

コードを実行してください

nonlocal — 外側の関数の変数を書き換える

クロージャは外側の変数を読むことはできますが、global のときと同じく count += 1 のように書き換えようとすると UnboundLocalError になります。これを許可するキーワードが nonlocal です。global がモジュール直下を対象にするのに対し、nonlocal1 つ外側の関数のローカル変数を対象にします。

状態を関数オブジェクトの中に閉じ込められる

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 とは別物
nonlocal で外側関数の変数を更新 — 集合のイメージ
モジュール(グローバル名前空間)
  • counter = create_counter() — increment を取得
  • 外側からは x には直接触れない
create_counter() のフレーム
  • x = 0 — 1 度だけ作られるカウンター
  • increment が nonlocal で参照する対象
increment() のフレーム
  • nonlocal x — 1 つ外側の x を指す
  • x += 1外側の x を更新
nonlocal は 1 つ外側の関数フレームの変数を指す。グローバルに漏らさずに、関数オブジェクトの中で状態を持てる。

注文 ID を 1 から順に発行するID ジェネレーターのような関数を、クロージャと nonlocal で作ります。

def create_order_id_issuer(): を定義し、関数の冒頭で next_id = 1 を初期化してください。

② 中に def issue(): を作り、nonlocal next_id のあとで current = next_idnext_id += 1return current の順に書いてください。

③ 最後に return issue で内部関数を返してください。

issue_id = create_order_id_issuer() と取得し、print(issue_id()) を 3 回続けて呼び出して、1 → 2 → 3 と増えることを確認してください。

Python エディタ

コードを実行してください
QUIZ

理解度チェック

まずは1問ずつ答えてみましょう。

Q1次のコードを実行したときの出力はどれですか?
stock = 100
def f():
stock = 50
f()
print(stock)

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)