Last Updated: 2/6/2024, 5:44:57 AM

# クロージャと nonlocal

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

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

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

# 0. 関数の中で定義する関数

関数の中で関数を定義することができます。

def double(x, y):
    def add(x, y):
        return x + y
    
    return 2 * add(x, y)

double(1, 2)
>>> double(1, 2)
6
>>> 

関数の中で関数を定義して、一体何に使うのでしょうか? 長くて読みにくい処理に名前をつけて、短く表現したい時に、関数を使います。

関数の最も基本的な機能は、複数の命令をひとつにまとめて、名前をつけることです。
JavaScript の関数で何ができるのか、もう一度考える - Subterranean Flower Blog (opens new window)

もし上のコードの add が長かったら、 もう少しメリットがわかりやすくなると思うのですが。 役に立ち、かつ短い、良いコードが思いつきませんでした... orz

で、ここで疑問なのが、処理に名前をつけたいだけなら、 外側で定義すればいいんじゃないの?という話です。 しかし、外側で定義されると「あー、ほか関数でも使われるのか」と、 誤解される可能性があります。 double の中でしか使わないのなら double の中で定義した方が良さそうですね。

def double(x, y):
    return 2 * add(x, y)

def add(x, y):
    return x + y

double(1, 2)

内側の関数名が外から見えず、他から使われないことが保証できるので読むとき楽
関数内関数を積極的に使うシチュエーションがわからない・意義がいまいち理解できない - teratail (opens new window)

# 1. クロージャ

自分は長いこと「関数の中で定義する関数をクロージャだ」と思っていました。 が、どうもそうではないようです。 「自由変数を持った関数が、クロージャだ」そうです。

ただ Python には明確な言語仕様が存在しないので、 こうだ!とは言いにくいところがあるので、 読み流していただけると嬉しいです。

# 1.1. 普通の関数

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

x = 0

# 関数 f は自由変数 x を持つ
# クロージャです。
def f():       
    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 (opens new window)

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

ポイント

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

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

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

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

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

h = f()
h()
>>> h()
1
>>> 

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

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

>>> # Python 2.1 以前では...
>>> h()
0
>>> 
ポイント

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

変数がなかった場合、

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

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

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

# 2.1. デコレータ

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

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


def measure(func):
    # 関数 wrapper は自由変数 func を持つ
    # クロージャです。
    def wrapper(*args, **kargs):  
        # 開始時間
        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 (opens new window)

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

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

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

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

# Step 1. カリー化
def splitter(b):
    # 関数 split は自由変数 b を持つ
    # クロージャです。
    def split(a):
        return a // b
    return split

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

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

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

ちなみにカリー化したり部分適用することが面倒だなと思ったら、 標準ライブラリ functools の中にある partial 関数を使うと、 既にある関数を元にカリー化することなく部分適用ができるそうです。

# ◯ 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 文 (opens new window)

# 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 というクラスが不要になったので、コードが短くなります。 ちょっと分かりにくいのですが、PEP の記載されていたコードを引用します。

#
# **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 (opens new window)

# ◯ 参考文献

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

# 6. クラスのスコープ

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

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

print(a)
# NameError

クラススコープの使いどきは property を使うときかなと思います。

#
# attr*2 はクラススコープの attr*1 を参照しています。
#
class Class:
    # 参照
    @property
    def attr(self): # attr*1
        return self._attr
    
    # 代入
    @attr.setter  # attr*2
    def attr(self, value):
        self._attr = value

instance = Class()
instance.attr = 0
print(instance.attr)
>>> print(instance.attr)
0
>>> 

property 自体、あまり目にすることがないので、 クラススコープを意識することはないのかなと思います。 property は、コードを改修する際に代入や参照を全て書き換えるのが面倒な時に、 逃げの一手として使うものだと個人的に思っています。

# 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 (opens new window)

もちろん、クラス定義内であれば、名前空間を明示しなくても使えます。 むしろ明示するとエラーになります。 なぜエラーになるかというと名前 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 (opens new window)

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

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

# 7. おわりに

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

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