Python常見面試題016. 請實現如下功能|談談你對閉包的理解

2023-04-10 18:00:24

016. 請實現如下功能|談談你對閉包的理解

摘自<流暢的python> 第七章 函數裝飾器和閉包

  • 實現一個函數(可以不是函數)avg,計算不斷增加的系列值的平均值,效果如下

    def avg(...):
        pass
    avg(10) =>返回10
    avg(20) =>返回10+20的平均值15
    avg(30) =>返回10+20+30的平均值20
    
  • Python常見面試題015.請實現一個如下功能的函數有點類似,但又不太一樣

  • 關鍵是你需要有個變數來儲存歷史值

類的實現方式

  • 參考程式碼

    class Average():
        def __init__(self):
            self.series = []
        def __call__(self, value):
            self.series.append(value)
            return sum(self.series)/len(self.series)
    
    avg = Average()
    print(avg(10))
    print(avg(20))
    print(avg(30))
    
  • avg是個Average的範例

  • avg有個屬性series,一開始是個空列表

  • __call__使得avg物件可以像函數一樣呼叫

  • 呼叫的時候series會保留,因為series只在第一次初始化的時候置為空列表

  • 下面的事情就變得簡單了


  • 但有沒有其他做法呢?
  • 有的,答案是:閉包

閉包實現

  • 參考程式碼

    def make_average():
        series = []
        def averager(value):
            series.append(value)
            return sum(series)/len(series)
        return averager
    avg = make_average()
    print(avg(10))
    print(avg(20))
    print(avg(30))
    
  • 仔細對比2個程式碼,你會發現相似度是極高的

  • 一個是類,一個是函數

  • 類中儲存歷史值的是self.series,函數中的是series區域性變數

  • 類範例能呼叫是實現了__call__,函數的實現中,avg是make_average()的返回值averager,是個函數名,所以它也能呼叫

閉包 closure 初識

  • 閉包closure定義:

    • 在一個外函數中定義了一個內函數
    • 內函數裡運用了外函數的臨時變數
    • 外函數的返回值是內函數的參照
  • 以上面的為例

    def make_average(): # 外函數
        series = [] # 臨時變數(區域性變數)
        def averager(value): # 內函數
            series.append(value)
            return sum(series)/len(series)
        return averager # 返回內函數的參照
    
  • 下面這些話你可能聽的雲裡霧裡的,姑且聽一下。

  • series 是 make_averager 函數的區域性變數,因為那個函數的定義體中初始化了series:series = []

  • 呼叫 avg(10) 時,make_averager 函數已經返回了,而它的本地作用域也一去不復返了

  • 在 averager 函數中,series 是自由變數(free variable)。這是一個技術術語,指未在本地作用域中繫結的變數

  • averager 的閉包延伸到那個函數的作用域之外,包含自由變數 series 的繫結

反組合(dis=Disassembler)

from dis import dis
dis(make_average)
  2           0 BUILD_LIST               0
              2 STORE_DEREF              0 (series)

  3           4 LOAD_CLOSURE             0 (series)
              6 BUILD_TUPLE              1
              8 LOAD_CONST               1 (<code object averager at 0x000002225DD1CBE0, file "<ipython-input-1-a43a8601eedd>", line 3>)
             10 LOAD_CONST               2 ('make_average.<locals>.averager')
             12 MAKE_FUNCTION            8 (closure)
             14 STORE_FAST               0 (averager)

  6          16 LOAD_FAST                0 (averager)
             18 RETURN_VALUE

Disassembly of <code object averager at 0x000002225DD1CBE0, file "<ipython-input-1-a43a8601eedd>", line 3>:
  4           0 LOAD_DEREF               0 (series)
              2 LOAD_METHOD              0 (append)
              4 LOAD_FAST                0 (value)
              6 CALL_METHOD              1
              8 POP_TOP

  5          10 LOAD_GLOBAL              1 (sum)
             12 LOAD_DEREF               0 (series)
             14 CALL_FUNCTION            1
             16 LOAD_GLOBAL              2 (len)
             18 LOAD_DEREF               0 (series)
             20 CALL_FUNCTION            1
             22 BINARY_TRUE_DIVIDE
             24 RETURN_VALUE
  • 讀懂上面的,不是人乾的事情,不過你依然有可能

    https://docs.python.org/zh-cn/3/library/dis.html#bytecodes
    

code屬性

  • 怎麼樣不雲裡霧裡呢

  • 檢視avg.__code__屬性

    [_ for _ in dir(avg.__code__) if _[:2]=='co']
    
    ['co_argcount',
     'co_cellvars',
     'co_code',
     'co_consts',
     'co_filename',
     'co_firstlineno',
     'co_flags',
     'co_freevars',
     'co_kwonlyargcount',
     'co_lnotab',
     'co_name',
     'co_names',
     'co_nlocals',
     'co_posonlyargcount',
     'co_stacksize',
     'co_varnames']
    
  • 官方解釋

    屬性 描述
    co_argcount 引數數量(不包括僅關鍵字引數、* 或 ** 引數)
    co_code 原始編譯位元組碼的字串
    co_cellvars 單元變數名稱的元組(通過包含作用域參照)
    co_consts 位元組碼中使用的常數元組
    co_filename 建立此程式碼物件的檔案的名稱
    co_firstlineno 第一行在Python原始碼的行號
    co_flags CO_* 標誌的點陣圖,詳見 此處
    co_lnotab 編碼的行號到位元組碼索引的對映
    co_freevars 自由變數的名字組成的元組(通過函數閉包參照)
    co_posonlyargcount 僅限位置引數的數量
    co_kwonlyargcount 僅限關鍵字引數的數量(不包括 ** 引數)
    co_name 定義此程式碼物件的名稱
    co_names 區域性變數名稱的元組
    co_nlocals 區域性變數的數量
    co_stacksize 需要虛擬機器器堆疊空間
    co_varnames 引數名和區域性變數的元組
  • 通過__code__分析

    def make_average(): 
        series = []
        def averager(value): 
            series.append(value)
            total = sum(series)
            return total/len(series)
        return averager 
    avg = make_average()
    avg.__code__.co_varnames  # 引數名和區域性變數的元組
    # ('value', 'total')  # value是引數,total是區域性變數名
    avg.__code__.co_freevars 
    # ('series',) # 自由變數的名字組成的元組(通過函數閉包參照)
    
    
    
    
    
  • 結合avg.__closure__

    avg.__closure__
    # (<cell at 0x000002225FA4DC70: list object at 0x000002225EE35600>,)
    # 這是個cell物件,list物件
    len(avg.__closure__) # 1
    avg.__closure__[0].cell_contents # [] 因為你還沒呼叫
    avg(10)
    avg(20)
    avg(30)
    avg.__closure__[0].cell_contents # [10, 20, 30] 儲存著真正的值
    
    
    
  • 閉包是一種函數,它會保留定義函數時存在的自由變數的繫結,這樣呼叫函數時,雖然定義作用域不可用了,但是仍能使用那些繫結。

  • 只有巢狀在其他函數中的函數才可能需要處理不在全域性作用域中的外部變數