オライリー・ジャパンの書籍effective pythonのメモ書きです。
https://www.oreilly.co.jp/books/9784873117560/
P31~35
クロージャのスコープを把握しておくことで、綺麗なコードが書けるようになる
ソートの例
数字を順にソートするが、優先したい数字があるとする
def sort_priority(values, group):
def helper(x):
if x in group:
return (0, x)
return (1, x)
values.sort(key=helper)
numbers = [8, 3, 1, 2, 5, 4, 7, 6]
group = {2, 3, 5, 7}
sort_priority(numbers, group)
print(numbers)
>>>
[2, 3, 5, 7, 1, 4, 6, 8]
クロージャの概念を知らないと、動作がイメージしにくいと思います
ここでは3つのポイントがあります
- クロージャの仕組みによって、helper関数がsort_priority関数の引数であるgroupにアクセスできる
- pythonの関数が第一級オブジェクトであるため、sortメソッドのkeyの引数としてhelper関数が代入できる
- helper関数の戻り値であるタプルによって、最初の添字が0と1に別れることで、優先順が別れる
優先リストに含まれるかどうかの判別を組み込む
上記のコードを元に、優先リストに含まれる場合にTrueを返す実装がしたいとする
def sort_priority2(numbers, group):
found = False
def helper(x):
if x in group:
found = True #ここでTrueになるはずだが。。。
return(0, x)
return(1, x)
numbers.sort(key=helper)
return found
found = sort_priority2(numbers, group)
print('Found', found)
print(numbers)
>>>
Found False
[2, 3, 5, 7, 1, 4, 6, 8]
本来、計算ではFoundはTrueになるはずだが、何故かFalseを返している。
この理由はクロージャのスコープにある。
#スコープを理解しておかないと、不可解な挙動に悩まされる
上記のコードがFalseを返さないのはFoundのスコープがhelper関数の外にとどまっていることが原因。
つまり
def sort_priority2(numbers, group):
found = False # ここのスコープでfoundが存在するため。。
def helper(x):
if x in group:
found = True # ここのスコープまで参照しに行かない
return(0, x)
return(1, x)
numbers.sort(key=helper)
return found
そのため、1つ上のスコープにあるFalseが返されてしまいます。
これを回避するために、python3ではnonlocal関数が用意されています。
nonlocalはスコープをクロージャの外に出す
def sort_priority3(numbers, group):
found = False
def helper(x):
nonlocal found # ここでfoundのスコープはhelper関数の外に出された
if x in group:
found =True
return(0, x)
return(1, x)
numbers.sort(key=helper)
return found
found = sort_priority3(numbers, group)
print('Found', found)
print(numbers)
>>>
Found True
[2, 3, 5, 7, 1, 4, 6, 8]
これで意図した動きになりました。
ただし、nonlocalを大規模な関数で使用すると、意図しない部分の範囲まで影響が及ぶため、かなり注意が必要。
万全を期するなら、nonlocal関数の代わりに、同様なクラスでラップすることが良い
class Sorter(object):
def __init__(self, group):
self.group = group
self.found = False
def __call__(self, x):
if x in self.group:
self.found = True
return(0, x)
return(1, x)
sorter = Sorter(group)
numbers.sort(key=sorter)
assert sorter.found is True
>>>
(assertによる例外は発生しない)
こちらの方がスコープを気にせずに使えるでしょう。
ちなみにpython2ではnonlocalはサポートされていません。
結論
- クロージャのスコープを把握することで、簡潔なコードが書ける
- スコープの範囲を把握しないと、意図しない動作が発生する
- python3はnonlocalを使用することで、クロージャ外にスコープを広げることができる(ただし単純な関数のみに限定したほうが良い)