寒月記

住みにくいところをどれほどか寛容て

Python のクロージャについて: 関数のスコープと、関数が第一級オブジェクトであることからちゃんと考える

※Qiita からこちらにも記事を移しました
こちら に補足記事も書いてます

まだまだ Python を勉強中なのですが、「クロージャ」という概念がパッとは分からなかったので調べました。

「Flask で LINE Bot 作ろう」 ->「デコレータでルーティングしてる、デコレータ理解してないから勉強しよう」 ->「デコレータはクロージャを使った仕組みらしいけどクロージャ分からないから勉強しよう」 という調べものスパイラルのさなかです。

手元の入門書を見たり、Web で検索したりして調べましたが、クロージャの解説は複雑・難解なものや、逆に省略し過ぎなものが多いように感じて困ったので、手元で動きを確認してみました。

また、本記事の執筆にあたって参考にさせていただいたサイト様を参考欄に記載しています、ありがとうございました。

この記事が役に立つであろう方

クロージャについて調べてたけどよくわからん、という方に、何かしらの気づきを共有できるのではないかと思います*1。 なお、クロージャで悩む方はご存知だと思うので、ごく初歩的なスコープ知識は前提としています。

クロージャとは

まずは、クロージャについての現時点での私の理解を述べます。

クロージャ
関数が第一級オブジェクトであるため、変数に関数を格納すると その関数定義自体 と共に その環境 が格納されることを利用する、 関数を状態ごと保持 したオブジェクト

これだけだとやはりわかりにくいと思います。 以下順を追って解説していきますので、とりあえず気にせず読み進めてください。

クロージャ理解のためにその1: Python の関数のスコープの基礎

関数のスコープの基本

クロージャの理解には、(Python の) 関数のスコープに関する知識が必要です。

そこで唐突ですが質問です。 以下の例では、func1() func2() それぞれの実行結果はどうなるでしょうか。

scope = 'global'


def func1():
    print(scope)


def func2():
    scope = 'local'
    func1()


func1()  # これと
func2()  # これの結果は?

scope_sample1.py

func1()globalfunc2()local」と思った方がいらっしゃるのではと思います。 私もそう思ってました。 実際の結果は以下です。

>>> func1()
global
>>> func2()
global

result-scope_sample1.py

この結果に納得がいかなかい方は、少し長いですが是非最後までお付き合いください。私は納得できなかったので調べました。

なぜこんな結果になるのかというと、「 (一部言語の) 関数のスコープでは、関数の実行時ではなく、関数の定義時の環境が参照可能な変数を決定するため」 となっています。 scope_sample.py だと、「func2 の内部で実行されている func1 の環境は、4、5行目で定義された時点で決定済み」であり、「その時点では func1 が参照可能な変数 scopeglobal しかなかった」 ので、func2 の実行結果も global となった、ということです。

では、次の例ではどうでしょうか。

scope = 'global'


def func1():
    scope = 'local1'  # ここでも変数 scopeを定義
    print(scope)


def func2():
    scope = 'local2'
    func1()


func1()  # これと
func2()  # これと
print(scope)  # これの結果は?

scope_sample2.py

scope_sample1.py とほぼ同じですが、今度は func1 の中でも scope を定義しています。 以下結果。

>>> func1()
local1
>>> func2()
local1
>>> print(scope)
global

result-scope_sample2.py

func1 のローカルスコープ = 定義時 のスコープ内に scope 変数があります。 同名変数は近いスコープから優先して解決される ので、この func1 のローカルスコープ内の変数 scopefunc1 の環境内でグローバルの scope変数よりも優先して参照 された結果、 func1func2 の実行結果が local1 となっています。 func2() の実行結果が local1 になるのは、scope_sample1.py で説明した通りの理屈ですね。

また、当然グローバル変数の scope が上書かれたり消えたりしたわけではないので、グローバルスコープで print(scope) を実行すると global となっています。

クロージャ理解のためにその2: 「Python の関数」は第一級オブジェクト

とても大事なことですが、次のセクションからの例を見ると分かりやすいと思うので、簡潔に要点だけ述べます。 Python では、関数は 第一級オブジェクト と呼ばれるオブジェクトに分類されており、このため以下のことができます。

  • 変数に関数を格納
  • 戻り値として変数を返す

この他にも引数として関数を渡せる、など色々な特徴がありますが、とりあえずのクロージャの理解には上の 2つを押さえておけば大丈夫です。 次のセクションで、さっそく実際の例を見ていきます。

クロージャ理解のためにその3: 関数内関数のスコープ

少し特殊な関数について、スコープを見ていきます。 「関数内関数」と呼ばれる、関数がネストされているパターンです。

scope = 'global'


def func1():
    scope = 'local1'
    print("inside of func1: " + scope)

    def func1_1():
        scope = 'local1_1'
        print("inside of func1_1: " + scope)

        def func1_2():
            print("inside of func1_2: " + scope)
        return func1_2
    return func1_1


func1_1 = func1()  # func1() の実行の結果、戻り値として func1_1を受け取っている
func1_2 = func1_1()  # 上の行で func1_1を定義しているので定義可能

print(scope)  # I これと
func1()  # II これと
func1_1()  # III これと
func1_2()  # IV これの結果は?

scope_sample3.py

ここで注目すべきポイントは、return func1_1 のように 「関数が戻り値として返されている」 ことと、func1_1 = func1() のように 「関数が変数に格納されている」 ことです。 これは、その2 で述べた通り、Python の関数が第一級オブジェクトであるからできていることです。

ちょっと複雑そうですが、とは言え難しいことはありません。 先ほどと同様、「関数のスコープは関数定義時の環境で決まる」 「同名変数は近いスコープから優先して解決される」 というルールに忠実に従うだけです。 結果を見てみます。

>>> print(scope)
global  # I の結果
>>> func1()
inside of func1: local1  # II の結果
<function func1.<locals>.func1_1 at 0x00000163058C9620>
# ↑ returnされた func1_1のオブジェクト
>>> func1_1()
inside of func1_1: local1_1  # III の結果
<function func1.<locals>.func1_1.<locals>.func1_2 at 0x00000163058C96A8>
# ↑ returnされた func1_2のオブジェクト
>>> func1_2()
inside of func1_2: local1_1  # IV の結果

results-scope_sample3.py

注目すべきポイントは、func1_2 の実行時に、ローカルfunc1_1scope 変数を参照している点です。 しかし、結局 scope_sample1.pyfunc1 がグローバルの scope 変数を参照できていたのと原理的には同じですね。 関数内関数については、

  1. 自分のローカルスコープ
  2. その一つ外側の関数のスコープ
  3. (あれば) その一つ外側の関数の...... (×n)
  4. グローバルのスコープ

という順で、「近いスコープから優先して参照」 のルールに従っているだけです。

ここまで読んで理解いただけている場合、特に難しいことはないと思います。 本当にルールに忠実に従ってるだけですね。

クロージャについて

いよいよ本丸です。 冒頭で述べた定義をもう一度振り返ってみます。

関数が第一級オブジェクトであるため、変数に関数を格納すると その関数定義自体 と共に その環境 が格納されることを利用する、 関数を状態ごと保持 したオブジェクト

ここまでの説明で、前半部分は何となくはわかるのではないかなと思います。 あとは、クロージャの特徴である 関数を状態ごと保持 の部分を中心に解説していきます。 今ピンと来なくても具体例を見るとわかるかもしれないので、少し進めてみましょう。

クロージャの体験: カウンターを作ってみる

クロージャの実例として、よく「カウンターの実装」が使われます。 以下の例のようなものです (リスト型を使っている理由は後で説明します)。

def createCounter():
    cnt = [0]
    print("running createCounter")

    def inner():
        cnt[0] += 1  # cnt[0] = cnt[0] + 1
        print(cnt)
        print("running inner")
    return inner


counter = createCounter()  # ちなみにここで "running createCounter" が printされる

closure_sample1.py

createCounter 関数で cnt というリスト型の変数を定義し、それを関数内関数 inner の中でインクリメントしています。

最終行では、変数 countercreateCounter の実行結果、すなわち戻り値として返される関数オブジェクト inner を格納しています。

ここで質問です。 counterinner を格納しているわけですが、では counter を実行するとどうなるでしょうか。


・・・・・・・・・・・・

結果は以下です。

>>> counter()
[1]
running inner

result-counter

そりゃそうだ、という感じです。 では、もう一度 counter() を実行するとどうなるでしょうか。


・・・・・・・・・・・・

ここが大事なポイントで、なんと counter() をもう一度実行すると今度は [2] となります。 変数 cnt はグローバル変数でもないのに、前回の実行結果がちゃんと保持されていて、更にインクリメントされていることになります。 もっと試してみましょう。

>>> counter
<function createCounter.<locals>.inner at 0x000001F1D3CA9510>
>>> counter()
[2]
running inner
>>> counter()
[3]
running inner
>>> counter()
[4]
running inner
>>> counter
<function createCounter.<locals>.inner at 0x000001F1D3CA9510>
>>> counter
<function createCounter.<locals>.inner at 0x000001F1D3CA9510>

result-repeat_counter

実行した分だけ、どんどん増えています。 ちなみに、counter() のように関数オブジェクトの末尾に () を付けると「関数実行の命令」を意味し、counter のように () 無しだとオブジェクトそのものを指します。

上の実行例では counter オブジェクト自身も何度か確認していますが、at 以後の値を見るとわかる通り、counter (= inner) オブジェクトは counter の実行前後や回数に関わらず、同一メモリアドレスにある同じものを指していることが分かります。オブジェクトが破棄などされず生存し続けている、ということですね。

これがクロージャの利用例です。 くどいですが、冒頭の定義を再掲します。

関数が第一級オブジェクトであるため、変数に関数を格納すると その関数定義自体 と共に その環境 が格納されることを利用する、関数を状態ごと保持 したオブジェクト

closure_sample1.py では、関数内関数である inner の定義時の環境は、

  1. inner 自身のローカルスコープ
  2. 外側の関数である createCounter のローカルスコープ
  3. グローバルスコープ

となります。 inner のローカルスコープ内で変数 cnt を定義していないので、cnt[0] += 1 で参照される cnt は、createCounter で定義した cnt = [0] となります。

そして、この inner を変数 counter に格納しており、この counter はこの例だとグローバル変数なので破棄はされません。 生み出されてから生存し続けている counter は、関数 inner をその 環境・状態ごと 格納しているので、その状態に対して行った更新も保持され続ける、ということになります。

では、counter = createCounter() のように変数に格納せず、直接 inner の実行を繰り返すとどうなるでしょうか。

>>> createCounter()()  # createCounter() で innerが返るので、
[1]                    # もう一つ () を付けて inner() を表している
>>> createCounter()()
[1]
>>> createCounter()()
[1]
>>> createCounter()()
[1]
>>> createCounter()
<function createCounter.<locals>.inner at 0x000001F1D3CA9620>
>>> createCounter()
<function createCounter.<locals>.inner at 0x000001F1D3CA9510>
>>> createCounter()
<function createCounter.<locals>.inner at 0x000001F1D3CA9620>
>>> createCounter()
<function createCounter.<locals>.inner at 0x000001F1D3CA9510>

今度はインクリメントしてませんね。 これは先ほどとは異なり、inner を変数に格納していない = メモリ領域を割り当てていないため、実行の都度 inner が生成 -> 破棄 されているためです。 この例では inner オブジェクト (createCounter()) は、オブジェクト参照の都度異なるメモリ領域が割り当てられていることが分かります (実際には 2箇所で生成・破棄を繰り返しているようです)。

もっとわかりやすく、実用性を実感できるように、もう一つ実行例を書いておきます。

def createCounter():
    cnt = [0]
    print("running createCounter")

    def inner():
        cnt[0] += 1  # cnt[0] = cnt[0] + 1
        print(cnt)
        print("running inner")
    return inner


counter1 = createCounter()  # 変数を二つ宣言
counter2 = createCounter()

closure_sample2.py

>>> counter1()  # ここでは counter1を実行
[1]
running inner
>>> counter1()
[2]
running inner
>>> counter2()  # ここから counter2を実行
[1]
running inner
>>> counter2()
[2]
running inner

result-repeat_counter

counter1counter2 それぞれに関数 inner を格納していますが、counter1counter2 は別の変数 = メモリ領域も別なので、それぞれに実行結果 ≒ cnt の値 ≒ オブジェクトの状態が保持されている ことが分かります。 こんな風に、「同じ処理を繰り返し、処理の結果更新される状態を保持したい」「変数ごとの異なる状態を保持したい」という場合に使えますね。 クロージャを使えば、global 空間を汚染せずに (= 変数の名前空間を分離させて) 、処理に応じて変数の値を変更 できます。

また、力尽きたのでここでは例までは書いてませんが、関数に渡す「引数」も受け取った関数のローカル変数のように振舞うので、引数とクロージャを組み合わせたりしても便利です。

補足

ここまででクロージャの説明は一通り終わりですが、以下からは皆さんが気になっているであろう点について補足をしておきます。

補足1: Python のスコープの不思議

closure_sample1.py などで、なぜ cnt に数値型でなく、わざわざリスト型を使ったのかをここで説明します。 私もはじめは参考サイトの JavaScript の例を見て、素直に数値型でサンプルを作って実行してみました。

def createCounter():
    cnt = 0

    def inner():
        cnt += 1  # cnt = cnt + 1
        print(cnt)
    return inner


counter = createCounter()

closure_sample3.py

自然に見えます。 何も疑問を持たず実行すると、以下の結果となりました。

>>> counter()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 5, in inner
UnboundLocalError: local variable 'cnt' referenced before assignment
>>>

result-closure_sample3.py

どうやら、行5でローカル変数 cnt が宣言前に参照されている、というエラーのようです。 じゃあ宣言しよう、と以下のスクリプトにして実行しました。

def createCounter():
    # cnt = 0

    def inner():
        cnt = 0  # inner() が cntを参照できるようにこっちで宣言
        cnt += 1  # cnt = cnt + 1
        print(cnt)
    return inner


counter = createCounter()

closure_sample4.py

結果。

>>> counter()
1
>>> counter()
1
>>> counter()
1

result-closure_sample4.py

...... counter() の実行 = inner の実行時に、inner のローカルスコープで cnt に 0を代入しているせいで、実行の度に cnt が 0に初期化されてしまいます*2

困っていろいろと調べたところ、「Python では JavaScript と異なり、外の関数のスコープの変数には、参照はできても代入はできない」という仕様であることがわかりました。 ややこしい、JavaScript ではできてるのに。

この仕様の回避方法の一つが、closure_sample1.py などで使っていた、「更新したい外側の変数をミュータブルな型で定義し、オブジェクトへの代入を行う (例ではリスト)」という方法です。 ローカルスコープ外変数への代入はエラー となるけど、ローカルスコープ外のミュータブルオブジェクトの書き換えは可能 なので、このようにクロージャが機能します。

もう一つ、こちらは Python3 でのみ利用可能 な方法ですが、nonlocal というキーワードを使うことで、「この変数はローカルに属する変数ではなく、一つ外側のスコープに属する変数 ですよ」と宣言することができます。

def createCounter():
    cnt = 0

    def inner():
        nonlocal cnt  # ここで変数 cntは nonlocalであると宣言
        cnt += 1
        print(cnt)
    return inner


counter = createCounter()

closure_sample5.py

実行結果です。

>>> counter()
1
>>> counter()
2
>>> counter()
3

result-closure_sample5.py

今度は cnt はイミュータブルな数値型ですが、ちゃんとインクリメントされてますね。 Python2 にはなかった nonlocal というキーワードが Python3 で追加されたということは、やはり要望が多かったんでしょうか。

ということで、クロージャを作るときには更新したい変数のデータ型にもご注意ください。

補足2: 類似機能であるインスタンス変数との使い分け

ここまでの説明や例を見て、「クラス」や「インスタンス」を知っている方からすると、「インスタンス変数と同じじゃないか?」という疑問が湧くと思います。

はい、機能的にはほぼその通りです。 これまでのカウンターの例は、以下のようにインスタンス変数でも同様に実現できます。

class Counter:
    def __init__(self, cnt):
        self.cnt = cnt  # インスタンス変数なのでインスタンスごとに値を持つ

    def incrementCount(self):
        self.cnt += 1
        print(self.cnt)

counter_class_sample.py

実行してみましょう。

>>> counter1 = Counter(0)  # インスタンス1作成
>>> counter2 = Counter(0)  # インスタンス2作成
>>>
>>> counter1.incrementCount()
1
>>> counter1.incrementCount()
2
>>> counter2.incrementCount()
1
>>> counter2.incrementCount()
2

result-counter_class_sample

これまでクロージャで説明してきたものと同じ結果が得られてますね。 このように、クラス、インスタンス変数を作ることでも同じ機能は実現はできます。 ただ、クラスを作るほどでもない簡易なものであれば、無用に新たなクラスという登場人物を増やすよりも、クロージャで十分なこともあるのではないでしょうか。

まとめ

当初の構想よりずいぶん長くなってしまいました。 参考サイトも記載していますので、そちらもぜひご確認いただければと思います。

繰り返しになりますが、もう一度私の理解するところのクロージャの定義を記載します。

クロージャ
関数が第一級オブジェクトであるため、変数に関数を格納すると その関数定義自体 と共に その環境 が格納されることを利用する、 関数を状態ごと保持 したオブジェクト

ここまで読んでいただけた方は、こちらの定義が伝わるのではないかと思います。 言葉にしてみると複雑そうですが、大事なことは以下の二つです。

  1. Python の関数は第一級オブジェクト
  2. Python の関数オブジェクトは、変数に格納した際に環境ごと格納される

結局「クロージャ」という名前がついていても、それは上の二つ (をはじめとする) 基本原則に忠実に従った結果の振る舞いを指しているだけです。 関数の振る舞いを理解すれば、クロージャというものがわけのわからない複雑なものではない、ということに気づけるのではないかなと思います*3

長文となりましたが、お付き合いいただきありがとうございました。 次はもう一段本来の目的に戻って、デコレータも調べてまとめたいです。

参考

  • http://analogic.jp/closure/
    取り上げている言語は JavaScriptですが、とても分かりやすく実例を踏まえて解説してくださっています。サンプルコードなども参考にさせていただきました。

コーディングを支える技術 ~成り立ちから学ぶプログラミング作法 (WEB+DB PRESS plus)

コーディングを支える技術 ~成り立ちから学ぶプログラミング作法 (WEB+DB PRESS plus)

*1:強い方には、全編通して誤った理解などのご指摘を頂けるととても嬉しいです。

*2:正確には数値型はイミュータブルなので、同じ変数が初期化されているのではなく、都度新しい cnt を作ってそれを参照している、という感じのようです。

*3:でも関数の振る舞いなんて調べるまでまともに知らなかったので、ほんとわけわからなかったです。初心者殺しだと思います。