装饰器(Decorators
)是 Python
的一个重要部分。简单地说:他们是修改其他函数的功能的函数。他们有助于让我们的代码更简短,也更Pythonic
(Python
范儿)。
其主要作用就是在不改变原有函数代码的前提下,给该函数添加新的功能
什么是闭包 在函数嵌套的前提下,内部函数使用了外部函数的变量,并且外部函数返回了内部函数,我们把这个使用外部函数变量的内部函数称为闭包 。
闭包的构成条件 通过闭包的定义,我们可以得知闭包的形成条件:
在函数嵌套(函数里面再定义函数)的前提下 内部函数使用了外部函数的变量(还包括外部函数的参数) 外部函数返回了内部函数 闭包的作用 闭包可以保存外部函数内的变量,不会随着外部函数调用完而销毁。 注意点:
由于闭包引用了外部函数的变量,则外部函数的变量没有及时释放,消耗内存。 原始代码 原始要求就是一台电脑,功能是可以播放音乐,如果我们在不改变computer
函数原有代码的情况下让电脑有其他功能该如何操作?
1 2 3 4 5 def computer (): print ('我可以播放音乐' ) computer()
使用其他函数调用 1 2 3 4 5 6 7 8 9 def computer (): print ('我可以播放音乐' ) def extend (): print ('我可以编写代码' ) computer() extend()
该方式确实没有改变原有函数代码,但是却改变了直接运行的函数,在一个项目中我们可能有很多地方要调用computer
,如果这样改写势必要将之前所有调用computer
的地方都要改成extend
进阶 我们是否可以定义一个叫computer
的变量来接收extend
,然后在执行computer
,被调用的地方不是就不用修改了?我们尝试下
1 2 3 4 5 6 7 8 9 10 11 12 13 def computer (): print ('我可以播放音乐' ) def extend (): print ('我可以编写代码' ) computer() computer = extend computer()
执行代码我们看下结果:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 我可以编写代码 我可以编写代码 我可以编写代码 我可以编写代码 我可以编写代码 我可以编写代码 我可以编写代码 我可以编写代码 我可以编写代码 我可以编写代码 我可以编写代码 我可以编写代码 我可以编写代码 我可以编写代码 我可以编写代码 我可以编写代码 我可以编写代码 我可以编写代码 我可以编写代码 我可以编写代码 我可以编写代码 我可以编写代码 我可以编写代码 我可以编写代码 我可以编写代码 我可以编写代码 我可以编写代码 我可以编写代码 我可以编写代码 我可以编写代码 我可以编写代码 Traceback (most recent call last): File "E:/test.py", line 9, in <module> computer() File "E:/test.py", line 7, in extend computer() File "E:/test.py", line 7, in extend computer() File "E:/test.py", line 7, in extend computer() [Previous line repeated 993 more times] File "E:/test.py", line 6, in extend print('我可以编写代码') RecursionError: maximum recursion depth exceeded while calling a Python object
很显然这样是有问题的,computer
的值是extend
,extend
内部又运行了一个computer
,等于运行了extend
,自己运行自己,死循环了。
使用闭包 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 def computer (): print ('我可以播放音乐' ) def extend (func ): def method (): print ('我可以编写代码' ) func() return method computer = extend(computer) computer()
修改闭包内使用的外部变量 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 def func_out (num1 ): def func_inner (num2 ): num1 = 99 res = num1 + num2 print ("最后的结果是:" , res) print ("num1初始:" , num1) func_inner(8 ) print ("num1修改后:" , num1) return func_inner func = func_out(66 ) func(33 )
以上代码运行结果来看,目的是达到了,但是自己观察发现我们在func_inner中修改的num1变量,没有修改成功。有人会讲了,那是因为你使用外部变量没有使用global,所以相当于在内部函数重新定义了一个局部变量num1,如你所愿我们来修改并运行下代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 def func_out (num1 ): def func_inner (num2 ): global num1 num1 = 99 res = num1 + num2 print ("最后的结果是:" , res) print ("num1初始:" , num1) func_inner(8 ) print ("num1修改后:" , num1) return func_inner func = func_out(66 ) func(33 )
运行结果:
1 2 3 4 5 6 num1初始: 66 最后的结果是: 107 num1修改后: 66 最后的结果是: 132 Process finished with exit code 0
num1外部变量依然没被改掉,这是因为修改闭包内使用的外部函数变量使用 nonlocal 关键字来完成。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 def func_out (num1 ): def func_inner (num2 ): nonlocal num1 num1 = 99 res = num1 + num2 print ("最后的结果是:" , res) print ("num1初始:" , num1) func_inner(8 ) print ("num1修改后:" , num1) return func_inner func = func_out(66 ) func(33 )
运行结果:
1 2 3 4 num1初始: 66 最后的结果是: 107 num1修改后: 99 最后的结果是: 132
这时候才终于修改掉了外部变量。
带参数 如果原函数是需要传递参数该如何处理呢?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 def computer (name, something ): print (f'我叫{name} 可以{something} ' ) def extend (func ): def method (): print ('我可以编写代码' ) func() return method computer = extend(computer) computer('银河1号' , '播放音乐' )
运行结果
method没有接受参数,但是你却给了我两个参数
1 2 3 4 Traceback (most recent call last): File "E:/test.py", line 16, in <module> computer('银河1号', '播放音乐') TypeError: method() takes 0 positional arguments but 2 were given
改进 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 def computer (name, something ): print (f'我叫{name} 可以{something} ' ) def extend (func ): def method (*args, **kwargs ): print ('我可以编写代码' ) func(*args, **kwargs) return method computer = extend(computer) computer('银河1号' , '播放音乐' )
如此我们即完成了对原有函数功能的扩展,又使其能正确的使用参数
返回结果 上述代码如果我们要获取computer的返回值,该如何处理
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 def computer (name, something ): print (f'我叫{name} 可以{something} ' ) return '我是computer原有代码的返回结果' def extend (func ): def method (*args, **kwargs ): print ('我可以编写代码' ) func(*args, **kwargs) return method computer = extend(computer) res = computer('银河1号' , '播放音乐' ) print (res)
运行发现最后返回的是None,因为原有扩展功能函数中method中执行了原有的computer函数,但是却没有返回该函数的结果
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 def computer (name, something ): print (f'我叫{name} 可以{something} ' ) return '我是computer原有代码的返回结果' def extend (func ): def method (*args, **kwargs ): print ('我可以编写代码' ) return func(*args, **kwargs) return method computer = extend(computer) res = computer('银河1号' , '播放音乐' ) print (res)
使用装饰器语法糖 使用语法糖其实就是省略了上述代码中的15行,简写后的代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 def extend (func ): def method (*args, **kwargs ): print ('我可以编写代码' ) return func(*args, **kwargs) return method @extend def computer (name, something ): print (f'我叫{name} 可以{something} ' ) return '我是computer原有代码的返回结果' res = computer('银河1号' , '播放音乐' ) print (res)
自定义装饰器规则 适用场景,上例中我们的computer
有时让其拥有功能A,有时需要拥有功能B,甚至有时候我们只要其原有功能。简单来说就是想要看电影就给装一个电影播放器,想要写代码就给你装一个IDE
,或者我什么都不想要了,就想要初始化的功能。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 def wraper (flag ): def extend (func ): def method (*args, **kwargs ): if flag == 1 : print ('我可以编写代码' ) return func(*args, **kwargs) elif flag == 2 : print ('我可以上网冲浪' ) return func(*args, **kwargs) else : return func(*args, **kwargs) return method return extend @wraper(3 ) def computer (name, something ): print (f'我叫{name} ,我的原始功能可以{something} ' ) return '我是computer原有代码的返回结果' res = computer('银河1号' , '播放音乐' ) print (res)
多个装饰器 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 def extend (func ): def method (*args, **kwargs ): print ('我可以编写代码' ) return func(*args, **kwargs) return method def extend2 (func ): def method (*args, **kwargs ): print ('我可以哈哈哈笑一整天的功能' ) return func(*args, **kwargs) return method @extend @extend2 def computer (name, something ): print (f'我叫{name} 可以{something} ' ) return '我是computer原有代码的返回结果' res = computer('银河1号' , '播放音乐' ) print (res)
执行结果:
1 2 3 4 我可以编写代码 我可以哈哈哈笑一整天的功能 我叫银河1号可以播放音乐 我是computer原有代码的返回结果
装饰器 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 import timedef decorator (func ): print ("我用来计算函数用时" ) def inner (): start = time.time() func() print ("用时:" , time.time()-start) return inner @decorator def run (): for item in range (100000 ): print (item) if __name__ == '__main__' : pass
运行结果:
注意:
装饰器的执行时间是加载模块时立即执行 ,这意味着只要加载了带装饰器的模块,即使不运行被装饰的函数,装饰器也运行了
装饰带有参数的函数 这里需求是,函数要输出错误编码和错误信息,装饰器负责将这些内容写入日志。
1 2 3 4 5 6 7 8 9 10 11 12 13 def logging (func ): def inner (code, msg ): res = func(code, msg) print ("错误信息已写入日志:【%s】" % res) return inner @logging def show (err, msg ): """带参数的函数""" return '错误代码:%s;错误提示:%s' % (err, msg) show('DBERROR' , '数据库出错了' )
输出结果:
1 2 3 错误信息已写入日志:【错误代码:DBERROR;错误提示:数据库出错了】 Process finished with exit code 0
装饰带有不确定参数个数的函数 同样是上个问题,加入我们不知道要被装饰的函数的个数是几个,或者说,我们就想写个通用装饰器,传几个参数都可以,我们可以使用python基础里的不定长参数元组*args和**kwagrs
1 2 3 4 5 6 7 8 9 10 11 12 13 def logging (func ): def inner (*args, **kwargs ): res = func(*args, **kwargs) print ("错误信息已写入日志:【%s】" % res) return inner @logging def show (err, msg ): """带参数的函数""" return '错误代码:%s;错误提示:%s' % (err, msg) show('DBERROR' , '数据库出错了' )
输出结果跟上面的一样
所以我们总结下通用装饰器的语法可以如下方式书写:
1 2 3 4 5 6 7 8 9 10 def func_out (fn ): def func_inner (*args, **kwargs ): print ("--被装饰的函数执行前要做的操作--" ) result = fn(*args, **kwargs) print ("--被装饰的函数执行后要做的操作--" ) return result return inner
使用多个装饰器 需求,我们想给某段文字添加固定的标签,如加入p段落,加上div容器。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 def make_div (func ): """添加div标签""" def func_inner (): res = func() return "<div>%s</div>" % res return func_inner def make_p (func ): """添加p标签""" def func_inner (): res = func() return "<p>%s</p>" % res return func_inner @make_div @make_p def show (): """带参数的函数""" return '我就是简单一句话,怎么了?' print (show())
运行结果:
1 2 3 <div><p>我就是简单一句话,怎么了?</p></div> Process finished with exit code 0
总结:多个装饰器的装饰过程是: 离函数最近的装饰器先装饰,然后外面的装饰器再进行装饰,由内到外的装饰过程
类装饰器 这里需求是,函数要输出错误编码和错误信息,装饰器负责将这些内容写入日志。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 class Logging (object ): def __init__ (self, func ): self.__func = func def __call__ (self, *args, **kwargs ): print ('装饰前的操作' ) res = self.__func(*args, **kwargs) print ("错误信息已写入日志:【%s】" % res) @Logging def show (err, msg ): """带参数的函数""" return '错误代码:%s;错误提示:%s' % (err, msg) show('DBERROR' , '数据库出错了' )
运行结果:
1 2 3 4 装饰前的操作 错误信息已写入日志:【错误代码:DBERROR;错误提示:数据库出错了】 Process finished with exit code 0
从结果看和我们刚才使用的函数装饰器能同样达到目的。
结论 想要让类的实例对象能够像函数一样进行调用,需要在类里面使用call 方法,把类的实例变成可调用对象(callable) 类装饰器装饰函数功能在call 方法里面进行添加 带有参数的装饰器 带有参数的装饰器就是使用装饰器装饰函数的时候可以传入指定参数,语法格式: @装饰器(参数,…)
我们先按照自己的思路写下带参数的装饰器
1 2 3 4 5 6 7 8 9 10 11 12 13 def logging (func, admin ): def inner (*args, **kwargs ): res = func(*args, **kwargs) print ("错误信息已写入日志:【%s】。操作人:%s" % (res, admin)) return inner @logging('张三' ) def show (err, msg ): """带参数的函数""" return '错误代码:%s;错误提示:%s' % (err, msg) show('DBERROR' , '数据库出错了' )
结果:
1 2 3 4 5 6 Traceback (most recent call last): File "/Users/tony/PycharmProjects/test_django/装饰器.py", line 244, in <module> @logging('张三') TypeError: logging() missing 1 required positional argument: 'admin' Process finished with exit code 1
为什么错误?装饰器只能接收一个参数,并且还是函数类型。 后面我们演示正确的写法。
函数装饰器 解决方案:在装饰器外面再包裹上一个函数,让最外面的函数接收参数,返回的是装饰器,因为@符号后面必须是装饰器实例。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 def decorator (admin ): def logging (func ): def inner (*args, **kwargs ): res = func(*args, **kwargs) print ("错误信息已写入日志:【%s】。操作人:%s" % (res, admin)) return inner return logging @decorator('张三' ) def show (err, msg ): """带参数的函数""" return '错误代码:%s;错误提示:%s' % (err, msg) show('DBERROR' , '数据库出错了' )
运行结果:
1 2 3 错误信息已写入日志:【错误代码:DBERROR;错误提示:数据库出错了】。操作人:张三 Process finished with exit code 0
总结:使用带有参数的装饰器,其实是在装饰器外面又包裹了一个函数,使用该函数接收参数,返回是装饰器,因为 @ 符号需要配合装饰器实例使用
类装饰器 我们依照函数装饰器加参数的方式来写下类装饰器.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 class Logging (object ): def __init__ (self, author ): self.author = author def __call__ (self, func ): def decorator (*args, **kwargs ): print ('装饰前的操作' ) res = func(*args, **kwargs) print ("错误信息已写入日志:【%s】,操作人:%s" % (res, self.author)) return decorator @Logging('李四' ) def show (err, msg ): """带参数的函数""" return '错误代码:%s;错误提示:%s' % (err, msg) show('DBERROR' , '数据库出错了' )
运行结果:
1 2 3 4 装饰前的操作 错误信息已写入日志:【错误代码:DBERROR;错误提示:数据库出错了】,操作人:李四 Process finished with exit code 0
总结,从带参数的类装饰器看,本质上跟函数装饰器一样,都借助了一个外部函数,我们在类里使用的是__call__
来作为外部函数使用了。