2.16. スコープ

Python が関数の内部で名前を探すとき、特定の順序のスコープを検索します。その順序を理解すると、なぜある代入は外側の名前を覆い隠し、別の代入はそれを変更するのか、そしてなぜネストされた関数が定義された場所の値を覚えていられるのかが分かります。

組み込みスコープの中にモジュールスコープがあり、 その中にローカルスコープがあるという入れ子の箱が示されており、 名前の検索が最も内側のフレームから外側へ進むことを 矢印が示しています。

名前の検索は、ローカルの関数スコープから始まり、一致が見つかるまで、モジュールスコープと組み込みスコープへと外側に向かって進みます。

2.16.1. ローカルスコープとモジュールスコープ

関数の内部で定義された名前は、その関数にローカルであり、呼び出しが終わると消えます。

def f():
    x = 10
    print(x)

f()
print(x)              # NameError: x is not defined

.py ファイルのトップレベルで定義された名前は、モジュールレベルグローバルと呼ばれることもあります)であり、そのファイル内のどこからでも、関数の内部も含めて見ることができます。

CAMERA = "OpenMV"

def banner():
    print("running on", CAMERA)

関数の内部からモジュールレベルの名前を読み取ることは自動的に行われます。関数の内部からその名前に代入することは違います -- その代入は新しいローカル変数を作り、その呼び出しの残りの間、モジュールレベルのものを覆い隠します。

counter = 0

def bump():
    counter = counter + 1     # UnboundLocalError

左辺の countercounterbump 内のローカル名にするため、右辺の読み取りには見つかる値がありません。

2.16.1.1. global キーワード

関数の内部からモジュールレベルの名前を実際に再代入するには、先に global と宣言します。

counter = 0

def bump():
    global counter
    counter += 1

global は控えめに使ってください。隠れた状態を変更する関数は、値を引数として受け取り新しい値を返す関数よりも、理解するのが難しくなります。「状態を共有する必要がある」場合の通常の解決策は、オブジェクト(リスト、辞書、クラスインスタンス)を引数として渡し、代わりにそれを変更することです。

2.16.2. ラムダ

lambda は、単一の式の中で小さな無名関数を作ります。

square = lambda x: x * x
square(7)             # 49

これは次と完全に等価です

def square(x):
    return x * x

lambda の本体は単一の式でなければなりません -- 文も、複数行も使えません。主な用途は、関数を取る何かに引数として小さな関数を渡すことです。

pairs = [("b", 2), ("a", 3), ("c", 1)]
pairs.sort(key=lambda item: item[1])
# [('c', 1), ('b', 2), ('a', 3)]

本体が 1 つの式を超えて大きくなったら、本物の def に切り替えてください。def で関数に名前を付けると、トレースバックでも名前が付きます。これは lambda にはないものです。

2.16.3. クロージャ

別の関数の内部で定義された関数は、それを囲む関数のスコープから名前を読み取ることができます。内側の関数はそれらの名前をキャプチャし、外側の呼び出しが返った後でも動き続けます。

def make_adder(n):
    def add(x):
        return x + n
    return add

add5 = make_adder(5)
add10 = make_adder(10)
print(add5(100), add10(100))

出力:

105 110

add5add10 は 2 つの別々の関数で、それぞれが自分自身の n を覚えています。このようにカスタマイズした内側の関数を作って返す関数をクロージャと呼びます。これは、そもそも言語にネストされた関数が必要となる主な理由です -- ある状態を関数の値に焼き込み、その値を単一の呼び出し可能なものとして渡す方法なのです。

キャプチャした名前の読み取りは自動的に行われます。その 1 つを再束縛するには、追加のキーワードが必要です。以下の例は、正しく見えることをしていますが失敗します。

def make_counter():
    count = 0
    def tick():
        count = count + 1     # UnboundLocalError
        return count
    return tick

tick 内の count への代入は、トップレベルの関数でローカルにしたのと同じように、counttick にローカルにします。nonlocal キーワードは Python に「この名前は囲む関数の中にあるので、そこで再束縛せよ」と伝えます。

def make_counter():
    count = 0
    def tick():
        nonlocal count
        count += 1
        return count
    return tick

c = make_counter()
print(c(), c(), c())

出力:

1 2 3

nonlocal が囲む関数のスコープに対する関係は、global がモジュールスコープに対する関係と同じです。なお、キャプチャしたオブジェクトを変更すること(some_list.append(...)some_dict[k] = v の呼び出し)には nonlocal は不要です -- 名前が再束縛されているのではなく、それが指すオブジェクトが変更されているだけだからです。