12步理解python-Decorator(译)

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

英文原文:Understanding Python Decorators in 12 Easy Steps!

扩展阅读:Python中的装饰器(decorator)

Creative Commons License
This work is licensed under a Creative Commons Attribution-ShareAlike 3.0 Unported License.
永久连接: http://www.nfvschool.cn/?p=676
标签:

发表评论