[Python小記] 通俗的理解閉包 閉包能幫我們做什麼?

chaseSpace-L發表於2018-05-27

熱身:

首先給出閉包函式的必要條件:

  • 閉包函式必須返回一個函式物件
  • 閉包函式返回的那個函式必須引用外部變數(一般不能是全域性變數),而返回的那個函式內部不一定要return

幾個典型的閉包例子:

# ENV>>> Python 3.6
    # NO.1
    def line_conf(a, b):
        def line(x):
            return a * x + b
        return line
    
    # NO.2
    def line_conf():
        a = 1
        b = 2

        def line(x):
            print(a * x + b)
        return line

    # NO.3
    def _line_(a,b):
        def line_c(c):
            def line(x):
                return a*(x**2)+b*x+c
            return line
        return line_c

正文:

    通過前面的例子相信你能夠初步理解閉包的三個必要條件了。一臉懵逼?沒關係,下面從python中函式的作用域開始講起,一步步地的理解閉包。

    一、函式中的作用域

           Python中函式的作用域由def關鍵字界定,函式內的程式碼訪問變數的方式是從其所在層級由內向外的,如“熱身”中的第一段程式碼:

    def line_conf(a, b):
        def line(x):
            return a * x + b
        return line

           巢狀函式line中的程式碼訪問了a和b變數,line本身函式體內並不存在這兩個變數,所以會逐級向外查詢,往上走一層就找到了來自主函式line_conf傳遞的a, b。若往外直至全域性作用域都查詢不到的話程式碼會拋異常。

            注意:不管主函式line_conf下面巢狀了多少個函式,這些函式都在其作用域內,都可以在line_conf作用域內被呼叫。

            思考上面這段程式碼實現了什麼功能?

    #定義兩條直線
    line_A = line_conf(2, 1) #y=2x+b
    line_B = line_conf(3, 2) #y=3x+2
    
    #列印x對應y的值
    print(line_A(1)) #3
    print(line_B(1)) #5

            是否感覺“哎喲,有點意思~”,更有意思的在後面呢。

            現在不使用閉包,看看需要多少行程式碼實現這個功能:

    def line_A(x):
        return 2*x+1
    def line_B(x):
        return 3*x+2
    
    print(line_A(1)) #3
    print(line_B(1)) #5

            不包括print語句的程式碼是4行,閉包寫法是6行,看起來有點不對勁啊?怎麼閉包實現需要的程式碼量還多呢?別急,我現在有個需求:

            再定義100條直線!

            那麼現在誰的程式碼量更少呢?很明顯這個是可以簡單計算出來的,採用閉包的方式新增一條直線只需要加一行程式碼,而普通做法需要添兩行程式碼,定義100條直線兩種做法的程式碼量差為:100+6 -(100*2+4) = -98。需要注意的是,實際環境中定義的單個函式的程式碼量多達幾十上百行,這時候閉包的作用就顯現出來了,沒錯,大大提高了程式碼的可複用性!

            注意:閉包函式引用的外部變數不一定就是其父函式的引數,也可以是父函式作用域內的任意變數,如“熱身”中的第二段程式碼:

    def line_conf():
        a = 1
        b = 2

        def line(x):
            print(a * x + b)
        return line

    二、如何顯式地檢視“閉包”

            接上面的程式碼塊:

    L = line_conf()
    print(line_conf().__closure__) #(<cell at 0x05BE3530: int object at 0x1DA2D1D0>,
    # <cell at 0x05C4DDD0: int object at 0x1DA2D1E0>)
    for i in line_conf().__closure__: #列印引用的外部變數值
        print(i.cell_contents) #1  ; #2

            __closure__屬性返回的是一個元組物件,包含了閉包引用的外部變數。

          ·  若主函式內的閉包不引用外部變數,就不存在閉包,主函式的_closure__屬性永遠為None:

    def line_conf():
        a = 1
        b = 2
        def line(x):
            print(x+1)  #<<<------
        return line
    L = line_conf()
    print(line_conf().__closure__) # None
    for i in line_conf().__closure__: #丟擲異常
        print(i.cell_contents)

           ·  若主函式沒有return子函式,就不存在閉包,主函式不存在_closure__屬性:

    def line_conf():
        a = 1
        b = 2
        def line(x):
            print(a*x+b)
        return a+b #<<<------
    L = line_conf()
    print(line_conf().__closure__) # 丟擲異常    

 

    三、為何叫閉包?

            先看程式碼:

    def line_conf(a):
        b=1
        def line(x):
            return a * x + b
        return line

    line_A = line_conf(2)
    b=20
    print(line_A(1))  # 3

              如你所見,line_A物件作為line_conf返回的閉包物件,它引用了line_conf下的變數b=1,在print時,全域性作用域下定義了新的b變數指向20,最終結果仍然引用的line_conf內的b。這是因為,閉包作為物件被返回時,它的引用變數就已經確定(已經儲存在它的__closure__屬性中),不會再被修改。

               是的,閉包在被返回時,它的所有變數就已經固定,形成了一個封閉的物件,這個物件包含了其引用的所有外部、內部變數和表示式。當然,閉包的引數例外。

    四、閉包可以儲存執行環境

               思考下面的程式碼會輸出什麼?

    _list = []
    for i in range(3):
        def func(a):
            return i+a
        _list.append(func)
    for f in _list:
        print(f(1))

                1 , 2,  3嗎?如果不是又該是什麼呢?    結果是3, 3, 3 。
              因為,在Python中,迴圈體內定義的函式是無法儲存迴圈執行過程中的不停變化的外部變數的,即普通函式無法儲存執行環境!想要讓上面的程式碼輸出1, 2, 3並不難,“術業有專攻”,這種事情該讓閉包來:

    _list = []
    for i in range(3):
        def func(i):
            def f_closure(a):  # <<<---
                return i + a
            return f_closure
        _list.append(func(i))  # <<<---
        
    for f in _list:
        print(f(1))

    五、閉包的實際應用

                現在你已經逐漸領悟“閉包”了,趁熱打鐵,再來一個小例子:

    def who(name):
        def do(what):
            print(name, 'say:', what)

        return do

    lucy = who('lucy')
    john = who('john')

    lucy('i want drink!')
    lucy('i want eat !')
    lucy('i want play !')
    
    john('i want play basketball')
    john('i want to sleep with U,do U?')

    lucy("you just like a fool, but i got you!")

                看到這裡,你也可以試著自己寫出一個簡單的閉包函式。

                OK,現在來看一個真正在實際環境中會用到的案例:

                1、【閉包實現快速給不同專案記錄日誌】

    import logging
    def log_header(logger_name):
        logging.basicConfig(level=logging.DEBUG, format='%(asctime)s [%(name)s] %(levelname)s  %(message)s',
                            datefmt='%Y-%m-%d %H:%M:%S')
        logger = logging.getLogger(logger_name)

        def _logging(something,level):
            if level == 'debug':
                logger.debug(something)
            elif level == 'warning':
                logger.warning(something)
            elif level == 'error':
                logger.error(something)
            else:
                raise Exception("I dont know what you want to do?" )
        return _logging

    project_1_logging = log_header('project_1')

    project_2_logging = log_header('project_2')

    def project_1():

        #do something
        project_1_logging('this is a debug info','debug')
        #do something
        project_1_logging('this is a warning info','warning')
        # do something
        project_1_logging('this is a error info','error')

    def project_2():

        # do something
        project_2_logging('this is a debug info','debug')
        # do something
        project_2_logging('this is a warning info','warning')
        # do something
        project_2_logging('this is a critical info','error')

    project_1()
    project_2()
#輸出
2018-05-26 22:56:23 [project_1] DEBUG  this is a debug info
2018-05-26 22:56:23 [project_1] WARNING  this is a warning info
2018-05-26 22:56:23 [project_1] ERROR  this is a error info
2018-05-26 22:56:23 [project_2] DEBUG  this is a debug info
2018-05-26 22:56:23 [project_2] WARNING  this is a warning info
2018-05-26 22:56:23 [project_2] ERROR  this is a critical info

                這段程式碼實現了給不同專案logging的功能,只需在你想要logging的位置新增一行程式碼即可。

擴充套件: python中的裝飾器特性就是利用閉包實現的,只不過用了@作為語法糖,使寫法更簡潔。如果掌握了閉包,接下來就去看一下裝飾器,也會很快掌握的。

 

宣告:本文章為個人對技術的理解與總結,不能保證毫無瑕疵,接收網友的斧正。

 

 

    

 

相關文章