Saturday, October 24, 2009

python closure的一個gotcha

下面的代码,想做一个counter 的 maker:
def counter(base,step):
        y=base
        def _inc():
                y=y+step
                return y
        return _inc

c2_5=counter(2,5)
print c2_5()
print c2_5()

結果,可恥地出現鳥:
UnboundLocalError: local variable 'y' referenced before assignment

這裡給出了一個例子,猛擊
原因是,在_inc的scope裏面,對y的"賦值"會先創建一個變量"y",因為在這個函數的作用域里y并不存在。
但是右邊的表達式卻引用了y,此時y還沒被賦值,因此出錯了……

自然地會想到用一個trick來fix,在引用y前,先利用變量名搜尋規則,把外層的y引用并存到一個臨時變量里不就可以了?
把_inc改為:
...
        def _inc():
                tmp=y  # 這裡引用了y,沒有對它賦值
                tmp=tmp+step
                return tmp
        return _inc
...
這樣自然就OK了吧?但是,程序運行的輸出不對……囧

因此,這樣寫也是有問題的,因為外層的變量並沒有被update……

真正的解決方法是list,如下:
                x=[base]
        def _inc():
                x[0]=x[0]+step
                return x[0]
        return _inc

有不用list能直接update到它的方法嗎?好像沒有……
因為不通過名字引用似乎不能修改到變量內容……而一賦值就會因為創建變量覆蓋了外層的變量名,從而發生未初始化錯誤……
用函數等得到的又是一個copy,而不是真正修改了變量內容,

不過要注意,在生成的每個閉包里,有自己的局部变量。
正如上述例子中,返回的每個_inc()有自己的x,因此執行下列的代碼,它們不會互相影響:
c2_5=counter(2,5)
c3_6=counter(3,6)
print c2_5()
print c3_6()
print c2_5()
print c3_6()

這正是閉包強大的地方:闭包是由函数及其相关的引用环境组合而成的实体.
闭包只是在形式和表现上像函数,但实际上不是函数。函数是一些可执行的代码,这些代码在函数被定义后就确定了,不会在执行时发生变化,所以一个函数只有一 个实例。闭包在运行时可以有多个实例,不同的引用环境和相同的函数组合可以产生不同的实例。所谓引用环境是指在程序执行中的某个点所有处于活跃状态的约束所组成的集合。其中的约束是指一个变量的名字和其所代表的对象之间的联系。

上面例子中的x就可以看为引用环境的一部分。在不同实例里,这个变量名绑定到具体实例对象,互不干扰。

1 comment: