资讯专栏INFORMATION COLUMN

python奇遇记:深入理解装饰器

lemon / 2390人阅读

摘要:可见装饰器改变了函数的功能。装饰器除了改变函数功能之外还有一个特性是,函数装饰器在导入模块时立即执行,而被装饰的函数只在明确调用时运行。

什么是装饰器

装饰器是什么,简单来说,装饰器可以改变一个函数的行为,比如原本有一个函数用来计算菲波那切数列,我们给这个函数加个计算执行时间的装饰器,这样原来的函数不仅能够计算菲波那切数列,而且还可以输出计算花费了多少时间。

在Python中,有几个很常见的内置装饰器:比如@staticmethod, 它可以将一个类的方法声明为静态的。@property, 为类中的变量设置get和set方法,保证了封装性。

如果你使用过python的web框架(比如flask)开发过网站,你应该经常会见到装饰器,像下面这样:

</>复制代码

  1. @app.route("/")
  2. def hello():
  3. return "Hello World!"

这段代码把路由绑定到hello函数上,这样你输入网址之后就可以看到Hello World

先来看个很简单的例子:

</>复制代码

  1. # 定义了一个装饰器
  2. def deco(func):
  3. def hah():
  4. print("hahha")
  5. return hah

上面我们定义了一个装饰器,打印hahah,接下来使用:

</>复制代码

  1. # 使用这个装饰器
  2. @deco
  3. def lal():
  4. pritn("lalalala")
  5. lal()

执行lal()会输出hahha。 可见deco装饰器改变了lal函数的功能。上面的代码中,我们实际上是把lal函数放入了deco函数,像这样:

</>复制代码

  1. lal = deco(lal)

只不过,直接使用@标志把装饰器放在某个函数上更方便一点而已。

装饰器其实就是一个函数嵌套另一个函数(这里涉及到一个概念叫做闭包,下面会讲到)。在装饰器的定义中,需要把内部的函数返回(像hah),内部函数用来真正的改变被装饰函数的功能。

不过,上面定义的装饰器好像没什么用,我们来真正的写一个装饰器,像文章开头说的那样,定义一个装饰器计算函数执行的时间。

实现一个简单的装饰器

</>复制代码

  1. import time
  2. # 这个装饰器接收一个函数作为参数
  3. def clock(func):
  4. # clocked用来改变被装饰函数功能
  5. # 接收任意可变参数
  6. def clocked(*args):
  7. #先计算时间
  8. t0 = time.perf_counter()
  9. # 然后运行被装饰的函数
  10. result = func(*args)
  11. # 计算运行前后的时间差
  12. elapsed = time.perf_counter()-t0
  13. # 函数的名字
  14. name = func.__name__
  15. # 被装饰函数的所有变量
  16. arg_str = ",".join(repr(arg) for arg in args)
  17. # 输出
  18. print("[%0.8fs] %s(%s) -> %r" % (elapsed, name, arg_str, result))
  19. # 返回被装饰函数执行结果
  20. # 可见装饰器是在原来的函数上增加了某些功能
  21. # 而不是完全改变被装饰函数
  22. return result
  23. # 把clocked函数返回
  24. return clocked

来使用一下上面定义的装饰器:

</>复制代码

  1. @clock
  2. def factorial(n):
  3. return 1 if n<2 else n*factorial(n-1)
  4. result = factorial(6)
  5. print(result)

执行结果:

</>复制代码

  1. [0.00000030s] factorial(1) -> 1
  2. [0.00004588s] factorial(2) -> 2
  3. [0.00007184s] factorial(3) -> 6
  4. [0.00060794s] factorial(4) -> 24
  5. [0.00064205s] factorial(5) -> 120
  6. [0.00066801s] factorial(6) -> 720
  7. 720

可以看到,在输出计算结果的同时,输出了每一步的执行时间。

装饰器除了改变函数功能之外还有一个特性是,函数装饰器在导入模块时立即执行,而被装饰的函数只在明确调用时运行。这点需要注意。

当然了,装饰器之上还可以放一个装饰器,不过是多了一层嵌套而已。

python中还有一个内置的模块functools,这里面定义了一些常用的装饰器函数,帮助你更好地定义自己的装饰器。这里就不讲了。

闭包

说到闭包,在上面的代码中我们已经见识到了,函数中嵌套函数就是闭包。严格来说,闭包是指延伸了作用域的函数,怎么理解?不如来看个例子:

我们定义一个函数不断计算平均值,它会记住上一次计算的值进行累计。

</>复制代码

  1. # 先看一些效果
  2. avg = make_averager()
  3. print(avg(10))
  4. print(avg(11))
  5. print(avg(12))

输出如下:

</>复制代码

  1. 10.0
  2. 10.5
  3. 11.0

第一次输出10,第二次输出10加11的平均值,第三次输出10加11加12的平均值。

怎么实现的?

</>复制代码

  1. def make_averager():
  2. # 局部变量series
  3. # 用来保存每次输入的值
  4. series = []
  5. def averager(new_value):
  6. series.append(new_value)
  7. total = sum(series)
  8. return total/len(series)
  9. return averager

上面的函数中,series是局部变量。当我们调用avg(10)的时候,函数已经返回了,按理说它的本地作用域已经不存在了,但是我们还是可以继续使用。这是因为series其实是自由变量,它不受本地作用域的限制。需要注意的是,对于不可变类型,需要显示用关键字nonlocal 声明自由变量,如果不声明的话,会隐式的创建局部变量,这样自由变量就会失效。而可变类型则不需要。比如,我们来更改一下上面的代码:

</>复制代码

  1. # 改一下求平均值的函数
  2. # 用另一种方法
  3. def make_averager():
  4. count = 0
  5. total = 0
  6. def averager(new_value):
  7. # count、total是不可变类型
  8. # 需要声明为自由变量
  9. nonlocal count, total
  10. count += 1
  11. total += new_value
  12. return total / count
  13. return averager

除了上面说的装饰器的用法之外,我们还可以为装饰器添加参数,像app.route("/") 这样,限于篇幅,下一篇文章再介绍。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/41224.html

相关文章

  • python奇遇:隐藏的python功能

    摘要:先不讲数据结构了,这次来说说中一些不被注意的功能。直接交换第二个功能。对的长度使用生成一个序列,然后遍历或者这样第三个功能。其实还接受第二个参数,它的作用是在迭代的过程中如果碰到第二个参数则停止。 先不讲数据结构了,这次来说说python中一些不被注意的功能。 在python的设计哲学中,有这么一条内容:Simple is better than complex,简单的代码比复杂的要好...

    APICloud 评论0 收藏0
  • python奇遇:迭代和生成

    摘要:来说说迭代器和生成器,还有可迭代对象和生成器表达式。有点绕是不是,其实,一般只要知道可迭代对象以及它是如何实现的就行了,中常常用生成器来代替迭代器,可以说,生成器就是迭代器。 来说说迭代器和生成器,还有可迭代对象和生成器表达式。 之前简单的提到过,一个对象是可迭代的可以理解为能够使用for循环。这样说其实不太准确,某个对象可迭代是因为它内部实现了$__iter__$这个特殊方法。比如在...

    atinosun 评论0 收藏0
  • Python奇遇:数据结构窥探2

    摘要:找出列表中小于的数据除了列表推导式,还有字典推导式,集合推导式,用法都一样。如果你的数据量很大的话,考虑使用生成器表达式。切片不仅对列表有用,同样适用于元组和字符串。切片命名使用方法,内部参数与切片一样。对剩余的的数据,使用星号代替即可。 上次我们讲了几个不常见的数据类型,每个都有自己特殊的用途,虽然不经常用到,了解一下也好。比如我们提到的数组类型,如果在数据量很大的时候同时要效率,就...

    Ocean 评论0 收藏0
  • python奇遇:数据结构窥探3

    摘要:字典和集合都是基于散列表实现的,散列表也就是表,了解过数据结构的应该知道。而使用另一种办法,任何键在找不到的情况下都会用中的值数据类型比如替换。在设计时就可以使用创建你的数据接口。 这次主要说说字典和集合这两种数据类型。 字典和集合都是基于散列表实现的,散列表也就是hash表,了解过数据结构的应该知道。与散列表相关的一个概念叫做可散列,什么是可散列?在python官方定义中是这样说的:...

    toddmark 评论0 收藏0
  • Python奇遇:特殊方法窥探

    摘要:在中,特殊方法以双下划线开始,以双下划线结束。真假值,如果向量模为,返回实现向量加法实现向量乘法,例如返回向量的模返回欧几里德范数找个例子运行下。怎么办中有个特殊方法,可以修改控制台输出的样式。 什么是特殊方法?当我们在设计一个类的时候,python中有一个用于初始化的方法$__init__$,类似于java中的构造器,这个就是特殊方法,也叫作魔术方法。简单来说,特殊方法可以给你设计的...

    niceforbear 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<