# クロージャと nonlocal

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

この記事は、いま書いています
この記事は、いま書いています
この記事は、いま書いています
この記事は、いま書いています
この記事は、いま書いています
この記事は、いま書いています
この記事は、いま書いています
この記事は、いま書いています
この記事は、いま書いています
この記事は、いま書いています
この記事は、いま書いています
この記事は、いま書いています
この記事は、いま書いています
この記事は、いま書いています
この記事は、いま書いています
この記事は、いま書いています
この記事は、いま書いています
この記事は、いま書いています
この記事は、いま書いています
この記事は、いま書いています
この記事は、いま書いています

# 1. クロージャ

いまいる1つ外側のスコープを nested スコープと言います。 簡単に機能をご紹介いたします。

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

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

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

関数 count は1つ外側の nested スコープを参照することができます。 このような関数をクロージャ(関数閉包)と表現されているのを見かけます。

この人すごい人だなと思っていた方でも、 多くの方が「クロージャ」の理解に、思った以上に時間を費やしている気がします。

これは何故でしょうか? 恐らく nested local スコープの理解がないからです。

クロージャは端的に言えば関数とスコープを一緒にしたもの。 利用シーンとしては

ボトムアップよりトップダウンの方が理解しやすいと思うのですが、 思いの外、クロージャについては、意外とボトムアップの方がわかりやすいのではないかなと思いました。

カウント()
カウント()
カウント()
カウント()
>>> カウント()
0
>>> カウント()
1
>>> カウント()
2
>>> カウント()
3
>>>
回数 = -1
def カウント():
    global 回数
    回数 += 1
    return 回数
def カウンターを作る():
    回数 = 0
    def カウント():
        nonlocal 回数
        回数 += 1
        return 回数
    return カウント

いろいろな見方があるけど オブジェクトは、 名前空間とメソッドを足し合わせたもの。

         カウンタ . 数える()
         名前空間 + メソッド()
# ポイント
#   名前空間を作ること
#   -> 根本的にはインスタンス化と同じ
#   -> この用途なら
#   -> 明示できる分、インスタンス化した方が良い
class カウンタ:
    def __init__(self):
        self._回数 = -1

    def 数える(self):
        self._回数 += 1
        return self._回数

# 感じたこと

ざっくり PEP に目を通しているのですが、 実は、スコープの名称に対する定義を見つけられないでいます。

冒頭では関数定義文とクラス定義文の中が local スコープだとお伝えさせていただきました。 しかし実際には、スコープは、いまいる場所からの相対的な位置付けかなと感じます。

  1. local スコープは、いまいる場所のスコープです。
  2. nested スコープは、いまいる場所の1つ外側のスコープです。
  3. global スコープは、いまいるモジュールのスコープです。
  4. built-in スコープは、いまいるインタープリタのスコープです。

そしてスコープを区切るのは、以下の4つかなと感じています。

  1. インタープリタ
  2. モジュール
  3. 関数定義文
  4. クラス定義文

# 2. 2つの名前解決

問題

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

x = 0
def f():
    print(x)

def g():
    x = 1
    f()

g()  # 実行結果1

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

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

# レキシカルスコープ

 変数を   定義   した箇所から外側に向けて広がるスコープ  をレキシカルスコープと言います。 Python はレキシカルスコープを採用しています。

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

# ダイナミックスコープ

 変数を   参照   した箇所から外側に向けて広がるスコープ  をダイナミックスコープと言います。 Python はダイナミックスコープは採用していません。

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

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

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

# 参考文献

以下の記事がとても参考になります。

# 3. nested スコープ

PEP 227 では nested スコープが導入されました。 PEP 227 で nested スコープが導入されたというのは、 PEP 227 以前では local スコープに変数がなければ 直接 global スコープが参照されていました。

# 4. nonlocal 文

PEP 3104 では nonlocal 文が導入されました。

# 5. 名前解決の例外

# 5.1. 動作

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

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

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

    return f

f = g()
f()

しかし、クラススコープは、たとえ外側にあったとしても、見ることができません。 クラスのローカルスコープは、それが許されません。

class A:
    x = 1
    class B:
        print(x)
...
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 3, in A
  File "<stdin>", line 4, in B
NameError: name 'x' is not defined
>>>

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

答え

実行結果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
>>>




# 5.2. PEP 227

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

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

    y = 1
    print(C.y)  # NameError
...     print(C.y)
...
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'
>>>

このような挙動は PEP 227 で議論、言及されたようです。 クラス変数の参照を、例外的に取り扱う旨の記述が見えます。

議論 ... 名前解決のルールは、静的スコープの言語に典型的なものです。 ただし、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

箇条書きの文章は、パッと見てもわかりづらいですが、 PEP 227 の中で、なんで例外的に取り扱ったかも含めて丁寧に説明してくれているようです。 自分はまだ、ちゃんと読めてません。

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

「変数は宣言されません」というのは、 例えばローカル変数を使う場合は、 わざわざ local var なんて書かなくていいですよ、と言っています。

PEP 227 については、この記事から知ることができました。 ハヤタカ先生、ありがとうございます。

# 6. おわりに

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

Last Updated: 11/11/2019, 11:17:29 PM