跳至主要内容

Python | 翻译:Python的装饰器(decorator)


这几天查装饰器的资料时,意外发现了stackoverflow上一篇很不错的文章,所以就翻译了过来,权当加深自己的理解。
 原网址:How to make a chain of function decorators?

是这样的,有人问了一个问题:
能不能用两个装饰器,像这样
@makebold
@makeitalic
def say():
   return "Hello"
可以返回这种东西:
"<b><i>Hello</i></b>"
就是HTML语句。

下面是回答:

Decorator基础

  • Python的函数都是对象

要理解decorators,首先要明白在Python中,函数都是对象。来看一个简单的例子: 把这一点牢记于心,我们很快就会用到它。
Python的函数有另外一个有趣的特性:它们可以在另一个函数的内部定义!

  • 函数引用

你已经知道函数都是对象了。因此,函数
  • 可以赋值给一个变量
  • 可以在另一个函数内部定义
这意味着一个函数可以返回另一个函数。
这不是全部! 
如果你可以返回一个函数,这意味着你也可以把函数作为一个参数。
现在,你已经具备了所有理解decorators所需要的知识了。 不难看出,decorators其实就是“包装纸”,这意味着他们可以让你在它们装饰好的函数前后执行你想要的代码,而不用改动函数本身。


  • 自制装饰器

如何手工制作装饰器呢:
现在,你可能希望,每次你调用 a_stand_alone_function 的时候,实际调用的是a_stand_alone_function_decorated 。这其实很容易,只需要用my_shiny_new_decorator 返回的函数来覆盖a_stand_alone_function


  • 揭秘decorators

使用decorator语法来实现前面的例子:
这就是全部了,非常简单。@decorator 其实就是
another_stand_alone_function = my_shiny_new_decorator(another_stand_alone_function)
的缩写而已。
decorators只是 decorator design pattern的一个变体而已。Python嵌入了几种经典的设计模式来简化开发(比如iterators)。
当然了,decorators是可以累加的:
现在:回答问题的时间到了:
现在你已经可以很轻易地回答这个问题了:
现在,你可以心满意足地离开了,或者再牺牲一些脑细胞来看一下decorators的高级用途。

更深一层的的decorators

  • 给被装饰的函数传递参数


  • 装饰方法

Python一个有趣的地方在于,方法和函数其实是一样的。唯一的不同是方法的第一个参数是对当前对象的一个引用(self)。
这意味着你也可以用同样的方法来创建一个装饰器!只需记得把 self传入即可:
如果你想要做一个可以通用的decorator —— 一个你可以传递给拥有无论什么参数的函数或者方法的decorator的话,请用 *args, **kwargs 吧:

  • 给decorator传递参数

很好,现在你可能会问:那要怎么给装饰器传递参数?

这可能有点坎坷,因为装饰器只能接受一个函数作为它的参数。
所以你不能直接传递额外的参数给装饰器。

在说出解决方法之前,这里先做一个小提醒:

这实际上是一样的:"my_decorator"@my_decorator被调用了。所以,当你使用了,你实际上是调用了函数 "my_decorator"  。

看一下最后的大boss: 一点都不令人惊讶。
让我们来做一件本质上一模一样的事情,跳过所有的中间变量: 还可以更简短一些: 看到没有?我们在用 "@" 语法调用一个函数!:)

回到带参数的decorators。如果我们可以用一个函数来即时生成decorator,我们就可以给我们的decorator传递参数了,对吧?
这里就是一个带参数的装饰器了。参数也可以是变量:
没错,你可以像使用这个技巧的任何函数一样把参数传给装饰器。如果你想要,你甚至可以使用*args, **kwargs但是记住,装饰器只被调用一次。就是Python导入脚本的那一刻。之后你没有办法动态地设置参数。当你"import x"的时候,函数已经被装饰了,所以你不能改变任何东西。



实践:装饰一个装饰器

好的,作为一个奖励,我会给你一段代码,使任何装饰器接受任何参数。毕竟,为了能够接受参数,我们使用另一个函数来创建我们的装饰器。
我们包装了装饰器。
还有什么其他的我们最近见过的可以用来包装函数的东西吗?
下面就来写一个用来装饰装饰器的装饰器! 它可以这么用: 我知道,这时你最后一次有这种感觉了,就是听了一个人说:“在理解递归之前,你必须首先理解递归。”但是现在,你不觉得掌握了这个真的很爽吗?


最佳做法:

  • Python 2.4才引入了装饰器,因此确保你的代码运行在2.4版本以上。
  • 装饰器降低了函数调用的速度。记住这一点。
  • 你无法撤销一个函数上面的装饰。(有人创建了可以撤销的装饰器,但没人使用它……)所以当一个函数被装饰了,它的所有代码都会被装饰。
  • 装饰器包装了函数,这使得他们很难debug。(Python 2.5之后情况好了一些,见下文)
Python 2.5引入了functools 模块,此模块包含了函数 functools.wraps()这个函数将被装饰的函数的name,mudule,和docstring复制给了它的wrapper。
(有趣的是:functools.wraps()也是一个装饰器!:) )

装饰器的用途?

有一个大问题:我能用装饰器做什么?

那有1000种可能性。最典型的用法是扩展一个第三方库的函数(你无法修改这个函数的时候),或者可以用来debug(你不想修改这个函数)。

你可以通过DRY(don't repeat yourself)原则来使用它们扩展几个函数,像这样:
当然,decorators的好处就是你可以立即在几乎任何东西上使用他们而不用重复的写代码。我是说DRY啦。
Python自己提供了几个装饰器:property,staticmethod,等等。

  • Django使用装饰器来管理缓存和查看权限。
  • 用来伪造内联异步函数调用。
总之就是一片秘密花园!

评论

此博客中的热门博文

算法 | 堆排序(heap sort)

    注:本文只讨论最大堆排序,最小堆排序同理 一 . 堆   堆(heap)可以定义为一棵 完全 二叉树,并且这棵树的每一个节点都大于或等于它的子女节点。  最大堆长这样子: 堆的特性 一棵n节点的 完全 二叉树,其高度为$$\lfloor \log_{2}n\rfloor$$ 堆的每一个节点都大于等于它的子女节点。 可以用数组来实现堆。留空H[0],然后在H[1]到H[n]中存在堆元素。留空H[0]的目的是为了让之后的堆排序更加的方便。 所有父母节点的键会在数组的前$$\lfloor n/2 \rfloor$$ 个位置中,而叶子节点会占据后面的$$\lceil n/2 \rceil$$个位置。 在数组中,对于每一个位于父母位置i的键来说,它的子女会位于2i和2i+1 对于一个位于位置i的键来说它的父母将会位于$$\lfloor i/2 \rfloor$$ 如何生成堆 自底向上堆构造(bottom-up heap construction): 对每个父母节点进行父母优势检查: 从最后的父母节点开始,到根为止,检查这些节点的键是否满足父母优势要求。 如果该节点不满足,就把该节点的键K与它子女的最大键进行交换。 然后再检查新位置上,K是不是满足父母的优势要求。 效率: 最坏情况下,每个位于树第i层的节点都要移动到树的最底层h,因为移动一层需要两次键值比较,因此移动到h层需要 2(h-i) 次键值比较,所以总键值比较次数为: 自顶向下堆构造(top-down heap construction): 把新的键连续插入到已经预先构造好的堆: 把包含k附加到堆的最后一个叶节点的后面。 拿k与它的父母节点作比较,如果k刚好小于它的父母节点,则算法停止。 否则,交换k和其父母节点的位置 重复2,3步骤直到k不大于它的父母,或者到达了根为止。 效率: 因为包含n个节点的树的高度大约是 log n ,因此每次插入的时间效率属于 O(log n) 总时间效率叠加即可。 二. 堆排序 堆排序的思路很简单: 把数组构造成堆,此时根是最大值。 把根从堆中“移除”,即把...

Javascript | 从swap函数思考js中的参数传递

事情是这样的,有一天,林逍遥同学在学js的时候,想要用js写一个swap函数,她是这么写的: 但是结果输出来之后,她发现事情并没有这么简单:a还是等于1,b还是等于2. 她赶紧翻了一下《Javascript高级程序设计》,发现里面有一段话: ECMAScript中所有函数的参数都是按值传递的。  传递基本类型的值时,被传递是值会被复制给一个局部变量(即命名参数)。 传递引用类型的值时,会把这个值在内存中的地址复制给一个局部变量,因此这个局部变量的变化会反应在函数的外部。  原来如此,已知a,b是两个基本类型的变量,当把a,b传入swap函数的时候,会生成两个新的局部变量,分别存储a,b的值,也相当于它们的副本,虽然值跟a,b是一样的,但在内存中的地址却是不同的,所以在swap函数里面做的改变并不会影响a,b的值,也就谈不上交换了。 综上所述,不可能写一个swap函数来交换两个基本类型的值。 不过对于引用类型的值(数组,对象),却是可以通过函数改变它们的值的。 当把person复制给xy时,同样也会把存储在person中的值复制一份给xy,不过,person存储的值并不是一个对象,而是这个对象的地址(也就是指针),因此复制的其实是地址值。复制操作结束后,person和xy将指向内存中的同一个地址。 所以在xy上做的改变。也会反映到person上面。 同样地,在传递参数的时候,相当于临时创建了一个局部变量,把person的地址也复制给了这个局部变量,因此在函数内部做的改变会反应到person上。 但是,在函数中对参数赋新值,不会影响到函数外部。 给参数赋值一个新的对象,相当于把参数指向另一块地址,所以不会对person产生影响。 总结:js中参数的传递都是按值传递,不过基本类型变量传递的是变量值,引用类型变量传递的是地址。 题外话:如何用一句话代码交换a,b的值? b={x:a,y:(a=b)}.x; js文化真是博大精深哪!

算法 | 用Javascript实现快速排序(quicksort)

最近在学各种排序算法,刚好写成几篇文记录下来,方便以后查看。 快速排序(quicksort)的主要思想就是:选择一个基准元素,把小于基准的元素全部移到左边,大于基准的元素移到右边。然后对左边和右边的元素分别再进行排序,如此循环到每个小分组只剩下一个元素为止。 对元素的划分有两种算法。一个是Lomuto算法。 把第一个元素作为基准元素。从第二个开始遍历余下的元素,a[i]>=p则继续往前走,当遇到一个小于p的元素时,就停下来,将s增加1,再交换a[s],a[i]的元素。这样就保证比p小的元素永远在左边,比p大的元素永远再右边。 循环结束后,再交换a[s]和a[left]的值。使基准元素成为分界点。 另一个算法是Hoare算法。Hoare就是提出快速排序思想的人,图灵奖得主。 变量i跟踪小于基准的元素,从左到右遍历,遇到大于等于基准的元素就停下来。j跟踪大于基准的元素,从右到左遍历,遇到小于基准的元素就停下来,然后交换a[i]和a[j]。直到i,j相遇。再把基准元素和a[j]交换。比较麻烦的就是每次都要判断i是否溢出。 两个划分算法效率是一样的,个人认为Lomuto算法比较容易理解。 有了划分算法之后,要写快速排序就容易多了。 下面是用js写的原地排序( in-place) : 还有另一种比较有js特色的快速排序实现,代码如下: 第二种快速排序实现有一个很大的缺点,就是很耗内存,对一个有n个元素的数组进行排序,每一次递归都要新建两个数组来存放两边的元素,最好情况下递归循环 log n 次,每次需要 n 个元素的空间,因此需要额外 n(log n) 的空间,加上创建数组需要一些额外开销。因此这种方法对于大数组而言就不合适了。 关于快速排序的效率: 最好情况下,每次都刚好平均分为两个相同长度的分组,递归循环 log n 次, 键值比较次数为 C(n)=2*C(n/2)+n,C(1)=0 最坏情况下,每次数组都会分成一边长度为0,一边长度为n-1的两个分组,递归循环  n-1 次,键值比较次数为 n+(n-1)+(n-2)+……+1 平均情况下,键值比较次数约等于 1.39nlog n     所以它们的效率分别是: 关于快速排序的优化: 更好的基准元素选择方...