Python中级知识之 装饰器介绍

举报
nineteens 发表于 2021/03/09 16:15:00 2021/03/09
【摘要】 Python中级知识之装饰器介绍

  七、函数装饰器

  装饰器(Decorators)在 Python 中,主要作用是修改函数的功能,而且修改前提是不变动原函数代码,装饰器会返回一个函数对象,所以有的地方会把装饰器叫做 “函数的函数”。

  还存在一种设计模式叫做 “装饰器模式”,这个后续的课程会有所涉及。

  装饰器调用的时候,使用 @,它是 Python 提供的一种编程语法糖,使用了之后会让你的代码看起来更加 Pythonic。

  7.1 装饰器基本使用

  在学习装饰器的时候,最常见的一个案例,就是统计某个函数的运行时间,接下来就为你分享一下。

  计算函数运行时间:

  import time

  def fun():

  i = 0

  while i < 1000:

  i += 1

  def fun1():

  i = 0

  while i < 10000:

  i += 1

  s_time = time.perf_counter()

  fun()

  e_time = time.perf_counter()

  print(f"函数{fun.__name__}运行时间是:{e_time-s_time}")

  如果你希望给每个函授都加上调用时间,那工作量是巨大的,你需要重复的修改函数内部代码,或者修改函数调用位置的代码。在这种需求下,装饰器语法出现了。

  先看一下第一种修改方法,这种方法没有增加装饰器,但是编写了一个通用的函数,利用 Python 中函数可以作为参数这一特性,完成了代码的可复用性。

  import time

  def fun():

  i = 0

  while i < 1000:

  i += 1

  def fun1():

  i = 0

  while i < 10000:

  i += 1

  def go(fun):

  s_time = time.perf_counter()

  fun()

  e_time = time.perf_counter()

  print(f"函数{fun.__name__}运行时间是:{e_time-s_time}")

  if __name__ == "__main__":

  go(fun1)

  接下来这种技巧扩展到 Python 中的装饰器语法,具体修改如下:

  import time

  def go(func):

  # 这里的 wrapper 函数名可以为任意名称

  def wrapper():

  s_time = time.perf_counter()

  func()

  e_time = time.perf_counter()

  print(f"函数{func.__name__}运行时间是:{e_time-s_time}")

  return wrapper

  @go

  def func():

  i = 0

  while i < 1000:

  i += 1

  @go

  def func1():

  i = 0

  while i < 10000:

  i += 1

  if __name__ == '__main__':

  func()

  在上述代码中,注意看 go 函数部分,它的参数 func 是一个函数,返回值是一个内部函数,执行代码之后相当于给原函数注入了计算时间的代码。在代码调用部分,你没有做任何修改,函数 func 就具备了更多的功能(计算运行时间的功能)。

  装饰器函数成功拓展了原函数的功能,又不需要修改原函数代码,这个案例学会之后,你就已经初步了解了装饰器。

  7.2 对带参数的函数进行装饰

  直接看代码,了解如何对带参数的函数进行装饰:

  import time

  def go(func):

  def wrapper(x, y):

  s_time = time.perf_counter()

  func(x, y)

  e_time = time.perf_counter()

  print(f"函数{func.__name__}运行时间是:{e_time-s_time}")

  return wrapper

  @go

  def func(x, y):

  i = 0

  while i < 1000:

  i += 1

  print(f"x={x},y={y}")

  if __name__ == '__main__':

  func(33, 55)

  如果你看着晕乎了,我给你标记一下参数的重点传递过程。

  还有一种情况是装饰器本身带有参数,例如下述代码:

  def log(text):

  def decorator(func):

  def wrapper(x):

  print('%s %s():' % (text, func.__name__))

  func(x)

  return wrapper

  return decorator

  @log('执行')

  def my_fun(x):

  print(f"我是 my_fun 函数,我的参数 {x}")

  my_fun(123)

  上述代码在编写装饰器函数的时候,在装饰器函数外层又嵌套了一层函数,最终代码的运行顺序如下所示:

  my_fun = log('执行')(my_fun)

  此时如果我们总结一下,就能得到结论了:使用带有参数的装饰器,是在装饰器外面又包裹了一个函数,使用该函数接收参数,并且返回一个装饰器函数。

  还有一点要注意的是装饰器只能接收一个参数,而且必须是函数类型。

  7.3 多个装饰器

  先临摹一下下述代码,再进行学习与研究。

  import time

  def go(func):

  def wrapper(x, y):

  s_time = time.perf_counter()

  func(x, y)

  e_time = time.perf_counter()

  print(f"函数{func.__name__}运行时间是:{e_time-s_time}")

  return wrapper

  def gogo(func):

  def wrapper(x, y):

  print("我是第二个装饰器")

  return wrapper

  @go

  @gogo

  def func(x, y):

  i = 0

  while i < 1000:

  i += 1

  print(f"x={x},y={y}")

  if __name__ == '__main__':

  func(33, 55)

  代码运行之后,输出结果为:

  我是第二个装饰器

  函数wrapper运行时间是:0.0034401339999999975

  虽说多个装饰器使用起来非常简单,但是问题也出现了,print(f"x={x},y={y}") 这段代码运行结果丢失了,这里就涉及多个装饰器执行顺序问题了。

  先解释一下装饰器的装饰顺序。

  import time

  def d1(func):

  def wrapper1():

  print("装饰器1开始装饰")

  func()

  print("装饰器1结束装饰")

  return wrapper1

  def d2(func):

  def wrapper2():

  print("装饰器2开始装饰")

  func()

  print("装饰器2结束装饰")

  return wrapper2

  @d1

  @d2

  def func():

  print("被装饰的函数")

  if __name__ == '__main__':

  func()

  上述代码运行的结果为:

  装饰器1开始装饰

  装饰器2开始装饰

  被装饰的函数

  装饰器2结束装饰

  装饰器1结束装饰

  可以看到非常对称的输出,同时证明被装饰的函数在最内层,转换成函数调用的代码如下:

  d1(d2(func))

  你在这部分需要注意的是,装饰器的外函数和内函数之间的语句,是没有装饰到目标函数上的,而是在装载装饰器时的附加操作。

  在对函数进行装饰的时候,外函数与内函数之间的代码会被运行。

  测试效果如下:

  import time

  def d1(func):

  print("我是 d1 内外函数之间的代码")

  def wrapper1():

  print("装饰器1开始装饰")

  func()

  print("装饰器1结束装饰")

  return wrapper1

  def d2(func):

  print("我是 d2 内外函数之间的代码")

  def wrapper2():

  print("装饰器2开始装饰")

  func()

  print("装饰器2结束装饰")

  return wrapper2

  @d1

  @d2

  def func():

  print("被装饰的函数")

  运行之后,你就能发现输出结果如下:

  我是 d2 内外函数之间的代码

  我是 d1 内外函数之间的代码

  d2 函数早于 d1 函数运行。

  接下来在回顾一下装饰器的概念:

  被装饰的函数的名字会被当作参数传递给装饰函数。

  装饰函数执行它自己内部的代码后,会将它的返回值赋值给被装饰的函数。

  这样看上文中的代码运行过程是这样的,d1(d2(func)) 执行 d2(func) 之后,原来的 func 这个函数名会指向 wrapper2 函数,执行 d1(wrapper2) 函数之后,wrapper2 这个函数名又会指向 wrapper1。因此最后的 func 被调用的时候,相当于代码已经切换成如下内容了。

  # 第一步

  def wrapper2():

  print("装饰器2开始装饰")

  print("被装饰的函数")

  print("装饰器2结束装饰")

  # 第二步

  print("装饰器1开始装饰")

  wrapper2()

  print("装饰器1结束装饰")

  # 第三步

  def wrapper1():

  print("装饰器1开始装饰")

  print("装饰器2开始装饰")

  print("被装饰的函数")

  print("装饰器2结束装饰")

  print("装饰器1结束装饰")

  上述第三步运行之后的代码,恰好与我们的代码输出一致。

  那现在再回到本小节一开始的案例,为何输出数据丢失掉了。

  import time

  def go(func):

  def wrapper(x, y):

  s_time = time.perf_counter()

  func(x, y)

  e_time = time.perf_counter()

  print(f"函数{func.__name__}运行时间是:{e_time-s_time}")

  return wrapper

  def gogo(func):

  def wrapper(x, y):

  print("我是第二个装饰器")

  return wrapper

  @go

  @gogo

  def func(x, y):

  i = 0

  while i < 1000:

  i += 1

  print(f"x={x},y={y}")

  if __name__ == '__main__':

  func(33, 55)

  在执行装饰器代码装饰之后,调用 func(33,55) 已经切换为 go(gogo(func)),运行 gogo(func) 代码转换为下述内容:

  def wrapper(x, y):

  print("我是第二个装饰器")

  在运行 go(wrapper),代码转换为:

  s_time = time.perf_counter()

  print("我是第二个装饰器")

  e_time = time.perf_counter()

  print(f"函数{func.__name__}运行时间是:{e_time-s_time}")

  此时,你会发现参数在运行过程被丢掉了。

  7.4 functools.wraps大连人流医院 http://mobile.84211111.cn/

  使用装饰器可以大幅度提高代码的复用性,但是缺点就是原函数的元信息丢失了,比如函数的 __doc__、__name__:

  # 装饰器

  def logged(func):

  def logging(*args, **kwargs):

  print(func.__name__)

  print(func.__doc__)

  func(*args, **kwargs)

  return logging

  # 函数

  @logged

  def f(x):

  """函数文档,说明"""

  return x * x

  print(f.__name__) # 输出 logging

  print(f.__doc__) # 输出 None

  解决办法非常简单,导入 from functools import wraps ,修改代码为下述内容:

  from functools import wraps

  # 装饰器

  def logged(func):

  @wraps(func)

  def logging(*args, **kwargs):

  print(func.__name__)

  print(func.__doc__)

  func(*args, **kwargs)

  return logging

  # 函数

  @logged

  def f(x):

  """函数文档,说明"""

  return x * x

  print(f.__name__) # 输出 f

  print(f.__doc__) # 输出 函数文档,说明

  7.5 基于类的装饰器

  在实际编码中 一般 “函数装饰器” 最为常见,“类装饰器” 出现的频率要少很多。

  基于类的装饰器与基于函数的基本用法一致,先看一段代码:

  class H1(object):

  def __init__(self, func):

  self.func = func

  def __call__(self, *args, **kwargs):

  return '

  ' + self.func(*args, **kwargs) + '

  '

  @H1

  def text(name):

  return f'text {name}'

  s = text('class')

  print(s)

  类 H1 有两个方法:

  __init__:接收一个函数作为参数,就是待被装饰的函数;

  __call__:让类对象可以调用,类似函数调用,触发点是被装饰的函数调用时触发。

  最后在附录一篇写的不错的 博客,可以去学习。

  在这里类装饰器的细节就不在展开了,等到后面滚雪球相关项目实操环节再说。

  装饰器为类和类的装饰器在细节上是不同的,上文提及的是装饰器为类,你可以在思考一下如何给类添加装饰器。

  7.6 内置装饰器

  常见的内置装饰器有 @property、@staticmethod、@classmethod。该部分内容在细化面向对象部分进行说明,本文只做简单的备注。

  7.6.1 @property

  把类内方法当成属性来使用,必须要有返回值,相当于 getter,如果没有定义 @func.setter 修饰方法,是只读属性。

  7.6.2 @staticmethod

  静态方法,不需要表示自身对象的 self 和自身类的 cls 参数,就跟使用函数一样。

  7.6.3 @classmethod

  类方法,不需要 self 参数,但第一个参数需要是表示自身类的 cls 参数。

【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。