Ok,也许是我的一个笑话,作为一个python指导员,我发现我的学生对python deccorator(装饰器)的理解一直停留在刚接触的那个阶段。因为装饰器太难理解了!要想要理解装饰器,首先需要理解Python中一些函数编程的规则(concepts)以及python函数定义与调用的特性,这让人理解起来感觉更舒服。“使用” 装饰器是容易地(如 第10部分)!但是写装饰器可能是复杂的。
我不能让装饰器变得简单,但是通过我一步一步地解开迷惑,我将帮助你在理解装饰器的时候获得更多自信。因为装饰器本身很负责,这篇文章注定会很长,所以请坚持将它读完。我将每一步写得尽可能的简单,如果你理解了每一步,你将理解整个装饰器的是怎么工作的!我在文章中假设阅读者知道的python知识很少,从而帮助更多python初学者理解。
我也注意到,在这篇文章中,我使用Python的 doctest模块运行python示例代码。这些代码看起来像一个交互式python控制台会话(>>>与…表示python语句,而输出另起一行)。偶尔有匿名评论以“doctest”开始–这些只是代表doctest,完全可以忽悠~
1、Functions(函数)
在python中,以def关键字定义函数,def后面接函数名与一些可选的函数参数,使用return返回值。下面我们定义一个简单函数并调用它:
>>> def foo(): ... return 1 >>> foo() 1
2、scope(作用域)
在python中,定义函数将产生一个新的作用域,Pythonistas 也可以说函数有自己的作用域。这意味着当在函数中使用一变量时,Python看起来是先从函数的作用域寻找变量。Python中包含两个在作用域中变量查看的函数:locals(),globals()。下面我们通过代码研究他们的不同:
>>> a_string = "This is a global variable" >>> def foo(): ... print locals() >>> print globals() # doctest: +ELLIPSIS {..., 'a_string': 'This is a global variable'} >>> foo() # 2 {}
python内置函数globals()函数返回一个字典,这个字典中含有python所知道的所有变量(在这里,为了清晰,我省略了python自动创建的一些变量)。在#2处,我调用了函数foo,让其打印在本函数命名空间的变量,正如我们所见,函数foo此时的局部变量为空。
3、variable resolution rules(变量解析规则)
当然,这并不能说明我们不能在我们的函数中访问全局变量。Python的作用域规则是在局部作用域创建的变量为局部变量,在局部作用域中,访问变量时首先搜索当前局部作用域,然后(若没有找到)再搜索所有的封闭作用域以寻找一个匹配。所有,如果我们修改我们的函数foo,让其打印全局变量,它也将按照我们期望的那样工作:
>>> a_string = "This is a global variable" >>> def foo(): ... print a_string # 1 >>> foo() This is a global variable
在#1处,python首先在函数中寻找局部变量,若没有找到,然后在寻找一个同名的全局变量。
另一方面,如果我们在函数中修改全局变量,它将不会按照我们想要的那样工作。
>>> a_string = "This is a global variable" >>> def foo(): ... a_string = "test" # 1 ... print locals() >>> foo() {'a_string': 'test'} >>> a_string # 2 'This is a global variable'
我们可以看到,全局变量可以被访问,但不能被赋值。在#1处,我们实际是在函数体中创建了一个新的局部变量,一个与全局变量一样名字的局部变量。
我们可以通过打印函数中的局部作用域查看,可以发现我们的函数中有一个变量项。我们也可以在#2处再次查看全局变量域,变量a_string没有任何改变。
4、variable lifetime(变量的生存期)
值得注意的是,变量不仅有作用域,而且有生存期。思考下面代码
>>> def foo(): ... x = 1 >>> foo() >>> print x # 1 Traceback (most recent call last): ... NameError: name 'x' is not defined
在#1处,不仅仅是因为作用域规则而导致的错误(虽然这是我们有NameError的原因),it also has to do with how function calls are implemented in python and many other languages. 没有任何语法可以支持我们在#1处这样调用,因为这时x变量已经不存在!我们的foo函数在调用时创建x变量,并在调用结束时销毁了它。
5、Function arguments and paramenters(函数的参数)
python 允许我们传递参数给函数,这些参数名字在函数中变成了局部变量。
>>> def foo(x): ... print locals() >>> foo(1) {'x': 1}
python中,有很多种定义函数参数与传递参数的方式,你可以在the python documentation on defining functions查看所有的方法。我在这里给出一个简短的版本:函数参数可以是 强制性的或被命名的位置参数,或者可选的默认值参数。
>>> def foo(x, y=0): # 1 ... return x - y >>> foo(3, 1) # 2 2 >>> foo(3) # 3 3 >>> foo() # 4 Traceback (most recent call last): ... TypeError: foo() takes at least 1 argument (0 given) >>> foo(y=1, x=3) # 5 2
在#1处,我们定义一个位置参数x与一个命名参数y的函数。在#2处,我们调用这个函数,并通过普通的方法传递参数给它,虽然函数中有个命名参数,我们仍然可以通过位移传递参数。如#3处所示,我们也可以不传递任何参数给命名参数,python将使用命名参数y的默认值0 作为其值。但我们不能不给(强制的,位置的)参数赋值,否则将出现错误,如#4处所示。
在#5处,我们通过两个命名参数的方式调用函数,即使函数的参数在定义的时候有一个是位置参数,但因为我们使用变量的名字调用参数,所有变量的顺序就无关紧要了。
当然,反过来也可以,即命名参数也可以通过位置赋值给它们。
6、Nested function(嵌套的函数)
python允许产生嵌套的函数,这意味着我们可以在函数里面定义函数,所有的作用域与生存周期和通常情况相同。
>>> def outer(): ... x = 1 ... def inner(): ... print x # 1 ... inner() # 2 ... >>> outer() 1
这个看起来有点复杂,但其仍然是一个明智的方法。让我们看看在#1处发生了什么:python寻找局部变量x,没有找到,于是在另外一个函数的作用域中查找!变量x是函数outer的局部变量,我们的函数inner也可以访问。在#2处,我们调用函数inner。值得注意的是,inner也是一个遵循python查找规则的变量名,python首先在outer函数的作用域查询它,找到一个命名为inner的局部变量。
7、Functions are first class objects in Python(函数也是object(第一个类)对象)
在python中,函数与其他所有一样也是objects对象,函数中也含有变量,没有什么特殊的。下面是个简单的观察实验:
>>> issubclass(int, object) # all objects in Python inherit from a common baseclass True >>> def foo(): ... pass >>> foo.__class__ # 1 <type 'function'> >>> issubclass(foo.__class__, object) True
你可能从来不认为你的函数含有属性,但事实是,在python中函数也是对象,一切皆对象(python中,类也是对象)。一个学术性的观点:函数与python中其他的值一样也是一个普通的值。这意味着,你可以将函数作为一个参数传递给函数,并将函数像值一样在一个函数中返回!若你从来没有考虑过这种事情,让我们看下下面完全合法的代码:
>>> def add(x, y): ... return x + y >>> def sub(x, y): ... return x - y >>> def apply(func, x, y): # 1 ... return func(x, y) # 2 >>> apply(add, 2, 1) # 3 3 >>> apply(sub, 2, 1) 1
这个例子你可能不会感到陌生:add 与 sub 都是普通的python函数,它们接收两个值,然后返回被计算的值。在#1处,我们看到,一个变量被定义用来接收函数,函数仅仅是一个与其他变量一样的普通变量。在#2处,我们调用传进apply函数的的函数,在python中,圆括号是调用操作符,其调用包含的两个变量。在#3处,我们发现,我们将函数作为值传递进函数没有产生任何语法错误。函数的名字和其他的变量一样只是一个变量的标签。
你可能在以前见过这种以函数为参数的行为–Python中,在使用sorted函数排序时,经常会传递一个函数参数给它用于自定义排序。但是,函数作为返回值返回又如何呢?
看下面代码:
>>> def outer(): ... def inner(): ... print "Inside inner" ... return inner # 1 ... >>> foo = outer() #2 >>> foo # doctest:+ELLIPSIS <function inner at 0x...> >>> foo() Inside inner
这看起来可能有点奇怪。在#1处,我返回一个变量inner,一个函数标签。这里没有特别的语法—-我们的函数返回inner函数,不然inner将不会被调用。还记得变量的生存时间吗?当每次调用outer函数的时候,函数inner都将被重新定义。但是如果inner没有从outer返回,it would simply cease to exist when it went out of scope.
在#2处,我们捕获返回值函数inner,并将这个变量存储在foo变量中。如果我们查看foo,我们可以看到,foo确实承载着我们函数inner。我们可以使用圆括号操作符调用它。这个看起来怪怪的,但到目前为止,没有什么难以理解,对不对?坚持下去,一切都将发生转变!
8、Closures(闭包)
下面我们直接以一个示例代码开始,我们稍微调整了我们上一个例子:
>>> def outer(): ... x = 1 ... def inner(): ... print x # 1 ... return inner >>> foo = outer() >>> foo.func_closure # doctest: +ELLIPSIS (<cell at 0x...: int object at 0x...>,)
在上一个例子中,我们使outer函数返回inner函数,并将其存储在变量foo中,然后我们使用foo()调用了它。然而其可以工作吗?让我们一起来考虑下作用域的问题。
根据python的作用域规则,一切都将这样工作 —- 变量x是函数outer的局部变量,其只当outer运行时才存在。在函数outer返回后,我们根本不可能去调用inner,因为根据我们的python工作模型,在我们调用inner时,其根本不存在了。应该发生运行错误。
然而事实证明,返回的函数inner可以正常工作,这个与我们的预计相悖。这是因为python支持一个特性,叫函数闭包(function closures):函数中定义了另外一个函数,外部函数的局部变量在外部函数返回之后继续存在,并能被内部函数引用。闭包是一个集合,它包含了外部函数的局部变量。我们可以通过 func_closure 属性查看包含的变量。
记住—–函数inner将在每次调用outer时被重新定义。现在 变量x的值没有变化,则我们获得的每一个inner函数都与其他inner函数做着同样的事情。—但如果我们稍微调整一下呢?
>>> def outer(x): ... def inner(): ... print x # 1 ... return inner >>> print1 = outer(1) >>> print2 = outer(2) >>> print1() 1 >>> print2() 2
从这个例子中,我们看到闭包特性使函数可以记住其外围作用域。可以构建硬件码参数的自定义函数。我们不是传递数字1或者数字2到inner函数,而是创建一个自定义的inner函数版本,其“记住”应该打印什么数字。
这是一个强大的技术—-从某些方面来说,你甚至可以认为它是面向对象的技术:outer是inner的一个构造函数,x变量为其私有变量。它的应用领域非常广泛。—如果你熟悉python中sorted函数的关键参数,你可以写一个lambda函数来排序一系列列表的第二项而不是第一项。你现在也许可以写一个itemgetter函数用于接受索引检索并返回一个可以适当传递关键参数的函数。
但是,请不要使用闭关做这些平常的事情,让我们再一次延伸它,使用它写一个装饰器!
9、Decorators(装饰器)
装饰器是一个可将函数作为参数并返回一个替换了的函数的callable。
我们看如下简单代码:
>>> def outer(some_func): ... def inner(): ... print "before some_func" ... ret = some_func() # 1 ... return ret + 1 ... return inner >>> def foo(): ... return 1 >>> decorated = outer(foo) # 2 >>> decorated() before some_func 2
仔细观察我们上面的装饰器的例子,我们定义了只有一个参数some_func的函数outer。在outer中,我们定义了一个嵌套函数inner。inner函数首先打印一串字符串然后调用some_func函数并获得其返回结果,如#1处所示。some_func的值可能在每次outer调用的时候都不一样,但是无论我们调用什么样的some_func,最后inner返回的都是some_func的返回值加1—–我们可以看到,当我们调用#2处返回的函数时,我们获得了打印的结果2,而不是我们我们调用函数foo时希望的结果1。
我们可以看到,装饰的变量是foo函数装饰的一个版本—-是foo加上些东西。事实上,如果我们写一些有用的装饰器,我们可能想用这些装饰后的函数替换原来的foo函数,即直接使用foo函数获得更多的功能(“Plus something”)。我们不用再学习新的语法,只需简单地重新给foo赋值即可。
>>> foo = outer(foo) >>> foo # doctest: +ELLIPSIS <function inner at 0x...>
现在,任何对foo()的调用都不会去调用原始的foo函数,他将调用那个被装饰的版本。理解了吗?下面让我们再写一个有用的装饰器。
想象我们有一个提供坐标对象的python库,他们可能主要由x,y坐标对组成。不幸的是,坐标对象不支持数学运算符,并且我们也不能修改库的源码来支持这种操作。可是,我们需要进行大量数学运算,我们需要给两两坐标对象添加加法与减法运算功能,从而实现一些数学运算。这些功能很容易实现:
>>> 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}
但是,如果我们的加法与减法函数需要添加一些边界检查功能呢?也许你仅仅只能对正坐标进行加减运算,并且任何计算结果也都限制为正坐标。因此,如下代码
>>> 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两个坐标相减的结果为{‘y’:0, ‘x’:0};one与three的和为{‘y’:200, ‘x’:100}。
当然我们可以通过给每一个函数添加“传入参数值”与”返回值“的边界检查来达到我们的目的,但是我们这里采用通过添加边界检查装饰器的方式:
>>> def wrapper(func): ... def checker(a, b): # 1 ... 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}
这个装饰器仍然像我们之前描述的那样工作,其返回一个修改了的函数版本,但是在这个示例中,我们通过检查与改变输入参数与返回值做了一些更有意义的工作。
那么,是否可以让我们的代码更加简洁呢?
10、The @ symbol applies a decorator to a function(使用@符号将一个装饰器应用到一个函数)
python2.4版本以后开始支持使用一个装饰器封装一个函数,装饰函数的方法是将一个@符号以及装饰器的名称写在函数定义前。我们原来使用的示例代码:
>>> add = wrapper(add)
现在可以表示为
>>> @wrapper ... def add(a, b): ... return Coordinate(a.x + b.x, a.y + b.y)
值得注意的是,现在这种使用装饰器的方法与原来的方法并没有什么不同,只是现在的方法看起来更简洁明了罢了。
再次说明–使用装饰器是简单的,即使写一些如staticmethod或者classmethod的装饰器是一件非常困难的事情,但我们使用它只需把@decoratorname放在定义函数前就行了。
11、*args and **kwargs(远足参数与字典参数)
我们已经写了一个有用的装饰器,但是它只能装饰那些特殊的函数,那些有两个参数的函数。因为我们的内部函数checker只接受两个参数。要是我们想要我们的装饰器装饰尽可能多的函数那该怎么办?我们在这里使用元组参数与字典参数。元组参数的使用如下:
>>> def one(*args): ... print args # 1 >>> one() () >>> one(1, 2, 3) (1, 2, 3) >>> def two(x, y, *args): # 2 ... print x, y, args >>> two('a', 'b', 'c') a b ('c',)
也可以这样用
>>> def add(x, y): ... return x + y >>> lst = [1,2] >>> add(lst[0], lst[1]) # 1 3 >>> add(*lst) # 2 3
关于字典参数,代码示例如下:
>>> def foo(**kwargs): ... print kwargs >>> foo() {} >>> foo(x=1, y=2) {'y': 2, 'x': 1}
也可以这样
>>> dct = {'x': 1, 'y': 2} >>> def bar(x, y): ... return x + y >>> bar(**dct) 3
12、More generic decorator
我们可以使用*args 与 **kwargs写一个装饰器logs,在装饰器中将函数参数简单的打印出来:
>>> def logger(func): ... def inner(*args, **kwargs): #1 ... print "Arguments were: %s, %s" % (args, kwargs) ... return func(*args, **kwargs) #2 ... return inner
值得注意的是,我们的inner函数可以接受任意数量与任意类型的参数,如#1处所示;并可以将他们传递给被封装的函数,如#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