# クロージャと nonlocal

自分が定義された時の
スコープを覚えている
関数です。

ここで見ていくクロージャは、主にデコレータで使う機能です。 デコレータが使えれば十分なので、細かい仕様は気にせず、気楽にざっくり流していただければと思います。

また PEP 227 と PEP 3104 の関係については、 コーディングを支える技術 の 「第7章 名前とスコープ」から知ることができました。 ありがとうございます。

# 1. クロージャ

# 1.1. 普通の関数

例えば、関数 f は自身が定義されたグローバルスコープを覚えています。 x を参照しても 0 を返してくれます。

x = 0
def f():       # 関数 f は自由変数 x を持つ
    return x   # クロージャ

f()
>>> f()
0
>>>

関数または関数の中で定義された関数に自由変数がある場合、 実行される各 def 文または lambda 式は  クロージャ  を作成します (訳注: ここで言う "実行" とは、定義した関数を実行する時ではなく、 関数を定義する時のことを指しています)
Each def or lambda expression that is executed will create a closure if the body of the function or any contained function has free variables.
PEP 227 -- Statically Nested Scopes

プログラミングにおいては、  自由変数  とは関数の中で参照される局所変数や引数以外の変数を意味する (訳注: ローカルスコープ以外から来た変数だと思えばいいかなと思います。上の関数 f の例で言えば、変数 x が該当します)
自由変数と束縛変数 - Wikipedia

ポイント

関数の中で定義された関数だけでなく、

普通の関数もクロージャらしい。

# 1.2. 関数の中で定義された関数

関数の中で関数を定義しました。 このとき関数 g は自分が定義されたスコープを覚えています。 関数 f のローカルスコープです。

y = 0
def f():
    y = 1
    def g():      # 関数 g は自由変数 y を持つ
        return y  # クロージャ
    return g

g_ = f()
g_()
>>> g_()
1
>>> 

用語集には機能とあるのですが、 いまいるスコープの外側のスコープを nested スコープと呼んで差し支えないかなと思っています。 以後 nested スコープと呼称していきます。

nested scope - 用語集
(ネストされたスコープ) 外側で定義されている変数を参照する機能です。 例えば、ある関数が別の関数の中で定義されている場合、内側の関数は外側の関数中の変数を参照できます。ネストされたスコープはデフォルトでは変数の参照だけができ、変数の代入はできないので注意してください。ローカル変数は、最も内側のスコープで変数を読み書きします。同様に、グローバル変数を使うとグローバル名前空間の値を読み書きします。 nonlocal で外側の変数に書き込めます。

PEP 227 によって Python 2.2 から nested スコープが導入されました。 より正確には Python 2.1 でも from __future__ import nested_scopes を宣言することで使えたようです。

PEP 227 で nested スコープが導入されたというのは、 Python 2.1 以前では local スコープに変数がなければ、 直接 global スコープが参照されていました。 上記のコードの実行結果は 1 ではなく 0 が返されていたということです。

>>> g_()
0
>>> 
ポイント

関数の中で定義された関数の中に、

変数がなかった場合、

外側の変数が参照される。

# 2. クロージャの使いどき

さてこんな機能、どこで使うのでしょうか? ここでは関数の中で定義された関数が nested スコープを使う例について2つ思いついたのですが、 いいサンプルコードが思い浮かびませんでした... orz

# 2.1. デコレータ

引数を取るデコレータを定義したい時は、nested スコープが欲しいです。 例えば、実行時間を計測するデコレータを書きました。

#
# 対話モード >>> に
# コピペで動きます。
#
import time


def measure(func):
    def wrapper(*args, **kargs):  # <--- クロージャ(自由変数 func が存在する)
        # 開始時間
        start_time = time.perf_counter()
        
        result = func(*args, **kargs)
        
        # 終了時間
        end_time = time.perf_counter()
        
        # 実行時間 = 終了時間 - 開始時間
        execution_time = end_time - start_time
        print(f'{func.__name__}: {execution_time}')
        return result
    
    return wrapper


# 素因数分解
@measure
def factorize(n):
    b = 2
    fct = []
    while b * b <= n:
        while n % b == 0:
            n //= b
            fct.append(b)
        b = b + 1
    if n > 1:
        fct.append(n)
    return fct


factorize(6700417 * 2147483647)
>>> factorize(6700417 * 2147483647)
factorize: 1.5311630189999998
[6700417, 2147483647]
>>>

私はPythonを関数型言語として見ていないが、クロージャの導入は価値があったと考えている。クロージャは他の多くのプログラミング機能の拡張の開発の役に立っているからである。例えば、新スタイルクラスや、デコレータなど、いくつかの新しい機能はクロージャを利用している。
(訳注: クロージャはもともと関数型言語を参考にして導入された機能だそうです。次に見る、カリー化と部分適用も関数型言語でのテクニックだと聞いています。)
Pythonの"関数型"の機能の起源 - The History of Python.js

# 2.2. カリー化と部分適用

カリー化、部分適用をしたい時は、nested スコープが欲しいです。

#
# 対話モード >>> に
# コピペで動きます。
#

# 通常
# def floordiv(a, b):
#     return a // b

# Step 1. カリー化
def splitter(b):
    def split(a):  # <--- クロージャ(自由変数 b が存在する)
        return a // b
    return split

# Step 2. 部分適用
half = splitter(2)

half(4)
>>> half(4)
2
>>> 

カリー化, 部分適用については Google 先生にお尋ねください。 正直、カリー化するメリットは、よくわかっていません。

ちなみに関数を定義し直すこと、カリー化することが面倒だなと思ったら、 functools.partial を使うと既にある関数を元にカリー化することなく部分適用ができるそうです。

# ◯ teratail の説明

クロージャを積極的に使うのは、あまりないかなと思っています。 teratail でも似たような質問がありました。

# ◯ Wikipedia の説明

Wikipedia に詳しく記述されています。自分は、理解できていません笑

まとめ

引数を取るデコレータ

カリー化と部分適用

# 3. nonlocal 文

クロージャが導入されたことで nested スコープの変数を参照できるようになりました。 しかし、そのままでは変更することができません。 そこで PEP 3104 によって Python 3.0 から nonlocal 文が導入されました。

# 3.1. カウンタ

さっそく動作を見てみましょう。

def create_counter():
    c = 0
    def count():
        nonlocal c  # 1つ外側の変数 c を束縛する。
        c = c + 1
        return c
    return count

count = create_counter()
count()
count()
count()
count()
count()
>>> count()
1
>>> count()
2
>>> count()
3
>>> count()
4
>>> count()
5
>>> 

nonlocal 文は、 列挙された識別子がグローバルを除く 一つ外側のスコープで先に束縛された変数を 参照するようにします。
7.13. nonlocal 文

# 3.2. 擬似乱数

擬似乱数生成する関数です。

def linear_congruential(a, x, b, m):
    def _():
        nonlocal x
        x = (a * x + b) % m
        return x
    return _


random = linear_congruential(48271, 8, 0, 2**31 - 1)
random()  # 386168
random()  # 1460846352
random()  # 1741224500

一次関数と剰余算が一緒になっている場合、線形合同法による擬似乱数を生成している可能性があります。 昔、競プロの問題でこれの規則を見つける問題と勘違いして辛い思いをしました。

答え

実行結果の組み合わせとして、正しいものはどれですか?

def create_counter():
    c = 0
    def count():
        nonlocal c
        c = c + 1
        return c
    return count

count_a = create_counter()
count_b = create_counter()
count_c = create_counter()

count_a()
count_b()
count_b()
count_c()
count_c()
count_c()
>>> count_a()
実行結果
>>> count_b()
実行結果
>>> count_b()
実行結果
>>> count_c()
実行結果
>>> count_c()
実行結果
>>> count_c()
実行結果
>>> 

# 4. nonlocal 文の使いどき

nonlocal 文を使いたい時はいつでしょうか? 専用のオブジェクトを作らないで、変数だけで簡単に状態を持たせたいということかなと思っています。

通常、なにか状態を持たせる時はクラスを作ります。

class Counter:
    def __init__(self):
        self._times = 0
    
    def count(self):
        self._times += 1
        return self._times

counter = Counter()
counter.count()
counter.count()
counter.count()
counter.count()
counter.count()
>>> counter.count()
1
>>> counter.count()
2
>>> counter.count()
3
>>> counter.count()
4
>>> counter.count()
5
>>> 

nonlocal を使えば、わざわざクラスを定義する必要もありませんでした。 ただ、この例では、結局書いている行数が変わらないので、分かりにくいです。

PEP の本文の中に、こういう時に便利だよね!という例があります。 Namespace というクラスが不要になったので、コードが短くなります。 ちょっと分かりにくいのですが。

#
# **Python 2** では、こうやって書くしかない
#
class Namespace:
    pass

def make_scoreboard(frame, score=0):
    ns = Namespace()
    ns.score = 0
    label = Label(frame)
    label.pack()
    for i in [-10, -1, 1, 10]:
        def increment(step=i):
            ns.score = ns.score + step
            label['text'] = ns.score
        button = Button(frame, text='%+d' % i, command=increment)
        button.pack()
    return label
#
# こうやって書けたらいいけど...
# -> nonlocal 文を導入しよう
#
def make_scoreboard(frame, score=0):
    label = Label(frame)
    label.pack()
    for i in [-10, -1, 1, 10]:
        def increment(step=i):
            score = score + step  # fails with UnboundLocalError
            label['text'] = score
        button = Button(frame, text='%+d' % i, command=increment)
        button.pack()
    return label

# ◯ クラスと nonlocal の使い分け

メソッドを1つしか持たないクラスの場合かつ、nonlocal 文を使うと短く簡潔に書ける場合は、 nonlocal 文を使ってもいいかなと思います。 ただ、多くの場合はクラス定義文を使った方が望ましいかなと感じています。

まず第一に、オブジェクトが状態を持っていると分かりやすいからです。 オブジェクトからメソッドを呼び出した場合、そのメソッドは何かしらの副作用を有しているかもしれないと考えることができます。 関数だけで副作用を有しているというのは正直、ちょっと怖い気がします。

また第二に nonlocal 文はあまり見かけません。 例えば GitHub で Django と Flask のソースコードに nonlocal で検索をしたのですが、 あまり使われている箇所は多くはありません。

# 5. 2つの名前の探し方

問題

以下のスクリプトを実行すると 01 どちらが表示されるでしょうか?

x = 0

def f():
    print(x)

def g():
    x = 1
    f()

g()  # 実行結果1

実行結果1に最も近いものは次のうち、どれですか?

  • 0
  • 1
  • 例外 NameError が raise される。

# 5.1. 静的スコープ

 変数を   定義   した箇所から外側に向けて広がるスコープ  を静的スコープと呼ばれているのを目にします。 Python は静的スコープを採用しています。

そんな言葉もあるんだなくらいに抑えておいていただければなと思います。 関数 f は呼び出されると、自分が定義されたスコープから外側へと変数を探しに行きます。

# 5.2. 動的スコープ

 変数を   参照   した箇所から外側に向けて広がるスコープ  を動的コープと呼ばれているのを目にします。 Python は動的スコープは採用していません。

もし Python がレキシカルスコープであったなら、 呼び出された箇所の外側のスコープを探し 1 が表示されます。

ダイナミックスコープは危険らしいです。 関数が呼び出される場所によって、 参照するスコープが変化してしまうのは、 可読性はちょっと辛そうな気はします。あまりちゃんと理解できていません。

ダイナミックスコープは危険で、最近の言語ではほとんど採用されていない。
dynamic scoping can be dangerous and few modern languages use it.
Scope - Wikipedia

# ◯ 参考文献

以下の記事がとても参考になります。 レキシカルスコープとは静的スコープのことです。 英語圏の文献ではレキシカルスコープ (lexical scope) の表記を割と多く見かけます。

# 6. クラスのスコープ

クラスにもスコープがあります。

class A:
    a = 1
    print(a)
    # 1

print(a)
# NameError

クラスのスコープは、すこし独特な動作をします。 この動作については以下の記事から知ることができました。ありがとうございます。

# 6.1. クラスの nested スコープ

スコープの基本的な考え方は  中から外は見ることができる  でした。 普通はローカルスコープで変数が定義されていなかった時、 1つ外側のスコープの名前が参照されます。

例えば、関数定義時は、ローカル変数が定義されていなかった場合、 1つ外側の名前空間を参照されるようになっています。

# 普通は...
x = 0
def g():
    x = 1  # <-- こっちが参照される
    def f():
        return x  # 1

    return f

f = g()
f()

しかし、クラススコープは、たとえ nested スコープがあったとしても、見ることができません。 直接 global スコープが参照されてしまいます。

x = 0
class A:
    x = 1
    class B:
        print(x)
... 
0
>>> 

メソッドを定義する時は、1つ外側の名前空間は、何もしないと参照されません。 もしクラス変数を参照したければ、 self.x と書かないといけません。

PEP 227 によると、ローカル変数とクラス変数(文中ではクラス属性)を明確に区別するために、 このような例外を設けたようです。

このルールは、クラス属性への参照とローカル変数への参照の間における奇妙な相互作用を防ぐためです。
This rule prevents odd interactions between class attributes and local variable access.
Statically Nested Scopes - PEP 227

もちろん、クラス定義内であれば、名前空間を明示しなくても使えます。 むしろ明示するとエラーになります。 なぜエラーになるかというと名前 C に クラスオブジェクトが束縛されていないからです。

class C:
    x = 0
    print(x)  # 0

    y = 1
    print(C.y)  # NameError
...
0
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 6, in C
AttributeError: class C has no attribute 'y'
>>>
答え

実行結果2はどれですか?

x = 0
class C:
    x = 1
    def f(self):
        print(x)

obj = C()
obj.f()
>>> obj.f()
実行結果2
>>>
問題

実行結果3を答えてください。

x = 0
class C:
    x = 1
    def f(self):
        print(self.x)

obj = C()
obj.f()
>>> obj.f()
実行結果3
>>>

# 6.2. 3 つの例外

Python の名前解決は、そのほかにも2つの例外があります。

議論
... 名前解決のルールは、静的スコープの言語に典型的なものです。 ただし、3つの例外があります:

  • クラススコープの名前は、参照できません。
  • global 文は通常のルールをショートカットします。
  • 変数は宣言されません。

Discussion
... The name resolution rules are typical for statically scoped languages, with three primary exceptions:

  • Names in class scope are not accessible.
  • The global statement short-circuits the normal rules.
  • Variables are not declared.

Statically Nested Scopes - PEP 227

「global 文は通常のルールをショートカットします。」というのは、 global 文が付与された変数は nestedd スコープをショートカッして直接 global スコープを参照することを指しています。

「変数は宣言されません」というのは、 例えばローカル変数を使う場合は、 他のプログラミング言語、例えば JavaScript のように let, const と書かなくていいですよ、 と言っています。

# 7. おわりに

ここまで以下のように見てきました。

以上になります。ありがとうございました。

Last Updated: 2/29/2020, 3:20:12 AM