iT邦幫忙

2018 iT 邦幫忙鐵人賽
DAY 28
2
Data Technology

30天python雜談系列 第 28

decorator與closure雜談之二———偷渡專家closure

python decorator與closure雜談之二

昨天稍微簡介了decorator的用法並指明了function在python中其實也是一個對象,今天來完整揭示究竟closure是一個什麼樣的東西,但在這之前我們先要搞懂一些先備知識。

首先就是python的作用域,雖然python的作用域有非常多的細節可以探討,但大抵可以區分為global和local變數,若再更詳細區分,那就是LEGB了(其中L是local,E是enclosing,G是global,B是build-in)。

先來看L和G,在python的世界中,只有class和function中才會有local變數的存在,也就是說if判斷式和for迴圈中,變數還是會去引用外層定義的變數,另外python對於作用域和作用域鏈是有嚴格的位置綁定的,這是什麼意思呢?

string = 'global string'

def func1():
    string = 'local string'
    print(string)

func1() # 輸出為local string
print(string) # 輸出為global string

在上面的範例中,func1()的輸出應該是不用懷疑的,但我們的重點是最後一行的print(string),這裏面的string並不會因為前面有呼叫func1()而讓原本的'global string'被改成'local string',func1裏面的local變數不會因為在外層被呼叫而使得作用域會因此洩漏到外層,他仍"被綁定在原本的程式碼位置中",所以作用域仍再func1內部才會生效,而同時func1內的變數也不會被外層的變數汙染。

def func1():
    string = 'local string'
    print(string)

def func2():
    string = 'local string2'
    func1() # 輸出為local string
    print(string) # 輸出為global string2

func2()

若讓func1在func2被呼叫仍然不會去汙染func2的變數,而func1內的變數也不會被func2的變數汙染。

string = 'global string'

def func3():
    print(string)

def func4():
    string = 'local string4'
    func3() # 輸出為global string

    def func5():
        print(string)

    func5() # 輸出為local string4

func4()

對於作用域鏈也是一樣的,所謂作用域鏈就是當自己所要用的變數在當下的作用域內並不存在時,會逐步的往外層尋找,而函式也不會因為呼叫的位置而讓其作用域鏈被改變,比如說,func3的外層就是全局作用域,並不會因為被呼叫在func4而讓外層變成了func4,從輸出可以證明這件事。

另外B就是python的built-in變量,通常python搜尋變數的順序是L->E->G->B,B是在排序中最後一個,他是python中最外層的作用域,裏面包含的是一些python預先定義好的function或是class,比如說range()、float()、print()、str()...等等,若要查閱所有built-in內容,可以試著輸出__builtins__.__dict__。

接下來的E就和我們所要介紹的closure有些關係了,若現在有一個巢狀函式,最外層的函式把其內層定義的嵌套函式作為回傳直傳遞出去,就會形成一個closure,比方說下面這個例子:

string = 'in global'

def outer_func():
    string = 'in outer_func'
    def inner_func():
        print(string)
    
    return inner_func

func_var=outer_func()
func_var() # 輸出為'in outer_func'

因為outer_func的回傳值是inner_func,所以基本上是func_var執行inner_func裏面的指令,重點是inner_func裏面的print(string)的輸出值究竟為何?

從inner_func的位置我們可以很輕易的推得string的值應該為'in outer_func'而不是'in global',事實上也是這樣沒錯拉XD,但我們應該好奇的是,當outer_func返回一個回傳值後,照理來說outer_func裏面的string的生命周期應該結束了才對,怎麼感覺好像string頑強的寄生在func_var裏面的樣子。這就好像outer_func的回傳值應該是:

string = 'in outer_func'
def inner_func():
    print(string)

而不是:

def inner_func():
    print(string)

而這正是閉包的作用,有人說閉包像是一個保護膜,保護函式內的變數不會被殺掉,但我倒認為閉包像是一個偷渡專家,趁著return的時候把函式裏面的變數都偷渡出境,讓他們不被殺掉,而所謂的作用域E,裏面存的就是這些被偷渡出去的變數。

若以比較專業的說法,閉包相當於是儲存當下函式的執行環境,只要是被inner_func用到的對象,閉包都會在inner_func被回傳時把那些對象(就算是class對象還是function對象也一樣)儲存起來,若要查閱閉包儲存了多少對象,可以試著print出__closure__:

In example.py:

def outer_func():
    string = 'in outer_func'
    inner_int = 12345

    class inner_class():
        def __init__(self):
            self.str = 'inner_class'

    def another_inner_func():
        print('another_inner_func')

    def inner_func():
        print(string)
        another_inner_func()

        class_var=inner_class()
        print(class_var.str)
    
    return inner_func

func_var = outer_func()
print(func_var.__closure__)

In bash:
$ python3 example.py
in outer_func
another_inner_func
inner_class
(<cell at 0x7f0d88b082e8: function object at 0x7f0d88b9dd08>, <cell at 0x7f0d88b082b8: type object at 0x1c723b8>, <cell at 0x7f0d88b083a8: str object at 0x7f0d88b07d70>) # 閉包把string, function和class都儲存起來了(type object所指的是inner_class),但inner_int因為沒有被inner_func使用到,所以就只能被殺掉了QQ!

closure的基本概念大概就是這樣了,作為回傳值的inner_func就像是一艘船,而閉包扮演的就是保護與偷渡的角色,其實說難理解也不會太難,明天就開始來更進一步講解decorator的概念與用法。


上一篇
decorator與closure雜談之一———初探decorator與函數對象
下一篇
decorator與closure雜談之三———真‧decorator介紹
系列文
30天python雜談30

尚未有邦友留言

立即登入留言