從作用域到閉包再到裝飾器

首先預祝抗擊在武漢肺炎第一線的兄弟姐妹們平安歸來,撐住!你們辛苦啦!沒事兒有我們共渡難關!


寫在開頭

最近裝飾器用的比較多,由此想到了閉包,再往上就是作用域的一些陷阱問題;索性記錄一下,類似於一個散文形式的,只是對自己的一些想法和網上的一些資料進行梳理,想法 -> 驗證 -> 查實 僅此而已,工作時候零零散散,現在拾掇一下,作爲總結,希望對你有所幫助~ 新年快樂!

一些顯而易見的case

引子1

a = 3
def foo1(x):
    b = 4
    print (x + a)
    print ("local", locals())  # local表示局部作用域

foo1(3)
6
local {'b': 4, 'x': 3}
引子2
a = 3
def foo2(x):
    a = 4
    b = 4
    print (x + a)
    print ("local",locals())

print(a) 
print('----')
foo2(3)
3
----
7
local {'b': 4, 'a': 4, 'x': 3}
引子3
a = 3
def foo3(x):
    global a
    a = a + 3
    print (a)
    print ("local",locals())

print(a) 
print('----')
foo3(3)
3
----
6
local {'x': 3}

看看下面一個例子,foo4(3)在做些什麼

def foo4(x):
    def _help():
        print ("local",locals())
        print (x + 3)
    return _help

foo4(3)    
<function __main__.foo4.<locals>._help>

foo4(3)執行完成後,返回的是_help,那麼如果再次對這個返回的值進行操作foo4(3)(),實質上是構成_help(),不就是執行一次正常的函數麼?

# 驗證假設
foo4(3)()  
local {'x': 3}
6

以上就是我們說的最近簡單的閉包 - 對內層函數和其內部引用的上層局部命名空間變量的一種封裝。先說下優缺點,不明白可以先略過,稍微留點印象即可

  • 優點
    • 減少全局變量的使用,減少變量污染
    • 適合隱藏數據,當一個類中只包含一個方法,使用閉包會更加優雅
  • 缺點
    • 多層級的嵌套可能帶來邏輯上的混亂以及閉包陷阱

上述的幾個例子發現,在執行完foo4(3)時,已經生成了一個_help的函數返回,且裏面還夾帶了“私貨”,一個外部變量3 (就是後文會講到的自由變量),所以閉包能夠自身傳遞內層函數所需要(引用)的變量,自身還能再傳入新的變量,詳細看下面一個例子

def foo5(x):
    def _help(*args):
        print ("local",locals())
        return (x*args[0] + 3)
    return _help

h = foo5(2)
print(h)   # 類似於scala中的柯里化
print(h(3))
<function foo5.<locals>._help at 0x10441c378>
local {'args': (3,), 'x': 2}
9

foo(5)完成了兩種參數的傳遞,一個是構造h=foo5(2)時傳入的2,還有一個就是後續執行h(3)時候傳遞的變量3(ps:*agrs表示以元組的形式封裝變量之後再拆包傳遞,不再贅述詳見:*args和**kwargs

作用域及閉包

下面詳細拆一下作用域到底體現在哪裏,以及對閉包的影響

在Python程序中聲明、改變、查找變量名時,都是在一個保存變量名的命名空間中進行中,此命名空間亦稱爲變量的作用域。python的作用域是靜態的,在代碼中變量名被賦值的位置決定了該變量能被訪問的範圍。即Python變量的作用域由變量所在源代碼中的位置決定.(From: python3的local, global, nonlocal簡析

  • L = Local 局部作用域
  • E = Enclosing 嵌套作用域
  • N = nonlocal 只作用於嵌套作用域,而且只是作用在函數裏面
  • G = global 全局作用域
  • B = Built-in 內置作用域

簡單來說,python引用變量的順序: 當前作用域局部變量->外層作用域變量->當前模塊中的全局變量->python內置變量,如果找不到則會報錯NameError: name 'x' is not defined;其中built-in可以類似認爲函數內置方法如len;(當然你在包內自己寫len函數,那麼可以根據LEGB原則覆蓋系統自帶的len) 下面給幾個例子:

  • L - Local 局部作用域 - 包含在def定義的函數體內

def foo6(x):
    a = 5  # 不再被使用
    def _help(*args):  
        a = 0 # 在閉包內找到a, 最先找到,直接使用; Local 可能是在一個函數或者類方法內部。
        print ("local",locals())
        return (x*args[0] + a)
    return _help

h = foo6(2)
print(h)
print(h(3))
<function foo6.<locals>._help at 0x10441cae8>
local {'a': 0, 'args': (3,), 'x': 2}
6
  • E - Enclosing 閉包空間,嵌套作用域

def foo7(x):
    a = 5  # Enclosed 可能是嵌套函數內,比如說 一個函數包裹在另一個函數內部。
    def _help(*args):
        print ("local",locals())
        return (x*args[0] + a)
    return _help

h = foo7(2)
print(h)
print(h(3))

# 相比較foo6,a在_help內部沒有,只有往上層找
<function foo7.<locals>._help at 0x104172f28>
local {'args': (3,), 'x': 2, 'a': 5}
11
  • G - Global,函數定義所在模塊的命名空間

a = 0
def foo8(x):
    def _help(*args):
        print ("local",locals())
        return (x*args[0] + a)
    return _help

h = foo8(3)
print(h)
print(h(3))
<function foo8.<locals>._help at 0x1045da268>
local {'args': (3,), 'x': 3}
9
作用域陷阱 - local variable 'xx' referenced before assignment的體現

一些“顯而易見”的使用方法可能並不是按照我們預期的那樣發展,這就是作用域陷阱,有時候也會被認爲是閉包陷阱,實質上都是作用域的選擇導致的問題


def foo9(x):
    a = 0
    def _help(*args):
        a += 1
        print ("local",locals())
        return (x*args[0] + a)
    return _help

h = foo9(3)
print("局部變量:",h.__code__.co_varnames)  # 局部變量
print("自由變量:",h.__code__.co_freevars)  # 自由變量
print(h(3))

局部變量: ('args', 'a')
自由變量: ('x',)
UnboundLocalError                         Traceback (most recent call last)

<ipython-input-32-fbb9c5fd9756> in <module>()
     14 print("局部變量:",h.__code__.co_varnames)  # 局部變量
     15 print("自由變量:",h.__code__.co_freevars)  # 自由變量
---> 16 print(h(3))
<ipython-input-32-fbb9c5fd9756> in _help(*args)
      6     a = 0
      7     def _help(*args):
----> 8         a += 1
      9         print ("local",locals())
     10         return (x*args[0] + a)
 UnboundLocalError: local variable 'a' referenced before assignment

對於數字,字符串,元組等不可變的類型來說,a +=1 只能讀取不能更新,如果嘗試重新綁定,相當於 a = a + 1 操作,會隱式創建局部變量 a ,這樣執行h = foo9(3)的時候,啓動去定義函數_help,會先判斷是否有需要引用到上游的變量,如果自己這一層有這個變量a,那麼上層的a就不是自由變量了,會被銷燬,沒辦法保存在閉包中;再舉個簡單的例子如下所示

b = 3
def bar(x):
    print (x)
    print (b)
    b = 9
    

print("局部變量:",bar.__code__.co_varnames)  # 局部變量
print("自由變量:",bar.__code__.co_freevars)  # 自由變量
bar = bar(3)

# 編譯函數體時,發現b被賦值(b=9),所以不採用向上找變量 b = 3,當真要啓動的時候,發現要用b,卻找不到值
局部變量: ('x', 'b')
自由變量: ()
3
---------------------------------------------------------------------------

UnboundLocalError                         Traceback (most recent call last)

<ipython-input-39-fabf28feceb0> in <module>()
      8 print("局部變量:",bar.__code__.co_varnames)  # 局部變量
      9 print("自由變量:",bar.__code__.co_freevars)  # 自由變量
---> 10 bar = bar(3)
     11 
     12 # 編譯函數體時,發現b被賦值(b=9),所以不採用向上找變量 b = 3,當真要啓動的時候,發現要用b,卻找不到值
     <ipython-input-39-fabf28feceb0> in bar(x)
      2 def bar(x):
      3     print (x)
----> 4     print (b)
      5     b = 9
      6 
      UnboundLocalError: local variable 'b' referenced before assignment

其實從字節碼的角度去考慮會更加清楚一些;我們知道python通過cpython解釋器解釋成機器能夠執行的字節碼去操作的;如果湊巧你還了解dis這個神奇的包,那就更好啦

b = 3
def bar(x):
    print (x)
    print (b)
    b = 9
    

import dis
dis.dis(bar)

3           0 LOAD_GLOBAL              0 (print)
              2 LOAD_FAST                0 (x)   加載局部變量
              4 CALL_FUNCTION            1
              6 POP_TOP

  4           8 LOAD_GLOBAL              0 (print)
             10 LOAD_FAST                1 (b)   加載局部變量
             12 CALL_FUNCTION            1
             14 POP_TOP

  5          16 LOAD_CONST               1 (9)  加載常量
             18 STORE_FAST               1 (b)  局部變量賦值
             20 LOAD_CONST               0 (None)
             22 RETURN_VALUE

那麼去掉裏面的 b=9會如何呢?

b = 3
def bar(x):
    print (x)
    print (b)
    

import dis
dis.dis(bar)

  3           0 LOAD_GLOBAL              0 (print)
              2 LOAD_FAST                0 (x)   加載局部變量
              4 CALL_FUNCTION            1
              6 POP_TOP

  4           8 LOAD_GLOBAL              0 (print)
             10 LOAD_GLOBAL              1 (b)  加載全局變量
             12 CALL_FUNCTION            1
             14 POP_TOP
             16 LOAD_CONST               0 (None)
             18 RETURN_VALUE

從上面兩個例子可以看出,其實print操作的時候,需要對b進行引用,而函數內部有b的局部變量被申明(加載),只是還未被賦值(參考第一個例子中對bSTORE_FAST操作),所以導致不再往上查找變量,所以全局變量b=3根本沒有引用,也就導致報錯的問題了;詳細的字節碼相關的可以參考:python 字節碼死磕,dis包官方文檔可以參考:dis — Disassembler for Python bytecode,這裏不做贅述;

再來個例子 說明 foo9的情況


def foo9_2(x):
    a = 0
    def _help(*args):
        c = a + 1
        a = c
        return (x*args[0] + a)
    return _help

h = foo9_2(3)
print("局部變量:",h.__code__.co_varnames)  # 局部變量
print("自由變量:",h.__code__.co_freevars)  # 自由變量
print(h(3))
局部變量: ('args', 'a', 'c')
自由變量: ('x',)
---------------------------------------------------------------------------

UnboundLocalError                         Traceback (most recent call last)

<ipython-input-40-7228e32407b2> in <module>()
     12 print("局部變量:",h.__code__.co_varnames)  # 局部變量
     13 print("自由變量:",h.__code__.co_freevars)  # 自由變量
---> 14 print(h(3))
<ipython-input-40-7228e32407b2> in _help(*args)
      4     a = 0
      5     def _help(*args):
----> 6         c = a + 1
      7         a = c
      8         return (x*args[0] + a)
      UnboundLocalError: local variable 'a' referenced before assignment

再來一個體會一下

def foo9_3(x):
    a = 0
    def _help(*args):
        c = a + 1
				# a = c
        return (x*args[0] + a)
    return _help

h = foo9_3(3)
print("局部變量:",h.__code__.co_varnames)  # 局部變量
print("自由變量:",h.__code__.co_freevars)  # 自由變量
print(h(3))
局部變量: ('args', 'c')
自由變量: ('a', 'x')
9

通過比較foo9_3foo9_2可知,當執行完h=foo9_2(3)時,發現在_help中已經申明瞭局部變量a,因爲python在編譯函數體時,它判斷a是局部變量(在函數體內被賦值:a = c),所以外層函數的自由變量a認爲自己將不被引用,將被銷燬;而調用_help時,會從局部變量開始往上找a,而局部變量a壓根就沒綁定值;而在foo9_3中,c = a + 1申明爲內層函數的局部變量,而且在內也沒有再次申明局部變量a,所以根據LEGB原則,往上層找,找到上層的局部變量a = 0,被引用。既然知道了作用域陷阱帶來的問題,那麼其實可以比較好的解決上述的一些問題達到期望達到的訴求

  • 解決方法1:傳遞綁定局部變量

def foo9_1(x):
    a = 0
    def _help(*args,z=a):
        z += 1
        print ("local",locals()) # 對於local來說,觀察的是針對這個函數生效的變量,所以上游的自由變量和這個函數內的局部變量都被計算
        return (x*args[0] + z)
    return _help

h = foo9_1(3)

print("局部變量:",h.__code__.co_varnames)  # 局部變量
print("自由變量:",h.__code__.co_freevars)  # 自由變量
print(h(3))


局部變量: ('z', 'args')
自由變量: ('x',)
local {'args': (3,), 'z': 1, 'x': 3}
10

一個比較重要的點是,函數是在定義時候傳遞進入的參數生效,並不需要執行的時候才生效h = foo9_1(3)時,其中的變量a已經隨着構造h的時候賦值給了z,所以h = foo9_1(3)執行完後a被銷燬,因爲下游不要引用到,取而代之的是z,所以當z+=1時,直接進行+1操作;下面是描述函數在定義時參數生效的例子(from:stackoverflow上高讚的回答

# python 默認參數是在函數定義的時候已經求值,而不是調用的時候   
import time
def report(when=time.strftime("%Y-%m-%d %X",time.localtime())):
    print (when)

report()
time.sleep(1)
report()

print('--------')

def report2():
    print(time.strftime("%Y-%m-%d %X",time.localtime()))

report2()
time.sleep(1)
report2()  
2020-01-25 14:39:00
2020-01-25 14:39:00
--------
2020-01-25 14:39:01
2020-01-25 14:39:02
  • 解決方法2:使用nonlocal來申明;nonlocal關鍵字用來在函數或其他作用域中使用外層(非全局)變量

def foo10(x):
    a = 0
    def _help(*args):
        nonlocal a 
        a += 1
        print ("local",locals())
        return (x*args[0] + a)
    
    return _help

h = foo10(3)
print("局部變量:",h.__code__.co_varnames)  # 局部變量
print("自由變量:",h.__code__.co_freevars)  # 自由變量
print(h(3))
局部變量: ('args',)
自由變量: ('a', 'x')
local {'args': (3,), 'x': 3, 'a': 1}
10

其實nonlocalglobal有個異曲同工之妙,可以說是打通兩層之間的橋樑,nonlocal是打通L->Eglobal?-> G

a = 1
def foo10(x):
    a = 0
    def _help(*args):    
        global a 
        a += 1
        print ("local",locals())
        return (x*args[0] + a)
    return _help

h = foo10(3)
print("局部變量:",h.__code__.co_varnames)  # 局部變量
print("自由變量:",h.__code__.co_freevars)  # 自由變量
print(h(3))  # 3*3 + 2
print(a)
局部變量: ('args',)
自由變量: ('x',)
local {'args': (3,), 'x': 3}
11
2

可以發現,global在函數內部改變全局變量,而nonlocal則是改變嵌套函數內空間的變量也就是Enclosed

  • 解決方法3:使用可變對象

def foo11(x):
    a = []
    def _help(*args):
        a.append(0)
        print ("local",locals())
        return (x*args[0] + a[0])
    return _help

h = foo11(3)
print("局部變量:",h.__code__.co_varnames)  # 局部變量
print("自由變量:",h.__code__.co_freevars)  # 自由變量
print(h(3))
局部變量: ('args',)
自由變量: ('a', 'x')
local {'args': (3,), 'x': 3, 'a': [0]}
9

foo9的例子中,對於不可變變量,只能被讀取不能被更新,我們所謂的更新只是變量的重新綁定;而對於列表來說,內部是可變的,可以理解爲一個不可變的容器但是容器內部可變,可以看下面的例子理解下

x = 3
print(id(x))
x = 4
print(id(x))
q = []
print(id(q))
q.append(3)
print(id(q))
4332976768
4332976800
4368241864
4368241864
閉包陷阱

特別是在循環體中變量的產生和銷燬過程中,形成陷阱

def foo12(x):
    fs = []
    for _ in range(x):
        def _help(*args):
            print ("local",locals())
            return ("id(_)={id_}; x={x}; _*args[0]={res}".format(id_=id(_) ,x= x, res = _*args[0]))
        fs.append(_help)
            
    return fs

h = foo12(4)
s = [_(3) for _ in h]
print (s)
local {'args': (3,), 'x': 4, '_': 3}
local {'args': (3,), 'x': 4, '_': 3}
local {'args': (3,), 'x': 4, '_': 3}
local {'args': (3,), 'x': 4, '_': 3}
['id(_)=4332976768; x=4; _*args[0]=9', 'id(_)=4332976768; x=4; _*args[0]=9', 'id(_)=4332976768; x=4; _*args[0]=9', 'id(_)=4332976768; x=4; _*args[0]=9']

id(_)並沒有按照想象的那樣,每次記錄下循環的變量值,而是直接採用的是循環的末尾值,使用__closure__屬性和cell對象來觀察閉包中的自由變量(並未在本地作用域中綁定的變量)

  • 所有函數都有一個__closure__ 屬性,如果這個函數是一個閉包的話,那麼它返回的是一個由cell對象 組成的元組對象。
  • cell對象的cell_contents屬性就是閉包中的自由變量。
for index_,value in enumerate(h):
    print (value, [i.cell_contents for i in value.__closure__])
<function foo12.<locals>._help at 0x10441c158> [3, 4]
<function foo12.<locals>._help at 0x10441cf28> [3, 4]
<function foo12.<locals>._help at 0x10441cd08> [3, 4]
<function foo12.<locals>._help at 0x104172f28> [3, 4]

可以發現閉包內包含的外部變量有傳入的遍歷時候的變量 _ 還有 x,都是屬於傳入閉包的外部變量

def foo13(x):
    fs = []
    for _ in range(x):
        def _help(*args,m=_):
            print ("local",locals())
            return ("id(m)={idm}; _={_}; x={x}; m*args[0]={res}".format(idm=id(m), _=_ ,x= x, res = m*args[0]))
        fs.append(_help)
            
    return fs

h = foo13(4)
s = [_(3) for _ in h]
print (s)

for index_,value in enumerate(h):
    print (value, [i.cell_contents for i in value.__closure__])

local {'args': (3,), 'm': 0, 'x': 4, '_': 3}
local {'args': (3,), 'm': 1, 'x': 4, '_': 3}
local {'args': (3,), 'm': 2, 'x': 4, '_': 3}
local {'args': (3,), 'm': 3, 'x': 4, '_': 3}
['id(m)=4332976672; _=3; x=4; m*args[0]=0', 'id(m)=4332976704; _=3; x=4; m*args[0]=3', 'id(m)=4332976736; _=3; x=4; m*args[0]=6', 'id(m)=4332976768; _=3; x=4; m*args[0]=9']
<function foo13.<locals>._help at 0x1045ea730> [3, 4]
<function foo13.<locals>._help at 0x1045ea620> [3, 4]
<function foo13.<locals>._help at 0x1045ead08> [3, 4]
<function foo13.<locals>._help at 0x1045eab70> [3, 4]

可以發現閉包內包含的外部變量只有傳入的 x,還有最後被引用的循環邊界 _ ,而沒有被賦值的m

所以解釋了爲什麼foo13中沒有外部變量_, 因爲在定義函數_help的時候(並不需要被執行),已經將 _ 傳遞給了m進行保存了,fs中保存了四個封裝好的_help函數,s = [_(3) for _ in h]這一步相當於給這幾個函數都傳遞了3這個參數進去;但當執行這一步之前,已經完成h = foo13(4),表示已經完成了執行,循環結束,_ 已經到了 3 ,(循環體內的值不斷進行銷燬,保留到最後被引用)所以當調用_help時候,_help就會根據LEGB的方式一級級往上找需要用到的_變量,_這個時候就是遍歷完後最後的值;可以參考下:程序員必知的Python陷阱與缺陷列表

舉兩個例子來描述一下這個現象,特別是針對參數中傳入可變參數時需要注意的一些情況;在《編寫高質量Python代碼的59個有效方法》中提到過,參數的初始化傳遞最好使用賦值None的方式處理,這樣對需要用到的時候可被進行初始化,避免一些變量泄露的情況出現“反常識”的現象

def report_1(data, list_=[]):
        print ("local",locals())
        list_.append(data)
        return list_
        
        
f1 = report_1("test1")
f1.append("test1_1")
print(f1)
print(id(f1))

print('------------------')
f2 = report_1("test2")
f2.append("test2_1")
print(f2) # 在f1的情況下繼續append
print(id(f2))
print('------------------')

f3 = report_1("test3",list_=[])
f3.append("test3_1")
print(f3) 
print(id(f3))


local {'list_': [], 'data': 'test1'}
['test1', 'test1_1']
4367345800
------------------
local {'list_': ['test1', 'test1_1'], 'data': 'test2'}
['test1', 'test1_1', 'test2', 'test2_1']
4367345800
------------------
local {'list_': [], 'data': 'test3'}
['test3', 'test3_1']
4368427400

在執行f1 = report_1("test1")的時候,參數爲一個空列表list_=[],相當於全局變量,當函數內部不再指定新同名變量時,用的就是全局變量;有一個很好的執行可視化的網站:http://www.pythontutor.com/visualize.html#mode=display 推薦給大家,可以一步步看到執行之後變量的存放等等
在這裏插入圖片描述

# 解決方法也比較簡單,先將默認參數置None,要用的時候再使用
def report_2(data, list_=None):
        if list_ is None:
            list_ = []
        print ("local",locals())
        list_.append(data)
        return list_
        
f1 = report_2("test1")
f1.append("test1_1")
print(f1)
print(id(f1))

print('------------------')
f2 = report_2("test2")
f2.append("test2_1")
print(f2) 
print(id(f2))


local {'list_': [], 'data': 'test1'}
['test1', 'test1_1']
4367815624
------------------
local {'list_': [], 'data': 'test2'}
['test2', 'test2_1']
4368472712

由此深入的還有兩個例子

  • 例子1,使用可變變量
def foo13_1(x):
    fs = []
    for _ in range(x):
        def _help(*args,m=[]):
            m.append(_)
            return (id(m), x, m[-1]*args[0])
        fs.append(_help)
            
    return fs

h = foo13_1(4)
s = [_(3) for _ in h]
print (s)

for index_,value in enumerate(h):
    print (value, [i.cell_contents for i in value.__closure__])
[(4489292616, 4, 9), (4490954888, 4, 9), (4491173640, 4, 9), (4491173704, 4, 9)]
<function foo13_1.<locals>._help at 0x10b9f48c8> [3, 4]
<function foo13_1.<locals>._help at 0x10b9f4a60> [3, 4]
<function foo13_1.<locals>._help at 0x10b9f4378> [3, 4]
<function foo13_1.<locals>._help at 0x10b9f4e18> [3, 4]
  • 例子2
def foo13_2(x):
    fs = []
    for _ in range(x):
        def _help(*args,m=None,_=_):
            m = []
            m.append(_)
            return (id(m), x, m[-1]*args[0])
        fs.append(_help)
            
    return fs

h = foo13_2(4)
s = [_(3) for _ in h]
print (s)

for index_,value in enumerate(h):
    print (value, [i.cell_contents for i in value.__closure__])
[(4491347912, 4, 0), (4491347912, 4, 3), (4491347912, 4, 6), (4491347912, 4, 9)]
<function foo13_2.<locals>._help at 0x10bb1e840> [4]
<function foo13_2.<locals>._help at 0x10bb1e9d8> [4]
<function foo13_2.<locals>._help at 0x10bb1e378> [4]
<function foo13_2.<locals>._help at 0x10bb1e2f0> [4]

裝飾器 - 閉包的語法糖

瞭解完閉包的一些性質後,再來看裝飾器(還不清楚的可以參考:理解Python裝飾器(Decorator),其實就是帶有語法糖@的閉包高階的用法.實質上,裝飾器感覺更多的是對閉包更多的是一種對閉包思想的一種優雅表達,特別是一些語法糖的使用會顯得尤其優雅簡潔;簡單說,裝飾器可以在不改變原始函數的情況下,給函數添加一些額外的功能;看過一篇知乎的回答比較形象,普通函數就像一個人穿了一件T恤,然後閉包就是各種外套啊,帽子什麼的,根據需求進行增加,這樣不會破壞被裝飾的函數;下面舉兩個例子

需求:對一個函數採用重試機制,每過一段時間重試一次

  • 原始業務代碼
import time

def pri():
    time_ = time.strftime("%Y-%m-%d %X",time.localtime())
    p = "execute time: %s"%(time_)
    print (p)

pri()

execute time: 2020-01-25 19:37:48
  • 裝飾代碼

# 需要牢記的一點事,在python中,一切都是對象,當然包括函數,可以將函數當做參數傳遞進去
def retry(fn):
    def wrap():
        for _ in range(1,4):
            print ("check %s times" % _)
            fn()
            time.sleep(1)
    return wrap


def pri():
    time_ = time.strftime("%Y-%m-%d %X",time.localtime())
    p = "execute time: %s"%(time_)
    print (p)

    
rep = retry(pri)  # 此時rep返回的是一個wrap函數,當執行wrap()後,內部函數開始執行
print(rep)
rep()
<function retry.<locals>.wrap at 0x1045da048>
check 1 times
execute time: 2020-01-25 19:37:44
check 2 times
execute time: 2020-01-25 19:37:45
check 3 times
execute time: 2020-01-25 19:37:46

這個可以看foo4foo5兩個例子,這裏不做贅述;利用@語法糖,將上述代碼再一次精簡,實現的效果是等效的,包括內部執行也是一樣


def retry(fn):
    def wrap():
        for _ in range(1,4):
            print ("check %s times" % _)
            fn()
            time.sleep(1)
    return wrap

@retry
def pri():
    time_ = time.strftime("%Y-%m-%d %X",time.localtime())
    p = "execute time: %s"%(time_)
    print (p)

pri()
check 1 times
execute time: 2020-01-25 19:39:22
check 2 times
execute time: 2020-01-25 19:39:23
check 3 times
execute time: 2020-01-25 19:39:24

上述的例子其中的重試次數在代碼中寫死了,這個對於需求來說只能說夠用但是不夠靈活,如果針對不同的函數需要不同的延遲時間和不同的重試次數,這個需要去修改代碼了,這個很不優雅的行爲,好在裝飾器可以傳遞參數

傳參裝飾器 - 給裝飾器更強靈活性
# 裝飾器中傳遞參數
def retry(x,y):
    x = x*y
    # ----- 這裏下面和上面完全一樣,不一樣的是再次使用閉包的性質往上套了一層,用來接收參數 x,y
    def myretry(fn):
        def wrap():
            for _ in range(1,x):
                print ("check %s times" % _)
                fn()
                time.sleep(1)
        return wrap
    # ----- 
    return myretry


def pri():
    time_ = time.strftime("%Y-%m-%d %X",time.localtime())
    p = "execute time: %s"%(time_)
    print (p)

# 方式1
# f = retry(2,3)
# rep = f(pri)

# 方式2
 
rep = retry(2,3)(pri)

rep()

print ([i.cell_contents for i in rep.__closure__]) 
# [<function pri at 0x10b9f4b70>, 6] 可見只是保存了x的值爲自由變量


check 1 times
execute time: 2020-01-25 19:41:24
check 2 times
execute time: 2020-01-25 19:41:25
check 3 times
execute time: 2020-01-25 19:41:26
check 4 times
execute time: 2020-01-25 19:41:27
check 5 times
execute time: 2020-01-25 19:41:28
[<function pri at 0x10441c9d8>, 6]

同樣改造成使用語法糖@的方式,這樣代碼更精簡

# 使用語法糖@的方式

def retry(x,y):
    x = x*y
    # --------
    def myretry(fn):
        def wrap():
            for _ in range(1,x):
                print ("check %s times" % _)
                fn()
                time.sleep(1)
        return wrap
    # --------
    return myretry


@retry(2,3)   # 區別
def pri():
    time_ = time.strftime("%Y-%m-%d %X",time.localtime())
    p = "execute time: %s"%(time_)
    print (p)


pri()
check 1 times
execute time: 2020-01-25 19:41:59
check 2 times
execute time: 2020-01-25 19:42:00
check 3 times
execute time: 2020-01-25 19:42:01
check 4 times
execute time: 2020-01-25 19:42:02
check 5 times
execute time: 2020-01-25 19:42:03
  • 構造外部參數影響函數內部;實際使用場景中可以將一些連接句柄通過單例的模式構造出來,然後通過裝飾器進行句柄(連接)傳遞,這樣只需要關心業務代碼,而不需要額外考慮如何連接數據庫和切短連接等;

def retry(x,y):
    x = x*y
    # --------
    def myretry(fn):
        def wrap(*args,**kwargs):  # 這裏*args,**kwargs用來監控被包裝函數的傳遞參數
            for _ in range(1,x):
                print ("check %s times" % _)
                fn(*args, param=(x, _), **kwargs)  # 甚至可以構造包裝函數中的自由變量進入傳遞
                time.sleep(1)
        return wrap
    # --------
    return myretry


@retry(2,3)
def pri(x, param=None):
    time_ = time.strftime("%Y-%m-%d %X",time.localtime())
    p = "owner param:%s; execute time: %s; receive param:%s"%(x, time_, param)
    print (p)


    
pri(3)
check 1 times
owner param:3; execute time: 2020-01-25 19:42:08; receive param:(6, 1)
check 2 times
owner param:3; execute time: 2020-01-25 19:42:09; receive param:(6, 2)
check 3 times
owner param:3; execute time: 2020-01-25 19:42:10; receive param:(6, 3)
check 4 times
owner param:3; execute time: 2020-01-25 19:42:11; receive param:(6, 4)
check 5 times
owner param:3; execute time: 2020-01-25 19:42:12; receive param:(6, 5)

既然我可以給函數“套上毛衣”,那麼我再這個毛衣外面再套“一層外套”不也可以麼,的確,裝飾器支持多層嵌套

# 多層裝飾

def log(fn):
    '''
    執行時候打一遍日誌
    '''
    def wrap(*args,**kwargs):
        print ("log begin... wrapped function name: %s"%(fn.__name__))
        fn(*args,**kwargs)
    return wrap
    
    
def retry(x,y):
    x = x*y
    def myretry(fn):
        def wrap(*args,**kwargs):
            for _ in range(1,x):
                print ("check %s times" % _)
                fn(*args, param=(x, _), **kwargs)
                time.sleep(1)
        return wrap
    return myretry


@log
@retry(2,3)
def pri(x, param=None):
    time_ = time.strftime("%Y-%m-%d %X",time.localtime())
    p = "owner param:%s; execute time: %s; receive param:%s"%(x, time_, param)
    print (p)


pri(3)

log begin... wrapped function name: wrap
check 1 times
owner param:3; execute time: 2020-01-25 19:44:27; receive param:(6, 1)
check 2 times
owner param:3; execute time: 2020-01-25 19:44:28; receive param:(6, 2)
check 3 times
owner param:3; execute time: 2020-01-25 19:44:29; receive param:(6, 3)
check 4 times
owner param:3; execute time: 2020-01-25 19:44:30; receive param:(6, 4)
check 5 times
owner param:3; execute time: 2020-01-25 19:44:31; receive param:(6, 5)

關於多層裝飾器執行順序是由底部開始往上嵌套,最接近函數的包裝最先執行

# 關於多層裝飾器執行順序的問題

def log(fn):
    def wrap(*args,**kwargs):
        print ("log begin... wrapped function name: %s"%(fn.__name__))
        fn(*args,**kwargs)
    return wrap
    
    
def retry(x,y):
    x = x*y
    def myretry(fn):
        def wrap(*args,**kwargs):
            for _ in range(1,x):
                print ("check %s times" % _)
                fn(*args, param=(x, _), **kwargs)
                time.sleep(1)
        return wrap
    return myretry


# @log
# @retry(2,3)
def pri(x, param=None):
    time_ = time.strftime("%Y-%m-%d %X",time.localtime())
    p = "owner param:%s; execute time: %s; receive param:%s"%(x, time_, param)
    print (p)


# 執行順序從底向上一層層嵌套
    
f = retry(2,3)(pri)
g = log(f)
g(3)

log begin... wrapped function name: wrap
check 1 times
owner param:3; execute time: 2020-01-25 19:45:32; receive param:(6, 1)
check 2 times
owner param:3; execute time: 2020-01-25 19:45:33; receive param:(6, 2)
check 3 times
owner param:3; execute time: 2020-01-25 19:45:34; receive param:(6, 3)
check 4 times
owner param:3; execute time: 2020-01-25 19:45:35; receive param:(6, 4)
check 5 times
owner param:3; execute time: 2020-01-25 19:45:36; receive param:(6, 5)
裝飾器之傳參可有可無 - 偏函數的妙用

裝飾器參數有時候可有可無,如果限定死必須傳遞參數又會給代碼顯得冗餘,畢竟誰也不想出現 @log()來替換@log這樣優雅的方式把;在此之前先說明一下偏函數用法,至於詳細的使用自行百度


from functools import partial
def partial_(x, y=None, z=None):
    return x+y+z
p = partial(partial_, 2, y=1)
print(p)
p(z=3)
functools.partial(<function partial_ at 0x10bb208c8>, 2, y=1)
6

利用偏函數保存變量,部分執行的特性,可以用於構造接受/不接受參數的裝飾器

# 使用偏函數設置可選傳入參數

from functools import wraps, partial


def myretry(fn=None, *, x=None, y=None): 
    if fn is None:
        time_ = time.strftime("%Y-%m-%d %X",time.localtime())
        # 裝飾器的其中一個特性就是只會在函數定義的時候應用一次,所以這個時刻是函數定義的時候並非執行的時刻
        print ("load myretry time:%s" % time_)
        return partial(myretry ,x=x, y=y)
    
    x = x if x else 3
    y = y if y else 2
    @wraps(fn)
    def wrap(*args,**kwargs):
        for _ in range(1,x):
            print ("check %s times" % _)
            r = fn(*args, param=(x, _), **kwargs)
            time.sleep(1)
            print (r)
        return r
    return wrap


def pri(x, param=None):
    time_ = time.strftime("%Y-%m-%d %X",time.localtime())
    p = "owner param:%s; execute time: %s; receive param:%s"%(x, time_, param)
    return p



def pri_1(x, param=None):
    time_ = time.strftime("%Y-%m-%d %X",time.localtime())
    p = "owner param:%s; execute time: %s; receive param:%s"%(x, time_, param)
    return p


pri_partial = myretry(x=4,y=3)  # 因爲沒有傳遞fn,則默認傳遞fn=None,會返回一個限定部分參數(x,y)的偏函數
print(pri_partial)
rep_pri = pri_partial(pri)
print ([i.cell_contents for i in rep_pri.__closure__]) # 這裏只有一個 x的自由變量,_是即產即銷,直接調用運行的,
rep_pri(3)


print("--------------")

rep_pri_1 = myretry(pri)  # 因爲傳遞fn,則x,y都被設置成默認3,2
print ([i.cell_contents for i in rep_pri_1.__closure__]) 
rep_pri_1(3)

load myretry time:2020-01-20 11:14:09
functools.partial(<function myretry at 0x10bb1ebf8>, x=4, y=3)
[<function pri at 0x10bb1e378>, 4]
check 1 times
owner param:3; execute time: 2020-01-20 11:14:09; receive param:(4, 1)
check 2 times
owner param:3; execute time: 2020-01-20 11:14:10; receive param:(4, 2)
check 3 times
owner param:3; execute time: 2020-01-20 11:14:11; receive param:(4, 3)
--------------
[<function pri at 0x10bb1e378>, 3]
check 1 times
owner param:3; execute time: 2020-01-20 11:14:12; receive param:(3, 1)
check 2 times
owner param:3; execute time: 2020-01-20 11:14:13; receive param:(3, 2)

'owner param:3; execute time: 2020-01-20 11:14:13; receive param:(3, 2)'
  • 語法糖模式
from functools import wraps, partial
def myretry(fn=None, *, x=None, y=None): 
    if fn is None:
        time_ = time.strftime("%Y-%m-%d %X",time.localtime())
        # 裝飾器的其中一個特性就是只會在函數定義的時候應用一次,所以這個時刻是函數定義的時候並非執行的時刻
        print ("load myretry time:%s" % time_)
        return partial(myretry ,x=x, y=y)
    
    x = x if x else 3
    y = y if y else 2
    @wraps(fn)
    def wrap(*args,**kwargs):
        for _ in range(1,x):
            print ("check %s times" % _)
            r = fn(*args, param=(x, _), **kwargs)
            time.sleep(1)
            print (r)
        return r
    return wrap

@myretry(x=4,y=3)
def pri(x, param=None):
    time_ = time.strftime("%Y-%m-%d %X",time.localtime())
    p = "owner param:%s; execute time: %s; receive param:%s"%(x, time_, param)
    return p


@myretry
def pri_1(x, param=None):
    time_ = time.strftime("%Y-%m-%d %X",time.localtime())
    p = "owner param:%s; execute time: %s; receive param:%s"%(x, time_, param)
    return p

print("-----啓動執行-----")
time.sleep(2)
pri(3)
print("-----------")
pri_1(3)


load myretry time:2020-01-20 11:21:55
-----啓動執行-----
check 1 times
owner param:3; execute time: 2020-01-20 11:21:57; receive param:(4, 1)
check 2 times
owner param:3; execute time: 2020-01-20 11:21:58; receive param:(4, 2)
check 3 times
owner param:3; execute time: 2020-01-20 11:21:59; receive param:(4, 3)
-----------
check 1 times
owner param:3; execute time: 2020-01-20 11:22:00; receive param:(3, 1)
check 2 times
owner param:3; execute time: 2020-01-20 11:22:01; receive param:(3, 2)

'owner param:3; execute time: 2020-01-20 11:22:01; receive param:(3, 2)'

上述的例子說明裝飾器會在函數定義的時候應用一次,所以這個時刻是函數定義的時候也就是@myretry(x=4,y=3)時,已經開始應用了,所以load myretry time:2020-01-20 11:21:55被打印出來了,可以看做一個裝飾器是一個偏函數的無參構造器情況

@wraps(func)使被裝飾函數信息保留

再回到多層裝飾器這個例子,我們發現出現了log begin... wrapped function name: wrap可以看出,由於裝飾器的使用,被包裝的函數損失了一些重要的元數據,比如函數名,函數註解及調用簽名等都會丟失,如上述的例子中fn.__name__返回的是被包裝函數的替身wrap;可以使用functools中的wraps來裝飾底層的包裝函數,這樣就可以實現保存函數的元數據

# 使用裝飾器保存函數元數據

from functools import wraps

def log(fn):
    @wraps(fn)
    def wrap(*args,**kwargs):
        print ("log begin... wrapped function name: %s"%(fn.__name__))
        fn(*args,**kwargs)
    return wrap
    
    
def retry(x,y):
    x = x*y
    def myretry(fn):
        @wraps(fn)
        def wrap(*args,**kwargs):
            for _ in range(1,x):
                print ("check %s times" % _)
                fn(*args, param=(x, _), **kwargs)
                time.sleep(1)
        return wrap
    return myretry


@log
@retry(2,3)
def pri(x, param=None):
    time_ = time.strftime("%Y-%m-%d %X",time.localtime())
    p = "owner param:%s; execute time: %s; receive param:%s"%(x, time_, param)
    print (p)


    
pri(3)
log begin... wrapped function name: pri
check 1 times
owner param:3; execute time: 2020-01-25 19:46:18; receive param:(6, 1)
check 2 times
owner param:3; execute time: 2020-01-25 19:46:19; receive param:(6, 2)
check 3 times
owner param:3; execute time: 2020-01-25 19:46:20; receive param:(6, 3)
check 4 times
owner param:3; execute time: 2020-01-25 19:46:21; receive param:(6, 4)
check 5 times
owner param:3; execute time: 2020-01-25 19:46:22; receive param:(6, 5)
丟棄裝飾器-解包

最後,如果說我調用函數的時候,只需要函數本身,並不需要其裝飾後的函數,那怎麼辦呢?總不能把別人的代碼去掉裝飾器吧,這會影響影響到別人對這個函數的調用;最好的方法就是使用解包的方式;將函數去掉裝飾性質,稱爲解包

from inspect import unwrap
#解包見參考 https://www.cnblogs.com/blackmatrix/p/6875359.html

def log(fn):
    @wraps(fn)
    def wrap(*args,**kwargs):
        print ("log begin... wrapped function name: %s"%(fn.__name__))
        r = fn(*args,**kwargs)
        return r
    return wrap
    

@log
def pri(x, param=None):
    time_ = time.strftime("%Y-%m-%d %X",time.localtime())
    p = "owner param:%s; execute time: %s; receive param:%s"%(x, time_, param)
    return p


unwrap_pri = unwrap(pri)
r = unwrap_pri(3)
print(r)
owner param:3; execute time: 2020-01-25 19:54:24; receive param:None

但有個前提,裝飾器函數需要注入@wraps(func)來獲取原始需要解包的函數,如以下的方式是解包失效的

from inspect import unwrap


def log(fn):
    def wrap(*args,**kwargs):
        print ("log begin... wrapped function name: %s"%(fn.__name__))
        r = fn(*args,**kwargs)
        return r
    return wrap
    

@log
def pri(x, param=None):
    time_ = time.strftime("%Y-%m-%d %X",time.localtime())
    p = "owner param:%s; execute time: %s; receive param:%s"%(x, time_, param)
    return p


unwrap_pri = unwrap(pri)
r = unwrap_pri(3)
print(r)
log begin... wrapped function name: pri
owner param:3; execute time: 2020-01-25 19:54:17; receive param:None

未完待續…

發佈了97 篇原創文章 · 獲贊 424 · 訪問量 92萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章