从作用域到闭包再到装饰器

首先预祝抗击在武汉肺炎第一线的兄弟姐妹们平安归来,撑住!你们辛苦啦!没事儿有我们共渡难关!


写在开头

最近装饰器用的比较多,由此想到了闭包,再往上就是作用域的一些陷阱问题;索性记录一下,类似于一个散文形式的,只是对自己的一些想法和网上的一些资料进行梳理,想法 -> 验证 -> 查实 仅此而已,工作时候零零散散,现在拾掇一下,作为总结,希望对你有所帮助~ 新年快乐!

一些显而易见的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)    
._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))
._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))
._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内部没有,只有往上层找
._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))
._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)

 in ()
     14 print("局部变量:",h.__code__.co_varnames)  # 局部变量
     15 print("自由变量:",h.__code__.co_freevars)  # 自由变量
---> 16 print(h(3))
 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)

 in ()
      8 print("局部变量:",bar.__code__.co_varnames)  # 局部变量
      9 print("自由变量:",bar.__code__.co_freevars)  # 自由变量
---> 10 bar = bar(3)
     11 
     12 # 编译函数体时,发现b被赋值(b=9),所以不采用向上找变量 b = 3,当真要启动的时候,发现要用b,却找不到值
      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)

 in ()
     12 print("局部变量:",h.__code__.co_varnames)  # 局部变量
     13 print("自由变量:",h.__code__.co_freevars)  # 自由变量
---> 14 print(h(3))
 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__])
._help at 0x10441c158> [3, 4]
._help at 0x10441cf28> [3, 4]
._help at 0x10441cd08> [3, 4]
._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']
._help at 0x1045ea730> [3, 4]
._help at 0x1045ea620> [3, 4]
._help at 0x1045ead08> [3, 4]
._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)]
._help at 0x10b9f48c8> [3, 4]
._help at 0x10b9f4a60> [3, 4]
._help at 0x10b9f4378> [3, 4]
._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)]
._help at 0x10bb1e840> [4]
._help at 0x10bb1e9d8> [4]
._help at 0x10bb1e378> [4]
._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()
.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__]) 
# [, 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
[, 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(, 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(, x=4, y=3)
[, 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)
--------------
[, 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
使用类装饰器

上述例子我们知道使用装饰器的核心是:Python中万物皆对象,即使是数字、字符串、函数、方法、类、模块,所以能够本能的将方法当做参数传递进入一个方法中进行处理,回顾下,左右两边等效,只是python使用@语法糖的方式进行简化了
从作用域到闭包再到装饰器_第1张图片
那么是否存在一个方法,让类也能够像函数一样调用呢?有的,__call__;关于__call__这里不做展开,详见这里


class Foo(object):
    def __init__(self,age=None):
        self.age = age
        print("init")
        
    def __call__(self,*args,**kwargs):
        print("call param: %s; init param:%s" % (args[0], self.age))

foo = Foo(age=23)
print("-"*20)
foo(34)

init
--------------------
call param: 34; init param:23

既然可以从将一个实例进行函数一样的调用,那么再配合上装饰器的语法性质,是否可以实现类装饰器呢?答案当然是可以的

从作用域到闭包再到装饰器_第2张图片
下面来看两个综合的例子


import time
class Log(object):
    
    def __init__(self):
        
        self.count = 0
        self.clock = time.strftime("%Y-%m-%d %X",time.localtime())
        print ("Log begin... init time: %s"%(self.clock))   
    
    def __call__(self, func):
        '''
        __call__的作用:将类的实例化作为像函数一样可以被调用和传参
        '''
        
        def wrap(*args,**kwargs):
            self.count +=1
            print("__call__ execu time: %s" % self.count)
            self.execu_log()
            return func(*args,count=self.count,**kwargs)
        return wrap

    
    def execu_log(self):
        call_clock = time.strftime("%Y-%m-%d %X",time.localtime())
        print("exec log: %s" % call_clock)

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

for _ in range(5):
    time.sleep(1)
    print(pri(_))
    print("-"*50)

Log begin... init time: 2020-01-10 11:16:32
__call__ execu time: 1
exec log: 2020-01-10 11:16:33
owner param:0; execute time: 2020-01-10 11:16:33; receive param:None; call log class:1 times
--------------------------------------------------
__call__ execu time: 2
exec log: 2020-01-10 11:16:34
owner param:1; execute time: 2020-01-10 11:16:34; receive param:None; call log class:2 times
--------------------------------------------------
__call__ execu time: 3
exec log: 2020-01-10 11:16:35
owner param:2; execute time: 2020-01-10 11:16:35; receive param:None; call log class:3 times
--------------------------------------------------
__call__ execu time: 4
exec log: 2020-01-10 11:16:36
owner param:3; execute time: 2020-01-10 11:16:36; receive param:None; call log class:4 times
--------------------------------------------------
__call__ execu time: 5
exec log: 2020-01-10 11:16:37
owner param:4; execute time: 2020-01-10 11:16:37; receive param:None; call log class:5 times
--------------------------------------------------    

如果要在原有的基础上进行扩展,也非常简单,使用类的继承就可以了,不仅保留原来的方法,还可以增加新的方法


import time
class Log(object):
    
    def __init__(self):
        
        self.count = 0
        self.clock = time.strftime("%Y-%m-%d %X",time.localtime())
        print ("Log begin... init time: %s"%(self.clock))   
    
    def __call__(self, func):
        '''
        __call__的作用:将类的实例化作为像函数一样可以被调用和传参
        '''
        
        def wrap(*args,**kwargs):
            self.count +=1
            print("__call__ execu time: %s" % self.count)
            self.execu_log()
            return func(*args,count=self.count,**kwargs)
        return wrap

    
    def execu_log(self):
        call_clock = time.strftime("%Y-%m-%d %X",time.localtime())
        print("exec log: %s" % call_clock)

        
# 使用继承的方法,很容易对已有的记录日志的类添加一些新的功能来进行扩展      
class SubLog(Log):
    
    def __init__(self,email_addr="[email protected]"):
        super().__init__()
        self.email_addr = email_addr
    
    
    def execu_log(self):
        super().execu_log()
        print("send email to :%s" % self.email_addr)

        
        
@SubLog(email_addr = "[email protected]")
def pri(x, count=None, param=None):
    time_ = time.strftime("%Y-%m-%d %X",time.localtime())
    p = "owner param:%s; execute time: %s; receive param:%s; call log class:%s times"%(x, time_, param, count)
    return p

for _ in range(5):
    time.sleep(1)
    print(pri(_))
    print("-"*50)

Log begin... init time: 2020-01-10 11:16:10
__call__ execu time: 1
exec log: 2020-01-10 11:16:11
send email to :husky@gmail.com
owner param:0; execute time: 2020-01-10 11:16:11; receive param:None; call log class:1 times
--------------------------------------------------
__call__ execu time: 2
exec log: 2020-01-10 11:16:12
send email to :husky@gmail.com
owner param:1; execute time: 2020-01-10 11:16:12; receive param:None; call log class:2 times
--------------------------------------------------
__call__ execu time: 3
exec log: 2020-01-10 11:16:13
send email to :husky@gmail.com
owner param:2; execute time: 2020-01-10 11:16:13; receive param:None; call log class:3 times
--------------------------------------------------
__call__ execu time: 4
exec log: 2020-01-10 11:16:14
send email to :husky@gmail.com
owner param:3; execute time: 2020-01-10 11:16:14; receive param:None; call log class:4 times
--------------------------------------------------
__call__ execu time: 5
exec log: 2020-01-10 11:16:15
send email to :husky@gmail.com
owner param:4; execute time: 2020-01-10 11:16:15; receive param:None; call log class:5 times
--------------------------------------------------


你可能感兴趣的:(Python基础,Python碎片)