Using closure in Python(2)

先日のエントリで、Pythonクロージャについて記載しました。問題のコードは以下のようなものでした。

counter1.py:

#!/usr/bin/python -t

def make_counter():
    x = 0
    def counter():
        x += 1
        print x,
        return counter
    return counter

make_counter()()()()

このコードは例外を発します。

./counter1.py
Traceback (most recent call last):
  File "./counter1.py", line 11, in <module>
    make_counter()()()()
  File "./counter1.py", line 6, in counter
    x += 1
UnboundLocalError: local variable 'x' referenced before assignment

これに対し、以下のコードでは例外が起こりません。

#!/usr/bin/python -t

def make_counter():
    x = 0
    def counter():
#         x += 1
        print x,
        return counter
    return counter

make_counter()()()()


どうにも不可解だった上述のコードの問題ですが、どうやらPython Reference Manualの二つの文章で説明出来そうです。

A block is a piece of Python program text that is executed as a unit. The following are blocks: a module, a function body, and a class definition.

Python Reference Manual - 4.1 Naming and binding

A scope defines the visibility of a name within a block. If a local variable is defined in a block, its scope includes that block. If the definition occurs in a function block, the scope extends to any blocks contained within the defining one, unless a contained block introduces a different binding for the name.

Python Reference Manual - 4.1 Naming and binding

和訳されたものも以下に引用します。

ブロック (block) は、Python のプログラムテキストからなる断片で、一つの実行単位となるものです。モジュール、関数本体、そしてクラス定義はブロックです。

Python リファレンスマニュアル - 4.1 名前づけと束縛 (naming and binding)

スコープ (scope) は、ある名前があるブロック内で参照できるかどうかを決めます。ローカル変数があるブロック内で定義されている場合、変数のスコープはそのブロックを含みます。関数ブロック内で名前の定義を行った場合、その名前に対して別の束縛を行っているブロックを除いた、関数内の全てのブロックを含むようにスコープが拡張されます。

Python リファレンスマニュアル - 4.1 名前づけと束縛 (naming and binding)


二つ目に引用した文章に関しては、英語のもののほうが理解しやすいよう感じます。そこで、英語のものを元に自分なりの理解を加え、箇条書きにしてみました。

  • Pythonにはブロックという概念がある。モジュール、関数ボディ、クラス定義は(代表的な)ブロックである。
  • ブロック内の任意の場所からローカル変数が参照出来るか否かは、スコープによって決定される。
  • ブロック内で宣言されたローカル変数のスコープは、そのブロック全体に適用される。
  • 関数ブロックにて定義されたローカル変数は、内包するブロック群内で同じ名前のバインディング(拘束)が行われない限り、内包するブロック群からも参照することが出来る。原文ではこれを、スコープを拡張すると表現している。

ローカル変数のスコープに関して記述しているのにも関わらず、同じ名前のバインディング(a different binding for the name)という表現が用いられています。これは、例えば以下のコードのように、あるブロックに内包されているブロック内で、同名の関数が定義されていた場合等も含んでいるのでしょう。

def make_counter():
    x = 0
    def counter():
        def x():
            pass
        print x,
        return counter
    return counter

さて、これを元に先に示した二つのコードにおけるローカル変数xのスコープを考えてみます。


一つ目はエラー終了するコードです。make_counter関数にてローカル変数xが宣言されています。

ローカル変数xのスコープはブロック全体に適用されます。しかしながら、make_counter関数が内包しているブロック、counter関数では同じ名前を使ったローカル変数xが宣言されているため、make_counter関数にバインドされているローカル変数xのスコープの対象から外れます。その結果、以下の*1で表している部分がmake_counter関数にバインドされたローカル変数xのスコープとなります。

def make_counter(): ....... (*1)
   x = 0 .................. (*1)
   def counter():
       x += 1
       print x,
       return counter
   return counter ......... (*1)

counter関数でもローカル変数xが宣言されています。ローカル変数のスコープはそのブロック全体に及ぶため、以下の*2で表している部分がcounter関数にバインドされたローカル変数xのスコープとなります。

def make_counter():
   x = 0
   def counter(): ......... (*2)
       x += 1 ............. (*2)
       print x, ........... (*2)
       return counter ..... (*2)
   return counter

そのため、counter関数にバインドされているローカル変数xは未定義であるのにもかかわらず+=演算子によって参照されるので、UnboundLocalErrorが発生します。


次に、エラーが出ないコードを考えます。上記のコードと同様、make_counter関数にてバインドされたローカル変数xのスコープは、*1に表す部分になります。

def make_counter(): ....... (*1)
   x = 0 .................. (*1)
   def counter():
       print x,
       return counter
   return counter ......... (*1)

内包されるブロック、counter関数では同じ名前xが拘束されていないため、make_counter関数にバインドされているローカル変数xのスコープはcounter関数内に拡張されます。これを*1-1で表します。

def make_counter(): ....... (*1)
   x = 0 .................. (*1)
   def counter(): ......... (*1-1)
       print x, ........... (*1-1)
       return counter ..... (*1-1)
   return counter ......... (*1)

結果counter関数の内部からmake_counter関数にて定義しているローカル変数xが参照出来るようになります。


やっとすっきりしました。

まとめ

  • Pythonにはブロックという概念がある
  • モジュール、関数およびクラス定義はブロックである
  • ローカル変数のスコープは、ブロック全体に適用される
  • ブロックが内包しているブロック群がローカル変数のスコープに含まれるか否かは、内包されているブロック群が拘束している名前の如何によって決定される

また、先日のエントリのまとめは全く検討違いのものであることも分かりました。

  • Pythonでのクロージャは、シャローコピーされた値である(ようだ?)
    • これは誤り。クロージャ外側のブロックにて宣言されたローカル変数のスコープが内包されるブロックに拡張されたため、参照出来たに過ぎない*1
  • Pythonでのクロージャは、変更出来ない(ようだ?)
    • これは誤り。クロージャに内包されたブロック内で同名のローカル変数が宣言されているため、クロージャにて宣言されたローカル変数は参照出来ない。クロージャにてローカル変数が宣言されているため、外側のブロックにて宣言されたローカル変数は参照出来ない*2。また、このエラーの本質は、未定義の変数を参照したことによる


今回引用した、Python Reference Manualと和訳されたマニュアルは以下になります。

Python Reference Manual4.1 Naming and binding


最後になりますが、先日のエントリにてid:nishiohirokazuさん、id:elecstaさんから貴重なコメントを頂きました。ありがとうございました。

*1:2008/8/1修正

*2:2008/8/1修正