何かを書き留める何か

数学や読んだ本について書く何かです。最近は社会人として生き残りの術を学ぶ日々です。

『Effective Python』Item 15: クロージャと変数スコープの関係を知ろう

『Effective Python』の続き。Pythonのスコープを勉強しなおした。 www.effectivepython.com

クロージャを理解しようとして書いたのが次の実用性に欠けるコードである。

def outer(out):
    def middle(mid):
        def inner(inn):
            print("In inner", out)
            print("In inner", mid)
            print("In inner", inn)
        inner('a')
        print("In midle", out)
        print("In midle", mid)
        # print("In inner", inn) 
    middle('b')
    print("In outer", out)
    # print("In midle", mid)
    # print("In inner", inn)

outer('c')

実行結果は次の通り。

In inner c
In inner b
In inner a
In midle c
In midle b
In outer c

なお、コメントを外すとNameErrorが発生する。

この辺の動きは『Python文法詳解』を読むのが手っ取り早い。

Python文法詳解

Python文法詳解

関数の内部で定義した関数は、外側の関数のローカル変数も参照できます。関数が何重にネストしていても、もっとも内側の関数から、外側の関数全てのローカル変数を参照できます。 内側の関数から外側の変数を参照したとき、その値は、内側の関数を定義したと時の値ではなく、内側の関数が呼び出され、実際に変数を参照した時の値になります。(P211)

では、次の例を見てみる。『Effective Python』の例である あるグループに属する整数が優先されるように整数のリストをソートしたい、というシチュエーションを踏まえた例である。

def sort_priority2(numbers, group):
    found = False
    def helper(x):
        if x in group:
            found = True  # Seems simple
            return (0, x)
        return (1, x)
    numbers.sort(key=helper)
    return found

numbers = [8, 3, 1, 2, 5, 4, 7, 6]
group = {2, 3, 5, 7}
found = sort_priority2(numbers, group)
print('Found:', found)
print(numbers)

実行結果は

Found: False
[2, 3, 5, 7, 1, 4, 6, 8]

となり、間違った結果となっている。

これも、『Python文法詳解』が役に立つ。『Effective Python』にもあるが。

関数外の名前空間で定義された自由変数は、内側の関数で上書きすることが出来ません。(P212)

ここでいう上書きというのがはっきりしないなあと思ったのが、Python 2の常套手段である次のコード。sort_priority2を改良したものである。Python 3でも動作する。

def sort_priority(numbers, group):
    found = [False]
    def helper(x):
        if x in group:
            found[0] = True
            return (0, x)
        return (1, x)
    numbers.sort(key=helper)
    return found[0]

この場合、上書きできないのはfoundがリストを参照していることであり、そのリスト内を書き換えることは出来る、と理解した。 なお、Python 3向けの解決方法は本文を見ていただくとする。

『Effective Python』の最後に、

This approach also works when the variable used to traverse the scope is a dictionary, a set, or an instance of a class you're defined.

とあるが、これは上記のPython 2向けアプローチはリストの他に辞書、集合、ユーザ定義のクラスのインスタンスに有効ですよ、という話を上記の例をリストから辞書や集合に置き換えても成立しますよ、という意味と勘違いして1日困っていた。ああ自分の読解力に不安がよぎる…。