【转】【Python】装饰器
1、闭包
>>> def outer():
... x = 1
... def inner():
... print x #
... return inner
>>> foo = outer()
>>> foo.func_closure # doctest: +ELLIPSIS
(<cell at 0x...: int object at 0x...>,)
从上一个示例可以看到,inner
是 outer
返回的一个函数,存储在变量 foo
里然后用 foo()
来调用。但是它能运行吗?先来思考一下作用域规则。
Python 中一切都按作用域规则运行—— x
是函数 outer
中的一个局部变量,当函数 inner
在 #1
处打印 x
时,Python 在 inner
中搜索局部变量但是没有找到,然后在外层作用域即函数 outer
中搜索找到了变量 x
。
但如果从变量的生命周期角度来看应该如何呢?变量 x
对函数 outer
来说是局部变量,即只有当 outer
运行时它才存在。只有当 outer
返回后才能调用 inner
,所以依据 Python 运行机制,在调用 inner
时 x
就应该不存在了,那么这里应该有某种运行错误出现。
结果并不是如此,返回的 inner
函数正常运行。Python 支持一种名为函数闭包的特性,意味着 在非全局作用域定义的 inner
函数在定义时记得外层命名空间是怎样的。inner
函数包含了外层作用域变量,通过查看它的 func_closure
属性可以看出这种函数闭包特性。
记住——每次调用函数 outer
时,函数 inner
都会被重新定义。此时 x
的值没有变化,所以返回的每个 inner
函数和其它的 inner
函数运行结果相同,但是如果稍做一点修改呢?
>>> def outer(x):
... def inner():
... print x #
... return inner
>>> print1 = outer(1)
>>> print2 = outer(2)
>>> print1()
1
>>> print2()
2
从这个示例可以看到闭包——函数记住其外层作用域的事实——可以用来构建本质上有一个硬编码参数的自定义函数。虽然没有直接给 inner
函数传参 1 或 2,但构建了能“记住”该打印什么数的 inner
函数自定义版本。
闭包是强大的技术——在某些方面来看可能感觉它有点像面向对象技术:outer
作为 inner
的构造函数,有一个类似私有变量的 x
。闭包的作用不胜枚举——如果你熟悉 Python中 sorted
函数的参数 key
,也许你已经写过 lambda
函数通过第二项而非第一项来排序一些列表。也可以写一个 itemgetter
函数,接收一个用于检索的索引并返回一个函数,然后就能恰当的传递给 key
参数了。
但是这么用闭包太没意思了!让我们再次从头开始,写一个装饰器。
2、 装饰器
装饰器其实就是一个以函数作为参数并返回一个替换函数的可执行函数。让我们从简单的开始,直到能写出实用的装饰器。
>>> def outer(some_func):
... def inner():
... print "before some_func"
... ret = some_func() #
... return ret + 1
... return inner
>>> def foo():
... return 1
>>> decorated = outer(foo) #
>>> decorated()
before some_func
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
版本。用不着学习任何新的语法,通过将包含函数的变量重新赋值就能轻松做到这一点:
>>> foo = outer(foo)
>>> foo # doctest: +ELLIPSIS
<function inner at 0x...>
现在任意调用 foo()
都不会得到原来的 foo
,而是新的装饰器版!明白了吗?来写一个更实用的装饰器。
想象一个提供坐标对象的库。它们可能主要由一对对的 x
、y
坐标组成。遗憾的是坐标对象不支持数学运算,并且我们也无法修改源码。然而我们需要做很多数学运算,所以要构造能够接收两个坐标对象的 add
和 sub
函数,并且做适当的数学运算。这些函数很容易实现(为方便演示,提供一个简单的 Coordinate
类)。
>>> class Coordinate(object):
... def __init__(self, x, y):
... self.x = x
... self.y = y
... def __repr__(self):
... return "Coord: " + str(self.__dict__)
>>> def add(a, b):
... return Coordinate(a.x + b.x, a.y + b.y)
>>> def sub(a, b):
... return Coordinate(a.x - b.x, a.y - b.y)
>>> one = Coordinate(100, 200)
>>> two = Coordinate(300, 200)
>>> add(one, two)
Coord: {'y': 400, 'x': 400}
但是如果 add
和 sub
函数必须有边界检测功能呢?也许只能对正坐标进行加或减,并且返回值也限制为正坐标。如下:
>>> one = Coordinate(100, 200)
>>> two = Coordinate(300, 200)
>>> three = Coordinate(-100, -100)
>>> sub(one, two)
Coord: {'y': 0, 'x': -200}
>>> add(one, three)
Coord: {'y': 100, 'x': 0}
但我们希望在不修改 one
、two
和 three
的基础上,one
和 two
的差值为 {x: 0, y: 0}
,one
和 three
的和为 {x: 100, y: 200}
。接下来用一个边界检测装饰器来实现这一点,而不用对每个函数里的输入参数和返回值添加边界检测。
>>> def wrapper(func):
... def checker(a, b): #
... if a.x < 0 or a.y < 0:
... a = Coordinate(a.x if a.x > 0 else 0, a.y if a.y > 0 else 0)
... if b.x < 0 or b.y < 0:
... b = Coordinate(b.x if b.x > 0 else 0, b.y if b.y > 0 else 0)
... ret = func(a, b)
... if ret.x < 0 or ret.y < 0:
... ret = Coordinate(ret.x if ret.x > 0 else 0, ret.y if ret.y > 0 else 0)
... return ret
... return checker
>>> add = wrapper(add)
>>> sub = wrapper(sub)
>>> sub(one, two)
Coord: {'y': 0, 'x': 0}
>>> add(one, three)
Coord: {'y': 200, 'x': 100}
装饰器和之前一样正常运行——返回了一个修改版函数,但在这次示例中通过检测和修正输入参数和返回值,将任何负值的 x
或 y
用 0
来代替,实现了上面的需求。
是否这么做是见仁见智的,它让代码更加简洁:通过将边界检测从函数本身分离,使用装饰器包装它们,并应用到所有需要的函数。可替换的方案是:在每个数学运算函数返回前,对每个输入参数和输出结果调用一个函数,不可否认,就对函数应用边界检测的代码量而言,使用装饰器至少是较少重复的。事实上,如果要装饰的函数是我们自己实现的,可以使装饰器应用得更明确一点。
3、 函数装饰器 @ 符号的应用
Python 2.4 通过在函数定义前添加一个装饰器名和 @ 符号,来实现对函数的包装。在上面代码示例中,用了一个包装的函数来替换包含函数的变量来实现了装饰函数。
>>> add = wrapper(add)
这种模式可以随时用来包装任意函数。但是如果定义了一个函数,可以用 @ 符号来装饰函数,如下:
>>> @ wrapper
... def add(a, b):
... return Coordinate(a.x + b.x, a.y + b.y)
值得注意的是,这种方式和简单的使用 wrapper
函数的返回值来替换原始变量的做法没有什么不同—— Python 只是添加了一些语法糖来使之看起来更加明确。
使用装饰器很简单!虽说写类似 staticmethod
或者 classmethod
的实用装饰器比较难,但用起来仅仅需要在函数前添加 @装饰器名
即可!
4、 args 和 *kwargs
上面我们写了一个实用的装饰器,但它是硬编码的,只适用于特定类型的函数——带有两个参数的函数。内部函数 checker
接收两个参数,然后继续将参数传给闭包中的函数。如果我们想要一个能适用任何函数的装饰器呢?让我们来实现一个为每次被装饰函数的调用添加一个计数器的装饰器,但不改变被装饰函数。这意味着这个装饰器必须接收它所装饰的任何函数的调用信息,并且在调用这些函数时将传递给该装饰器的任何参数都传递给它们。
碰巧,Python 对这种特性提供了语法支持。请务必阅读 Python Tutorial 以了解更多,但在定义函数时使用 *
的用法意味着任何传递给函数的额外位置参数都是以 *
开头的。如下:
>>> def one(*args):
... print args #
>>> one()
()
>>> one(1, 2, 3)
(1, 2, 3)
>>> def two(x, y, *args): #
... print x, y, args
>>> two('a', 'b', 'c')
a b ('c',)
第一个函数 one
简单的打印了传给它的任何位置参数(如果有)。在 #1
处可以看到,在函数内部只是简单的用到了变量 args
—— *args
只在定义函数时用来表示位置参数将会保存在变量 args
中。Python 也允许指定一些变量,并捕获任何在 args
里的额外参数,如 #2
处所示。
*
符号也可以用在函数调用时,在这里它也有类似的意义。在调用函数时,以 *
开头的变量表示该变量内容需被取出用做位置参数。再举例如下:
>>> def add(x, y):
... return x + y
>>> lst = [1,2]
>>> add(lst[0], lst[1]) #
3
>>> add(*lst) #
3
在 #1 处的代码和 #2 处的作用相同——可以手动做的事情,在 #2 处 Python 帮我们自动处理了。这看起来不错,*args
可以表示在调用函数时从迭代器中取出位置参数, 也可以表示在定义函数时接收额外的位置参数。
接下来介绍稍微复杂一点的用来表示字典和键值对的 **
,就像 *
用来表示迭代器和位置参数。很简单吧?
>>> def foo(**kwargs):
... print kwargs
>>> foo()
{}
>>> foo(x=1, y=2)
{'y': 2, 'x': 1}
当定义一个函数时,使用 **kwargs
来表示所有未捕获的关键字参数将会被存储在字典 kwargs
中。此前 args
和 kwargs
都不是 Python 中语法的一部分,但在函数定义时使用这两个变量名是一种惯例。和 *
的使用一样,可以在函数调用和定义时使用 **
。
>>> dct = {'x': 1, 'y': 2}
>>> def bar(x, y):
... return x + y
>>> bar(**dct)
3
5、 更通用的装饰器
用学到的新知识,可以写一个记录函数参数的装饰器。为简单起见,仅打印到标准输出:
>>> def logger(func):
... def inner(*args, **kwargs): #
... print "Arguments were: %s, %s" % (args, kwargs)
... return func(*args, **kwargs) #
... return inner
注意在 #1
处函数 inner
接收任意数量和任意类型的参数,然后在 #2
处将他们传递给被包装的函数。这样一来我们可以包装或装饰任意函数,而不用管它的签名。
>>> @logger
... def foo1(x, y=1):
... return x * y
>>> @logger
... def foo2():
... return 2
>>> foo1(5, 4)
Arguments were: (5, 4), {}
20
>>> foo1(1)
Arguments were: (1,), {}
1
>>> foo2()
Arguments were: (), {}
2
每一个函数的调用会有一行日志输出和预期的返回值。
再聊装饰器
如果你一直看到了最后一个实例,祝贺你,你已经理解了装饰器!你可以用新掌握的知识做更多的事了。
你也许考虑需要进一步的学习:Bruce Eckel 有一篇很赞的关于装饰器文章,他使用了对象而非函数来实现了装饰器。你会发现 OOP 代码比纯函数版的可读性更好。Bruce 还有一篇后续文章 providing arguments to decorators,用对象实现装饰器也许比用函数实现更简单。最后,你可以去研究一下内建包装函数 functools,它是一个在装饰器中用来修改替换函数签名的装饰器,使得这些函数更像是被装饰的函数。
【转】【Python】装饰器的更多相关文章
- 关于python装饰器
关于python装饰器,不是系统的介绍,只是说一下某些问题 1 首先了解变量作用于非常重要 2 其次要了解闭包 def logger(func): def inner(*args, **kwargs) ...
- python装饰器通俗易懂的解释!
1.python装饰器 刚刚接触python的装饰器,简直懵逼了,直接不懂什么意思啊有木有,自己都忘了走了多少遍Debug,查了多少遍资料,猜有点点开始明白了.总结了一下解释得比较好的,通俗易懂的来说 ...
- Python 装饰器学习
Python装饰器学习(九步入门) 这是在Python学习小组上介绍的内容,现学现卖.多练习是好的学习方式. 第一步:最简单的函数,准备附加额外功能 1 2 3 4 5 6 7 8 # -*- c ...
- python 装饰器修改调整函数参数
简单记录一下利用python装饰器来调整函数的方法.现在有个需求:参数line范围为1-16,要求把9-16的范围转化为1-8,即9对应1,10对应2,...,16对应8. 下面是例子: def fo ...
- python 装饰器学习(decorator)
最近看到有个装饰器的例子,没看懂, #!/usr/bin/python class decorator(object): def __init__(self,f): print "initi ...
- Python装饰器详解
python中的装饰器是一个用得非常多的东西,我们可以把一些特定的方法.通用的方法写成一个个装饰器,这就为调用这些方法提供一个非常大的便利,如此提高我们代码的可读性以及简洁性,以及可扩展性. 在学习p ...
- 关于python装饰器(Decorators)最底层理解的一句话
一个decorator只是一个带有一个函数作为参数并返回一个替换函数的闭包. http://www.xxx.com/html/2016/pythonhexinbiancheng_0718/1044.h ...
- Python装饰器由浅入深
装饰器的功能在很多语言中都有,名字也不尽相同,其实它体现的是一种设计模式,强调的是开放封闭原则,更多的用于后期功能升级而不是编写新的代码.装饰器不光能装饰函数,也能装饰其他的对象,比如类,但通常,我们 ...
- Python装饰器与面向切面编程
今天来讨论一下装饰器.装饰器是一个很著名的设计模式,经常被用于有切面需求的场景,较为经典的有插入日志.性能测试.事务处理等.装饰器是解决这类问题的绝佳设计,有了装饰器,我们就可以抽离出大量函数中与函数 ...
- python装饰器方法
前几天向几位新同事介绍项目,被问起了@login_required的实现,我说这是django框架提供的装饰器方法,验证用户是否登录,只要这样用就行了,因为自己不熟,并没有做过多解释. 今天查看dja ...
随机推荐
- angular中的表单数据自定义验证
之前说过了angular是如何给表单的数据进行基本的,常用的验证的:angular学习笔记(二十)-表单验证 但是在实际工作中,这些验证是远远不够的,很多时候我们需要自定义一些验证规则,以及一些异步, ...
- Android调用Webservice发送文件
一服务器端C#这里有三个上传方法1.uploadFile( byte []bs, String fileName); PC机操作是没有问题2. uploadImage(String filename, ...
- 【内核】linux内核启动流程详细分析
Linux内核启动流程 arch/arm/kernel/head-armv.S 该文件是内核最先执行的一个文件,包括内核入口ENTRY(stext)到start_kernel间的初始化代码, 主要作用 ...
- maven 远程仓库的配置
setting.xml <profile> <id>development</id> <repositories> <repositor ...
- tf.squared_difference
tf.squared_difference squared_difference( x, y, name=None ) 功能说明: 计算张量 x.y 对应元素差平方 参数列表: 参数名 必选 类型 说 ...
- google3aac509c9040e79d
google-site-verification: google3aac509c9040e79d.html
- 让cpu跑到100%的bat文件
请你新建一个叫“virus.bat”的批处理文件,里面填写上如下三段命令: Echo "welcom to the aqjava's world"Tree /fCall virus ...
- JQUERY双下拉框内容的左右移动
教程源码:http://bbs.php100.com/read-htm-tid-74060.html用到的方法为:appendTo() 格式:$(content).appendTo(selector ...
- udev简述
udev 是 Linux 内核的设备管理器.总的来说,它取代了devfs和hotplug,负责管理/dev中的设备节点.同时,udev 也处理所有用户空间发生的硬件添加.删除事件,以及某些特定设备所需 ...
- Java编程的逻辑 (70) - 原子变量和CAS
本系列文章经补充和完善,已修订整理成书<Java编程的逻辑>,由机械工业出版社华章分社出版,于2018年1月上市热销,读者好评如潮!各大网店和书店有售,欢迎购买,京东自营链接:http: ...