一步步教你领略Python装饰器
当前位置:以往代写 > Python教程 >一步步教你领略Python装饰器
2019-06-14

一步步教你领略Python装饰器

一步步教你领略Python装饰器

或者你已经用过装饰器,它的利用方法很是简朴但领略起来坚苦(其实真正领略的也很简朴),想要领略装饰器,你需要懂点函数式编程的观念,python函数的界说以及函数挪用的语礼貌则等,固然我没法把装饰器变得简朴,可是我但愿可以通过下面的步调让你由浅入深大白装饰器是什么。假定你拥有最根基的Python常识,本文叙述的对象大概对那些在事情中常常打仗Python的人有很大的辅佐。下面我们来一步步领略python的装饰器:

1、函数(Functions)

在Python里,函数是用def要害字后跟一个函数名称和一个可选的参数表列来建设的,可以用要害字return指定返回值。下面让我们建设和挪用一个最简朴的函数:

>>> def foo():
...     return 1
>>> foo()
1

该函数的函数体(在Python里迁就是多行语句)是强制性的而且通过缩进来表白。我们可以通过在函数名后头添加双括号来挪用函数。

2、浸染域(Scope)

在Python中,每个函数城市建设一个浸染域。Pythonistas也大概称函数拥有它们本身的定名空间(namespace)。这意味着当在函数体里碰着变量名时,Python首先在该函数的定名空间中查找,Python包括了一些让我们查察定名空间的函数。让我们写一个简朴的函数来探查一下local和global浸染域的区别。

>>> a_string = "This is a global variable"
>>> def foo():
...     print locals()
>>> print globals() # doctest: +ELLIPSIS
{..., 'a_strin': 'This ia a global variable'}
>>> foo() # 2
{}

内建的globals函数返回一个字典工具,它包括所有Python知道的变量名(为了清楚明白起见,我已经忽略了一些Python自动建设的变量)。在#2处我挪用了函数foo,它将函数内部的local namespace里的内容打印了出来。正如我们看到的foo函数拥有本身的独立namespace,此刻它照旧空的。

3、变量理会法则(variable resolution rules)

虽然,这并不料味着在函数内部我们不能会见全局变量。Python的浸染域法则是,变量的建设总会建设一个新的local变量,可是变量的会见(包罗修改)会先查找local浸染域然后顺着最相近的浸染域去寻找匹配。因此,假如我们修改foo函数来让它打印global变量,功效就会像我们但愿的那样:

>>> a_string = "This is global variable"
>>> def foo():
...     print a_string # 1
>>> foo()
This is a global variable

在#1处,Python在函数中寻找一个local变量,可是没有找到,然后在global变量中找到了一个同名的变量。

另一方面,假如我们实验在函数里给global变量赋值,功效将不如我们所愿:

>>> 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处我们实际上建设了一个新的local变量,它和全局变量拥有沟通的名字,它将全局变量给包围了。我们可以通过在foo函数内部打印local namespace来发明到它已经有了一个条目,通过对函数外部的#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的原因),并且也出于在Python和很多其它语言里的函数挪用实现的原因。此处,我们没有任何可用的语法来获取变量x的值——字面上是不存在的。每次当挪用foo函数时,它的namespace被从头构建,而且当函数竣事时被销毁。

5、函数的参数(Function parameters)

#p#分页标题#e#

Python答允我们向函数通报参数。参数名成为了该函数的local变量。

>>> def foo(x):
...        print locals()
>>> foo(1)
{'x': 1}

Python有很多差异的界说和通报函数参数的要领。要想更具体深入地相识请参照the Python documentation on defining functions。这里我展示一个简版:函数参数既可以是强制的位置参数(positional parameters)可能是定名参数,参数的默认值是可选的。

>>> 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处我们可以通过普通的值通报来挪用函数,纵然一个参数(译者注:这里指参数y)在函数界说里被界说为一个定名参数。在#3处我们可以看到,我们甚至可以不为定名参数通报任何值就可以挪用函数——假如foo函数没有吸收到传给定名参数y的值,Python将会用我们声明的默认值0来挪用函数。虽然,我们不能遗漏第一个(强制的,定好位置的)参数——#4以一个异常描写了这种错误。

都很清晰和直接,不是吗?下面变得有点儿让人迷惑——Python也支持函数挪用时的定名参数而不可是在函数界说时。请看#5处,这里我们用两个定名参数挪用函数,尽量这个函数是以一个定名和一个位置参数来界说的。因为我们的参数有名字,所以我们通报的参数的位置不会发生任何影响。 相反的景象虽然也是正确的。我们的函数的一个参数被界说为一个定名参数可是我们通过位置通报参数—— #4处的挪用foo(3, 1)将一个3作为第一个参数通报给我们排好序的参数x并将第二个参数(整数1)通报给第二个参数,尽量它被界说为一个定名参数。

Whoo!这就像用许多话来描写一个很是简朴的观念:函数的参数可以有名称可能位置。

6、内嵌函数(Nested functions)

Python答允建设嵌套函数,这意味着我们可以在函数内声明函数而且所有的浸染域和声明周期法则也同样合用。

>>> def outer():
...     x = 1
...     def inner():
...         print x # 1
...     inner() # 2
...
>>> outer()
1

这看起来稍显巨大,但其行为仍相当直接,易于领略。思量一下在#1处产生了什么——Python寻找一个名为x的local变量,失败了,然后在最相近的外层浸染域里搜寻,这个浸染域是另一个函数!变量x是函数outer的local变量,可是和前文提到的一样,inner函数拥有对外层浸染域的会见权限(最起码有读和修改的权限)。在#2处我们挪用了inner函数。请记着inner也只是一个变量名,它也遵从Python的变量查找法则——Python首先在outer的浸染域里查找之,找到了一个名为inner的local变量。

7、函数是一等国民(Functions are first class objects in Python)

在Python中,这是一个知识,函数是和其它任何对象一样的工具。呃,函数包括变量,它不是那么的非凡!

>>> issubclass(int, object) # all objects in Python inherit from a common baseclass
True
>>> def foo():
...     pass
>>> foo.__class__ # 1>>> issubclass(foo.__class__, object)
True

你也许从没想到过函数也有属性,可是在Python中,和其它任何对象一样,函数是工具。(假如你觉察这令你感想狐疑,请等一下,知道你相识到在Python中像其它任何对象一样,class也是工具!)也许正是因为这一点使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

#p#分页标题#e#

这个例子对你来说大概也不是太奇怪——add和sub是尺度的Python函数,它们都接管两个值并返回一个计较了的功效。在#1处你可以看到变量接管一个函数就像其它任何普通的变量。在#2处我们挪用传入apply的函数——在Python里双括号是挪用操纵符,而且挪用变量名包括的值。在#3处你可以看出在Python中将函数当做值举办通报并没有任何非凡语法——函数名就像任何其它变量一样只是变量标签。

你之前大概见过这种行为——Python将函数作为参数常常见于像通过为key参数提供一个函数来自界说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函数(挪用outer()函数并不发生可见的执行)。还记得变量的生命周期吗?每当outer函数被挪用时inner函数就会从头被界说一次,可是假如inner函数不被(outer)返回那么当超出outer的浸染域后,inner将不复存在了。

在#2处我们可以获取到返回值,它是我们的inner函数,它被存储于一个新的变量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...>,)

从上一个例子中我们看到inner是一个由outer返回的函数,存储于一个名为foo的变量,我们可以通过foo()挪用它。可是它能运行吗?让我们先来思量一下浸染域法则。

一切都依照Python的浸染域法则而运行——x是outer函数了一个local变量。当inner在#1处打印x时,Python在inner中寻找一个local变量,没有找到;然后它在外层浸染域即outer函数中寻找并找到了它。

可是自此处从变量生命周期的角度来看又会如何呢?变量x是函数outer的local变量,这意味着只有当outer函数运行时它才存在。只有当outer返回后我们才气挪用inner,因此依照我们关于Python如何运作的模子来看,在我们挪用inner的时候x已经不复存在了,那么某个运行时错误大概会呈现。

事实与我们的预想并纷歧致,返回的inner函数简直正常运行。Python支持一种称为闭包(function closures)的特性,这意味着界说于非全局浸染域的inner函数在界说时记得它们的外层浸染域长什么样。这可以通过查察inner函数的func_closure属性来查察,它包括了外层浸染域里的变量。

请记着,每次当outer函数被挪用时inner函数都被从头界说一次。今朝x的值没有改变,因此我们获得的每个inner函数和其它的inner函数拥有沟通的行为,可是假如我们将它做出一点改变呢?

>>> def outer(x):
...     def inner():
...         print x # 1
...        return inner
>>> print1 = outer(1)
>>> print2 = outer(2)
>>> print1()
1
>>> print2()
2

#p#分页标题#e#

从这个例子中你可以看到closures——函数记着他们的外层浸染域的事实——可以用来构建本质上有一个硬编码参数的自界说函数。我们没有将数字1可能2通报给我们的inner函数可是构建了能"记着"其应该打印数字的自界说版本。

closures就是一个强有力的技能——你甚至想到在某些方面它有点雷同于面向工具技能:outer是inner的结构函数,x饰演着一个雷同私有成员变量的脚色。它的浸染有许多,假如你熟悉Python的sorted函数的key参数,你大概已经写过一个lambda函数通过第二项而不是第一项来排序一些列list。也许你此刻可以写一个itemgetter函数,它吸收一个用于检索的索引并返回一个函数,这个函数适合通报给key参数。

可是让我们不要用闭包做任何恶梦般的工作!相反,让我们从头从新开始来写一个decorator!

9、装饰器(Decorators)

一个decorator只是一个带有一个函数作为参数并返回一个替换函数的闭包。我们将从简朴的开始一直到写出有用的decorators。

>>> 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

请仔细看我们的decorator实例。我们界说了一个接管单个参数some_func的名为outer的函数。在outer内部我们界说了一个名为inner的嵌套函数。inner函数打印一个字符串然后挪用some_func,在#1处缓存它的返回值。some_func的值大概在每次outer被挪用时差异,可是无论它是什么我们都将挪用它。最终,inner返回some_func的返回值加1,而且我们可以看到,当我们挪用存储于#2处decorated里的返回函数时我们获得了输出的文本和一个返回值2而不是我们期望的挪用foo发生的原始值1.

我们可以说decorated变量是foo的一个“装饰”版本——由foo加上一些对象组成。实际上,假如我们写了一个有用的decorator,我们大概想用装饰后的版原来替换foo,从而可以获得foo的“增添某些对象”的版本。我们可以不消进修任何新语法而做到这一点——从头将包括我们函数的变量举办赋值:

>>> foo = outer(foo)
>>> foo # doctest: +ELLIPSIS
<function inner at 0x...>

此刻任何对foo()的挪用都不会获得原始的foo,而是会获得我们颠末装饰的版本!贯通到了一些decorator的思想吗?

10、装饰器的语法糖–@标记(The @ symbol applies a decorator to a function)

Python 2.4通过在函数界说前添加一个@标纪录现对函数的包装。在上面的代码示例中,我们用一个包装了的函数来替换包括函数的变量来实现了包装。

>>> add = wrapper(add)

这一模式任何时候都可以用来包装任何函数,可是假如们界说了一个函数,我们可以用@标记像下面示例那样包装它:

>>> @wrapper
... def add(a, b):
...     return Coordinate(a.x + b.x, a.y + b.y)

请留意,这种方法和用wrapper函数的返回值来替换原始变量并没有任何差异,Python只是增添了一些语法糖(syntactic sugar)让它看起来更明明一点。

11、*args and **kwargs

我们已经写了一个有用的decorator,可是它是硬编码的,它只合用于特定种类的函数——带有两个参数的函数。我们函数内部的checker函数接管了两个参数,然后继承将参数闭包里的函数。假如我们想要一个能包装任何范例函数的decorator呢?让我们实现一个在不改变被包装函数的前提下对每一次被包装函数的挪用增添一次计数的包装器。这意味着这个decorator需要接管所有待包装的任何函数并将通报给它的任何参数通报给被包装的函数来挪用它(被包装的函数)。

这种环境很常见,所以Python为这一特性提供了语法支持。请确保阅读Python Tutorial以相识更多,可是在函数界说时利用*运算符意味着任何通报给函数的特别位置参数最终以一个*作为前导。因此:

>>> 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')

#p#分页标题#e#

第一个函数one只是简朴的将任何(假如有)通报给它的位置参数打印出来。正如你在#1处见到的,在函数内部我们只是引用了args变量——*args只是表白在函数界说中位置参数应该生存在变量args中。Python也答允我们指定一些变量并捕捉到任安在args变量里的其它参数,正如#2地方示。

*运算符也可以用于函数挪用中,这时它也有着雷同的意义。在挪用一个函数时带有一个以*为前导的变量作为参数暗示这个变量内容需要被理会然后用作位置参数。再一次以实例来说明:

>>> def add(x, y):
...     return x + y
>>> lst = [1, 2]
>>> add(lst[0], lst[1]) # 1
3
>>> add(*lst) # 2
3

#1处的代码抽取出了和#2处沟通的参数——在#2处Python为我们自动理会了参数,我们也可以像在#1处一样本身理会出来。这看起来不错,*args既暗示当挪用函数是从一个iterable抽取位置参数,也暗示当界说一个函数是接管任何特另外位置变量。

当我们引入**时,工作变得越发巨大点,与*暗示iterables和位置参数一样,**暗示dictionaries & key/value对。很简朴,不是么?

>>> def foo(**kwargs):
...     print kwargs
>>> foo()
{}
>>> foo(x=1, y=2)
{'y': 2, 'x': 1}

当我们界说一个函数时我们可以用**kwargs表白所有未捕捉的keyword变量应该被存储在一个名为kwargs的字典中。前面的例子中的args和本例中的kwargs都不是Python语法的一部门,可是在函数界说时利用这两个作为变量名时一种老例。就像一样,我们可以在函数挪用时利用\*。

>>> dct = {'x': 1, 'y': 2}
>>> def bar(x, y):
...     rturn x + y
>>> bar(**dct)
3

12、更通用的装饰器(More generic decorators)

用我们把握的新“兵器”我们可以写一个decorator用来“记录”函数的参数。为了简朴起见,我们将其打印在stdout上:

>>> def logger(func):
...     def inner(*args, **kwargs): # 1
...     print "Arguments were: %s, %s" % (args, kwargs)
...     return func(*args, **kwargs) # 2
... 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

对函数的挪用会发生一个"logging"输出行,也会输出一个如我们期望的函数返回值。

假如你一直跟到了最后一个实例,祝贺你,你已司领略了decorators了!

    关键字:

在线提交作业