Python的装饰器实例用法小结

时间:2021-04-30 22:30:59

这篇文章主要介绍了Python装饰器用法,结合实例形式总结分析了Python常用装饰器的概念、功能、使用方法及相关注意事项

一、装饰器是什么

python的装饰器本质上是一个Python函数,它可以让其他函数在不需要做任何代码变动的前提下增加额外功能,装饰器的返回值也是一个函数对象

它经常用于有切面需求的场景,比如:插入日志、性能测试、事务处理、缓存、权限校验等场景。装饰器是解决这类问题的绝佳设计,有了装饰器,我们就可以抽离出大量与函数功能本身无关的雷同代码并继续重用。

概括的讲,装饰器的作用就是为已经存在的对象添加额外的功能。

二、为什么需要装饰器

  • 1、先来看一个简单例子: ```python

    In [12]: def foo():

    ...: print('I am foo')

    ...:

- 2、增加需求
现在有一个新的需求,希望可以记录下函数的执行日志,于是在代码中添加日志代码:
```python
In [13]: def foo():
...: print('I am foo')
...: print('foo is running')
...:
  • 3、又有需求

    假设现在有100个函数需要增加这个需求,并且后续可能还要对这一百个函数都增加执行前打印日志的需求。此时再一个个改会造成大量雷同的代码。为了减少重复写代码,我们可以这样做,重新定义一个函数:专门处理日志 ,日志处理完之后再执行真正的业务代码。
In [14]: def use_logging(func):
...: print('%s is running' % func.__name__)
...: func()
...: def bar():
...: print('I am bar') ...: use_logging(bar)
...:
#输出:
bar is running
I am bar

通过以上use_logging函数我们增加了日志功能,不管以后有多少函数需要增加日志或者修改日志的格式我们只需要修改use_logging函数,并执行use_logging(被装饰的函数)就达到了我们想要的效果。

In [18]: def use_logging(func):  # use_logging装饰函数
...: print('%s is running' % func.__name__)
...: return func # 使用@ 语法糖要求装饰函数必须return一个函数对象
...:
...: @use_logging # @ 符号作为装饰器的语法糖
...: def bar():
...: print('i am bar')
...: bar()
...:
#输出
bar is running
i am bar

三、基础装饰器入门

  • 1、装饰器语法糖

    Python提供了@ 符号作为装饰器的语法糖,使我们更方便的应用装饰函数。但是用语法糖要求装饰函数必须return一个函数对象。因此我们将上面的func函数使用内嵌函数包裹并return。

装饰器相当于执行了装饰函数use_logging后又返回被装饰函数bar,因此bar()被调用的时候相当于执行了两个函数。等价于use_logging(bar)()

In [19]: def use_logging(func):
...: def _deco():
...: print('%s is running' %func.__name__)
...: func()
...: return _deco
...: In [20]: @use_logging #注意: 把@ use_logging放在bar()函数定义的地方,相当于执行了语句: bar = use_logging(bar)
...: def bar(): #程序首先调用use_logging(bar),得到的返回结果赋值给bar(注意:这里在Python中函数名只是个指向函数首地址的函数指针而已)
...: print(' i am bar')
...: In [21]: bar() # bar()被调用的时候相当于执行了两个函数。等价于use_logging(bar)()
bar is running
i am bar In [22]: bar
Out[22]: <function __main__.use_logging.<locals>._deco>

补充: 装饰器其实就是一个以函数作为参数并返回一个替换函数的可执行函数。让我们从简单的开始,直到能写出实用的装饰器。

In [9]: def outer(some_func):
...: def inner():
...: print('before some_func')
...: ret = some_func() #1
...: return ret+1
...: return inner
...: In [10]: def foo():
...: return 1
...: In [11]: decorated = outer(foo) #2 In [12]: decorated()
before some_func
Out[12]: 2

请仔细看这个装饰器示例。首先,定义了一个带单个参数 some_func 的名为 outer 的函数。然后在 outer 内部定义了一个内嵌函数 inner。inner 函数将打印一行字符串然后调用 some_func,并在 #1 处获取其返回值。在每次 outer 被调用时,some_func 的值可能都会不同,但不论 some_func 是什么函数,都将调用它。最后,inner 返回 some_func() 的返回值加 1。在 #2 处可以看到,当调用赋值给 decorated 的返回函数时,得到的是一行文本输出和返回值 2,而非期望的调用 foo 的返回值 1。

我们可以说变量 decorated 是 foo 的装饰版——即 foo 加上一些东西。事实上,如果写了一个实用的装饰器,可能会想用装饰版来代替 foo,这样就总能得到“附带其他东西”的 foo 版本。用不着学习任何新的语法,通过将包含函数的变量重新赋值就能轻松做到这一点:

In [13]: foo = outer(foo)

In [14]: foo
Out[14]: <function __main__.outer.<locals>.inner> In [15]: foo()
before some_func
Out[15]: 2

现在任意调用 foo() 都不会得到原来的 foo,而是新的装饰器版!

文章来源:简单 12 步理解 Python 装饰器


  • 2、对带参数的函数进行装饰

    现在我们的参数需要传入两个参数并计算值,因此我们需要对内层函数进行改动传入我们的两个参数a和b,等价于use_logging(bar)(1,2)
In [23]: def use_logging(func):
...: def _deco(a,b):
...: print('%s is running' % func.__name__)
...: func(a,b)
...: return _deco
...: In [24]: @use_logging
...: def bar(a,b):
...: print('i am bar: %s' %(a+b))
...: bar(1,2)
...:
bar is running
i am bar: 3

我们装饰的函数可能参数的个数和类型都不一样,每一次我们都需要对装饰器做修改吗?这样做当然是不科学的,因此我们使用Python的变长参数 *args和**kwargs来解决我们的参数问题。

  • 3、函数参数数量不确定

    不带参数装饰器版本,这个格式适用于不带参数的装饰器。

经过以下修改我们已经适应了各种长度和类型的参数。这个版本的装饰器已经可以任意类型的无参数函数。

In [37]: def use_logging(func):
...: def _deco(*args,**kwargs):
...: print('%s is running' % func.__name__)
...: func(*args,**kwargs)
...: return _deco
...: In [38]: @use_logging #不带参数装饰器
...: def bar(a,b):
...: print(' i am bar: %s' %(a+b))
...: In [39]: @use_logging #不带参数装饰器
...: def foo(a,b,c):
...: print(' i am bar: %s' %(a+b+c))
...: In [40]: bar(2,3)
bar is running
i am bar: 5 In [41]: foo(1,4,7)
foo is running
i am bar: 12
  • 4、装饰器带参数

    带参数的装饰器,这个格式适用于带参数的装饰器。

某些情况,我们需要让装饰器带上参数,那就需要编写一个返回一个装饰器的高阶函数,写出来会更复杂。例如:

In [45]: def use_logging(level):
...: def _deco(func):
...: def _deco(*args,**kwargs):
...: if level == 'warn':
...: print('%s is running' % func.__name__)
...: return func(*args,**kwargs)
...: return _deco
...: return _deco
...: In [46]: @use_logging(level = 'warn') #带参数的装饰器
...: def bar(a,b):
...: print('i am bar: %s' %(a+b))
...: In [47]: bar(2,5)
bar is running
i am bar: 7
  • 5、functools.wraps

    使用装饰器极大地复用了代码,但是他有一个缺点就是原函数的元信息不见了,比如函数的docstring__name__参数列表,先看例子:
In [48]: def use_logging(func):
...: def _deco(*args,**kwargs):
...: print('%s is running' %func.__name__)
...: func(*args,**kwargs)
...: return _deco
...: In [49]: @use_logging
...: def bar():
...: print('i am bar')
...: print(bar.__name__)
...: In [50]: bar()
bar is running
i am bar
_deco #函数名变为_deco而不是bar,这个情况在使用反射的特性的时候就会造成问题。因此引入了functools.wraps解决这个问题。

使用functools.warps:

In [54]: import functools
...: def use_logging(func):
...: @functools.wraps(func)
...: def _deco(*args,**kwargs):
...: print("%s is running" % func.__name__)
...: func(*args,**kwargs)
...: return _deco
...: In [55]: @use_logging
...: def bar():
...: print('i am bar')
...: print(bar.__name__)
...: In [56]: bar()
bar is running
i am bar
bar #bar.__name__输出的结果是bar,正是我们想要的。
  • 6、实现带参数和不带参数的装饰器自适应
In [57]: import functools

In [58]: def use_logging(arg):
...: if callable(arg): #判断传入的参数是否是函数,不带参数的装饰器调用这个分支
...: @functools.wraps(arg)
...: def _deco1(*args,**kwargs):
...: print('%s is running' %arg.__name__)
...: arg(*args,**kwargs)
...: return _deco1
...: else: #带参数的装饰器调用这个分支
...: def _deco1(func):
...: @functools.wraps(func)
...: def _deco2(*args,**kwargs):
...: if arg == 'warn':
...: print('warn %s is running' %func.__name__)
...: return func(*args,**kwargs)
...: return _deco2
...: return _deco1
...: In [59]: @use_logging('warn') #带有参数的装饰器执行else分支语句
...: def bar():
...: print('i am bar')
...: print(bar.__name__)
...: In [60]: bar()
warn bar is running #执行的是print('warn %s is running' %func.__name__)
i am bar
bar In [61]: @use_logging #不带参数的装饰器执行if分支语句
...: def bar():
...: print('i am bar')
...: print(bar.__name__)
...: In [62]: bar()
bar is running #执行的是 print("%s is running" % arg.__name__)
i am bar
bar

三、类装饰器

使用类装饰器可以实现带参数装饰器的效果,但实现的更加优雅简洁,而且可以通过继承来灵活的扩展。

  • 1、类装饰器
In [68]:   class loging(object):  #类装饰器
...: def __init__(self,level="warn"):
...: self.level = level
...:
...: def __call__(self,func):
...: @functools.wraps(func)
...: def _deco(*args, **kwargs):
...: if self.level == "warn":
...: self.notify(func)
...: return func(*args, **kwargs)
...: return _deco
...:
...: def notify(self,func):
...: # loging只打日志,不做别的
...: print ("%s is running" % func.__name__)
...:
...:
...: @loging(level="warn") #执行__call__方法
...: def bar(a,b):
...: print('i am bar:%s'%(a+b)) In [69]: bar(1,3)
bar is running
i am bar:4
  • 2、继承扩展类装饰器
In [72]: class email_loging(loging):   #括号内的loging参数为上面的loging类装饰器,email_loging继承并扩展了loging类装饰器
...: """
...: 一个loging的实现版本,可以在函数调用时发送email给管理员
...: """
...: def __init__(self,email = 'admin = chj@ncu.edu.com',*args,**kwargs):
...: self.email = email
...: super(email_loging,self).__init__(*args,**kwargs)
...:
...: def notify(self,func):
...: #发送一份email到self.email
...: print('%s is running' %func.__name__)
...: print('sending email to %s' %self.email)
...: In [73]: @email_loging(level = 'warn')
...: def bar(a,b):
...: print('i am bar: %s' %(a+b))
...: In [74]: bar(1,3)
bar is running
sending email to admin = chj@ncu.edu.com
i am bar: 4

更多参考:

  1. 知乎:如何理解Python装饰器?
  2. 伯乐在线:简单 12 步理解 Python 装饰器
  3. Python装饰器用法实例总结
  4. Python装饰器原理与简单用法实例分析