鱼C论坛

 找回密码
 立即注册
查看: 2884|回复: 7

[技术交流] 实用干货:如何理解Python装饰器?

[复制链接]
发表于 2017-3-30 20:13:08 | 显示全部楼层 |阅读模式

马上注册,结交更多好友,享用更多功能^_^

您需要 登录 才可以下载或查看,没有账号?立即注册

x
先来个形象比方

内裤可以用来遮羞,但是到了冬天它没法为我们防风御寒,聪明的人们发明了长裤,有了长裤后宝宝再也不冷了,装饰器就像我们这里说的长裤,在不影响内裤作用的前提下,给我们的身子提供了保暖的功效。

如果使用如下的代码

  1. @makebold
  2. @makeitalic
  3. def say():
  4.    return "Hello"
  5. 打印出如下的输出:

  6. <b><i>Hello<i></b>
复制代码


你会怎么做?最后给出的答案是

  1. def makebold(fn):
  2.     def wrapped():
  3.         return "<b>" + fn() + "</b>"
  4.     return wrapped

  5. def makeitalic(fn):
  6.     def wrapped():
  7.         return "<i>" + fn() + "</i>"
  8.     return wrapped

  9. @makebold
  10. @makeitalic
  11. def hello():
  12.     return "hello world"

  13. print hello() ## 返回 <b><i>hello world</i></b>
复制代码


现在我们来看看如何从一些最基础的方式来理解Python的装饰器。英文讨论参考Here。

装饰器是一个很著名的设计模式,经常被用于有切面需求的场景,较为经典的有插入日志、性能测试、事务处理等。装饰器是解决这类问题的绝佳设计,有了装饰器,我们就可以抽离出大量函数中与函数功能本身无关的雷同代码并继续重用。概括的讲,装饰器的作用就是为已经存在的对象添加额外的功能。


1.1. 需求是怎么来的

装饰器的定义很是抽象,我们来看一个小例子。



  1. def foo():
  2.     print 'in foo()'
  3. foo()
复制代码


这是一个很无聊的函数没错。但是突然有一个更无聊的人,我们称呼他为B君,说我想看看执行这个函数用了多长时间,好吧,那么我们可以这样做:

  1. import time
  2. def foo():
  3.     start = time.clock()
  4.     print 'in foo()'
  5.     end = time.clock()
  6.     print 'used:', end - start

  7. foo()
复制代码


很好,功能看起来无懈可击。可是蛋疼的B君此刻突然不想看这个函数了,他对另一个叫foo2的函数产生了更浓厚的兴趣。

怎么办呢?如果把以上新增加的代码复制到foo2里,这就犯了大忌了~复制什么的难道不是最讨厌了么!而且,如果B君继续看了其他的函数呢?

1.2. 以不变应万变,是变也

还记得吗,函数在Python中是一等公民,那么我们可以考虑重新定义一个函数timeit,将foo的引用传递给他,然后在timeit中调用foo并进行计时,这样,我们就达到了不改动foo定义的目的,而且,不论B君看了多少个函数,我们都不用去修改函数定义了!

  1. import time

  2. def foo():
  3.     print 'in foo()'

  4. def timeit(func):
  5.     start = time.clock()
  6.     func()
  7.     end =time.clock()
  8.     print 'used:', end - start

  9. timeit(foo)
复制代码


看起来逻辑上并没有问题,一切都很美好并且运作正常!……等等,我们似乎修改了调用部分的代码。原本我们是这样调用的:foo(),修改以后变成了:timeit(foo)。这样的话,如果foo在N处都被调用了,你就不得不去修改这N处的代码。或者更极端的,考虑其中某处调用的代码无法修改这个情况,比如:这个函数是你交给别人使用的。

1.3. 最大限度地少改动!

既然如此,我们就来想想办法不修改调用的代码;如果不修改调用代码,也就意味着调用foo()需要产生调用timeit(foo)的效果。我们可以想到将timeit赋值给foo,但是timeit似乎带有一个参数……想办法把参数统一吧!如果timeit(foo)不是直接产生调用效果,而是返回一个与foo参数列表一致的函数的话……就很好办了,将timeit(foo)的返回值赋值给foo,然后,调用foo()的代码完全不用修改!

  1. #-*- coding: UTF-8 -*-
  2. import time

  3. def foo():
  4.     print 'in foo()'

  5. # 定义一个计时器,传入一个,并返回另一个附加了计时功能的方法
  6. def timeit(func):
  7.      
  8.     # 定义一个内嵌的包装函数,给传入的函数加上计时功能的包装
  9.     def wrapper():
  10.         start = time.clock()
  11.         func()
  12.         end =time.clock()
  13.         print 'used:', end - start
  14.      
  15.     # 将包装后的函数返回
  16.     return wrapper

  17. foo = timeit(foo)
  18. foo()
复制代码


这样,一个简易的计时器就做好了!我们只需要在定义foo以后调用foo之前,加上foo = timeit(foo),就可以达到计时的目的,这也就是装饰器的概念,看起来像是foo被timeit装饰了。在在这个例子中,函数进入和退出时需要计时,这被称为一个横切面(Aspect),这种编程方式被称为面向切面的编程(Aspect-Oriented Programming)。与传统编程习惯的从上往下执行方式相比较而言,像是在函数执行的流程中横向地插入了一段逻辑。在特定的业务领域里,能减少大量重复代码。面向切面编程还有相当多的术语,这里就不多做介绍,感兴趣的话可以去找找相关的资料。

这个例子仅用于演示,并没有考虑foo带有参数和有返回值的情况,完善它的重任就交给你了 :)

上面这段代码看起来似乎已经不能再精简了,Python于是提供了一个语法糖来降低字符输入量。


  1. import time

  2. def timeit(func):
  3.     def wrapper():
  4.         start = time.clock()
  5.         func()
  6.         end =time.clock()
  7.         print 'used:', end - start
  8.     return wrapper

  9. @timeit
  10. def foo():
  11.     print 'in foo()'

  12. foo()
复制代码


重点关注第11行的@timeit,在定义上加上这一行与另外写foo = timeit(foo)完全等价,千万不要以为@有另外的魔力。除了字符输入少了一些,还有一个额外的好处:这样看上去更有装饰器的感觉。

-------------------

要理解python的装饰器,我们首先必须明白在Python中函数也是被视为对象。这一点很重要。先看一个例子:


  1. def shout(word="yes") :
  2.     return word.capitalize()+" !"

  3. print shout()
  4. # 输出 : 'Yes !'

  5. # 作为一个对象,你可以把函数赋给任何其他对象变量

  6. scream = shout

  7. # 注意我们没有使用圆括号,因为我们不是在调用函数
  8. # 我们把函数shout赋给scream,也就是说你可以通过scream调用shout

  9. print scream()
  10. # 输出 : 'Yes !'

  11. # 还有,你可以删除旧的名字shout,但是你仍然可以通过scream来访问该函数

  12. del shout
  13. try :
  14.     print shout()
  15. except NameError, e :
  16.     print e
  17.     #输出 : "name 'shout' is not defined"

  18. print scream()
  19. # 输出 : 'Yes !'
  20. 我们暂且把这个话题放旁边,我们先看看python另外一个很有意思的属性:可以在函数中定义函数:

  21. def talk() :

  22.     # 你可以在talk中定义另外一个函数
  23.     def whisper(word="yes") :
  24.         return word.lower()+"...";

  25.     # ... 并且立马使用它

  26.     print whisper()

  27. # 你每次调用'talk',定义在talk里面的whisper同样也会被调用
  28. talk()
  29. # 输出 :
  30. # yes...

  31. # 但是"whisper" 不会单独存在:

  32. try :
  33.     print whisper()
  34. except NameError, e :
  35.     print e
  36.     #输出 : "name 'whisper' is not defined"*
复制代码

评分

参与人数 1鱼币 +5 收起 理由
雷锤 + 5

查看全部评分

本帖被以下淘专辑推荐:

想知道小甲鱼最近在做啥?请访问 -> ilovefishc.com
回复

使用道具 举报

发表于 2017-4-3 00:43:14 | 显示全部楼层
干货!~
想知道小甲鱼最近在做啥?请访问 -> ilovefishc.com
回复

使用道具 举报

 楼主| 发表于 2017-4-3 09:24:27 | 显示全部楼层

谢谢支持~
想知道小甲鱼最近在做啥?请访问 -> ilovefishc.com
回复 支持 反对

使用道具 举报

发表于 2020-3-21 19:00:55 From FishC Mobile | 显示全部楼层
没想到,最后一个帖子写的这么好
想知道小甲鱼最近在做啥?请访问 -> ilovefishc.com
回复 支持 反对

使用道具 举报

发表于 2021-10-14 17:31:47 | 显示全部楼层
爱了爱了
想知道小甲鱼最近在做啥?请访问 -> ilovefishc.com
回复 支持 反对

使用道具 举报

发表于 2022-2-13 22:18:39 | 显示全部楼层
我 看  的 更 蛋疼
想知道小甲鱼最近在做啥?请访问 -> ilovefishc.com
回复 支持 反对

使用道具 举报

发表于 2022-2-14 10:18:23 | 显示全部楼层

看看
想知道小甲鱼最近在做啥?请访问 -> ilovefishc.com
回复

使用道具 举报

发表于 2022-4-27 03:02:40 From FishC Mobile | 显示全部楼层
感谢,看懂了
想知道小甲鱼最近在做啥?请访问 -> ilovefishc.com
回复 支持 反对

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|手机版|Archiver|鱼C工作室 ( 粤ICP备18085999号-1 | 粤公网安备 44051102000585号)

GMT+8, 2024-4-30 19:13

Powered by Discuz! X3.4

© 2001-2023 Discuz! Team.

快速回复 返回顶部 返回列表