如何理解python类装饰器装饰器

[Python] 对 Python 装饰器的理解的一些心得分享出来给大家参考 - 推酷
[Python] 对 Python 装饰器的理解的一些心得分享出来给大家参考
最近写一个py脚本来整理电脑中的文档,其中需要检校输入的字符,为了不使代码冗长,想到使用装饰器。
上网搜索有关python的装饰器学习文档,主要看的是
的一篇博文,以及Limodou的一篇文章。作为初学者,这两篇文章对新手有很大的帮助,但仍然有些不易理解的地方。因此在此以一个初学者的认知记录一下python的装饰器的学习心得。
1. 什么是装饰器?
顾名思义,装饰器就是在方法上方标一个带有@符号的方法名,以此来对被装饰的方法进行点缀改造。
当你明白什么是装饰器之后,自然会觉得这个名字取得恰如其分,但作为初学者来说多少还是会有些迷茫。下面用代码来说明怎么理解装饰器。
def target():
print('this is target')
def decorator(func):
print('this is decorator')
decorator(target)
-------------------------------------------
运行结果为:
this is target
this is decorator
Python允许将方法当作参数传递,因此以上脚本就是将target方法作为参数传入decorator方法中,这其实也是装饰器的工作原理,以上代码等同于:
def decorator(func):
print('this is decorator')
@decorator
def target():
print('this is target')
-------------------------------------------
运行结果:
this is target
this is decorator
因此可以看出,所谓的装饰器就是利用了Python的方法可作参数传递的特性,将方法target作为参数传递到方法decorator中。
@decorator
def target():
这种在一个方法的上方加一个@符号的写法,就是表示位于下方的方法将被作为参数传递到位于@后面的decorator方法中。使用@符号只是让脚本1中的代码换了一个写法,更加好看,当然也会更加灵活与实用,后面会讲到这点。
但它们的本质其实是一样的,这也就是装饰器的工作原理。
2. 装饰器的原理
如果你仔细看的话,会在脚本2中发现一个问题,那就是脚本2中最后一行的target只是一个方法名字,它不是正确的方法调用,正确写法应该加上左右括号的target(),如下:
def decorator(func):
print('this is decorator')
@decorator
def target():
print('this is target')
--------------------------------------------
运行结果:
this is target
this is decorator
Traceback (most recent call last):
File "C:/Users/Me/Desktop/ff.py", line 34, in &module&
TypeError: 'NoneType' object is not callable
正如你所看到的,如果按照正确的写法,运行结果你会看到应该出现的两行打印文字&this is target&和&this is decorator&,还会出现错误提示,ff.py是我为写这篇心得临时编写的一个py脚本名字,提示说'NoneType'对象不可调用。这是怎么回事?好吧,我现在必须告诉你,其实脚本2和脚本3中并不是一个使用装饰器的正确写法,不是使用错误,而是作为装饰器的decorator方法写的并不友好,是的,我不认为它是错误的写法,只是不友好。但只要你明白其中的道理,使用恰当的手段也是可以运行正常的,这就是为什么脚本2看似写错了调用方法却得出了正确的结果。当然学习还是得规规矩矩,后面我会具体说正确的装饰器怎么书写,在这里我先解释了一下脚本2和脚本3的运行原理,了解它们的运行原理和错误原因,其实就是了解装饰器的原理。
脚本2和脚本3的区别在于target和target(),也就是说真正的差别在于()这个括号。当()被附加在方法或者类后面时,表示调用,或者称为运行及实例化,无论称呼怎样,本质意义没有不同,都是调用给出的对象,当对象不具备调用性的时候,就会报错:'某个类型' object is not callable。当一个方法被调用后,即target(),是否能被再次执行,取决于它是否会return一个对象,并且该对象可以被调用。也许你会有点迷糊,对比一下代码会比较容易理解我想表达的意思:
1 &&&def returnme():
print('this is returnme')
4 &&&def target():
print('this is target')
7 &&&target
8 &function target at 0xA40D0&
10 &&&target()
12 &function returnme at 0xA4268&
14 &&&target()()
16 returnme
18 &&&returnme()()
19 returnme
20 Traceback (most recent call last):
File "&pyshell#15&", line 1, in &module&
returnme()()
23 TypeError: 'NoneType' object is not callable
如上所示,当直接在脚本中输入target,它只是告诉编译器(我想是编译器吧,因为我也不是很懂所谓编译器的部分),总之就是告诉那个不知道在哪个角落控制着所有python代码运行的“大脑”,在
0xA40D0位置(这个位置应该是指内存位置)存有一个function(方法)叫target;在target后面加上(),表示调用该方法,即输入target(),“大脑”便按照target方法所写的代码逐条执行,于是打印出了target字符串,并且“大脑”明白在0xA4268位置有一个叫returnme的方法;因为target对象调用后是会返回一个returnme方法,并且方法是可以被调用的,因此你可以直接这样书写target()(),“大脑”会逐条执行target中的代码,然后return一个returnme,因为多加了一个(),表示要对返回的returnme进行调用,于是再次逐条执行returnme中的代码,最后便能看到15、16的打印结果;而returnme方法是没有返回任何可调用的对象,因此当输入returnme()()时,“大脑”会报错。
下面我们可以来解释一下脚本2和脚本3的运行详情,之前说过,装饰器的工作原理就是脚本1代码所演示的那样。
@decorator
def target():
def decorator(target)():
注:python语法中以上写法是非法的,以上只是为了便于理解。
当你调用被装饰方法target时,其实首先被执行的是作为装饰器的decorator函数,然后“大脑”会把target方法作为参数传进去,于是:
def decorator(func):
print('this is decorator')
@decorator
def target():
print('this is target')
-------------------------------------------
实际运行情况:
首先调用decorator方法:decorator()
因为decorator方法含1个参数,因此将target传入:decorator(target)
运行代码“func()”,根据传入的参数,实际执行target(),结果打印出:this is target
运行代码"print('this is decorator')",结果打印出:this is decorator
对比脚本3的运行情况:
def decorator(func):
print('this is decorator')
@decorator
def target():
print('this is target')
-------------------------------------------
实际运行情况:
首先调用decorator方法:decorator()
因为decorator方法含1个参数,因此将target传入:decorator(target)
运行代码“func()”,根据传入的参数,实际执行target(),结果打印出:this is target
运行代码"print('this is decorator')",结果打印出:this is decorator
以上与脚本2中运行情况完全相同,接下来便是执行脚本2中target没有的(),也就是执行调用命令。
由于decorator(target)没有返回一个可以被调用的对象,因此“大脑”提示错误:'NoneType' object is not callable
如果你还不是很清楚,请看下面的等价关系:
@decorator
def target():
def decorator(target)():
target == decorator(target)
target() == decorator(target)()
假设有一个变量var=target,在将target赋值给var时,其实是将decorator(target)的调用结果赋值给var,因为var不具备调用性(not callable),因此执行var()时,编译器会报错它是个NoneType对象,不能调用。
综上所述,你大概已经能够明白所谓的装饰器是怎么一回事,它是怎么工作的。但脚本2和脚本3中的写法会带来一些困惑,这个困惑就是通过我们编写的decorator装饰器对target进行装饰后,将target变成了一个永远不能被调用的方法,或者说变成了一个调用就报错的方法。这跟我们的使用习惯以及对方法的认识是很不协调的,毕竟我们还是习惯一个方法天生注定可以被调用这种看法。所以为了满足我们对方法的定义,我们最好将作为装饰器的方法写成一个可以返回具有被调用能力的对象的方法。
def whatever():
print('this is whatever')
def decorator(func):
print('this is decorator')
return whatever  #1
@decorator
def target():
print('this is target')
------------------------------
输入:target
this is target
this is decorator
输入:target()
this is target
this is decorator
this is whatever
在#1的位置,你可以return任何可以被调用的方法或类,甚至你可以直接写成:
def whatever():
print('this is whatever')
def decorator(func):
return whatever
@decorator
def target():
print('this is target')
------------------------------
输入:target
结果:告诉编译器在内存某个位置有一个叫whatever的方法
输入:target()
结果:this is whatever
以上装饰器的作用就是将target方法,完完全全变成了whatever方法。但这只能完美解释装饰器的功效,在实际当中则毫无意义,为什么要辛辛苦苦写了一大堆代码之后,最后的结果却是完完整整地去调用另一个早已存在的方法?如果要调用whatever方法,我为什么不在我的代码中直接写whatever呢?
装饰器,顾名思义,它就是要对被装饰的对象进行一些修改,通过再包装来达到不一样的效果,尽管它可以将被装饰对象变得面目全非或者直接变成另一个对象,但这不是它被发明出来的主要意义,相反它被发明出来是帮助我们更高效更简洁的编写代码,对其他方法或类进行装饰,而非摧毁它们。例如对接收数据的检校,我们只要写好一个校验方法,便可以在其他许多方法前作为装饰器使用。
3. 常规的装饰器
从广义上来说,装饰器就是脚本1中,利用python可以将方法传参的特性,用一个方法去改变另一个方法,只要改变成功,都可以认为是合格的装饰器。但这是理论上的合格,毕竟装饰器是一种语法糖,应该是为我们带来便利而不是无用功,所以:
1. 将方法装饰成不能被调用,不好:
def decorator(func):
print('this is decorator')
2. 将原方法彻底消灭,直接变成另一个方法,不好:
def decorator(func):
return whatever
3. 保留原方法的同时再加上别的功能,不好:
def decorator(func):
print('this is decorator')
return whatever
在以上3种写法中,前两种明显不好,简直就是将装饰器变成了恶魔。而第3种写法,虽然看起来重新包装了被修饰方法,但却在方法调用前擅自执行了一些命令,即当你输入target,而非target()时:
def whatever():
print('this is whatever')
def decorator(func):
print('this is decorator')
return whatever  #1
@decorator
def target():
print('this is target')
------------------------------
输入:target
this is target
this is decorator
你尚未执行target(),编译器却已经打印了两行字符串。这并不是我们想要的,当我们在代码中写下target时,我们是不希望编译器立即执行什么命令,我们是希望编译器在碰到target()时才执行运算。而且如果我们并不希望返回whatever,我们只想要通过装饰器,使得target方法除了打印自己的&this is target&,再多打印一行&this is decorator”,所有代码只含有target和decorator两个方法,无其他方法介入,应该怎么办?
当你这样问的时候,其实就是已经开始了解装饰器存在的意义了。以下便是为解决这些问题的装饰器的常规写法:
def decorator(func):
def restructure():
print('this is decorator')
return restructure
@decorator
def target():
print('this is target')
是的,从最外层讲,以上代码其实只有两个方法,decorator和target——装饰和被装饰方法。但在decorator内部内嵌了一个方法restructure,这个内嵌的方法才是真正改造target的东西,而decorator其实只是负责将target传入。这里的restructure,相当于在脚本4中,被写在decorator外面的whatever角色,用装饰器的常规写法也可以写出脚本4的效果,如下:
def decorator(func):
print('this is decorator')
def whatever():
print('this is whatever')
return restructure
@decorator
def target():
print('this is target')
对比以上的写法你会发现,python的方法传参和类的继承性质很相似,在decorator之内,whatever之外,你可以写入任何代码,当执行target时,就开始初始化decorator,也就是执行decorator内部可以执行的代码;当执行target()时,会对初始化之后的decorator开始调用,因此这就要求decorator完成初始化之后必须返回一个具备调用性的对象。所以,除非你想要target方法初始化之前(实际上是对decorator进行初始化)就执行一些代码,否则不要在decorator和whatever中间插入代码。
正常情况下,当decorator完成初始化,应该return一个可调用对象,也就是脚本5中的restructure方法,这个方法就是替代target的克隆人,在restructure中你可以对target进行重写,或其他代码来包装target。因此你只是想初始化target的话(实际就是对restructure初始化),就应将你要初始化的代码写入到restructure内部去。另外你也可以在decorator中内嵌多个方法,或多层方法,例如:
def decorator(func):
def restructure():
print('this is decorator')
def whatever():
print('this is whatever')
return restructure
@decorator
def target():
print('this is target')
被decorator装饰的target最后会多打印一行'this is decorator'还是'this is whatever’,取决于decorator方法return的是它内部的哪一个方法(restructure或whatever),因此以上代码等价于以下写法:
执行target()
首先初始化decorator(target),结果返回一个restructure,即:target == decorator(target) == restructure。
然后调用,即:target() == decorator(target)() == restructure()
与类一样,当target被传入decorator之后,作为decorator内嵌方法是可以调用(继承)target方法的,这就是为什么restructure不用接受传参就可以改造target的原因。
注:在python中,以上多个()()的写法是非法的,这样写只是为了便于理解。
装饰器这个概念本来也可以设计成多个()这样的形式,但这样就破坏了python的基本写法,而且不好看,尤其当有多重装饰的时候,因此使用@置于方法前的设计是更加优雅和清晰的。
我之所以使用多个()这样并不被python支持的写法来阐述我对python的装饰器的理解,是因为我相信通过这样被python放弃的语法也能更好地帮助你理解python的继承、传参以及装饰器,尤其是带有参数的装饰器。
4. 装饰带有参数的方法
首先请看以下代码:
def target(x):
print('this is target %s'%x)
def decorator(func,x):
print('this is decorator %s'%x)
decorator(target,'!')
def decorator(func):
def restructure(x):
print('this is decorator %s'%x)
return restructure
@decorator
def target(x):
print('this is target %s'%x)
target('!')
target(x)中的参数x是如何传入,什么时候传入装饰器的?首先尝试以下代码:
def decorator(func):
#增加一行代码
def restructure(x):
print('this is decorator %s'%x)
return restructure
@decorator
def target(x):
print('this is target %s'%x)
target('!')
此时编译器会报错参数x没有被定义,也就是说在初始化decorator的时候,只有target方法被传入,其参数x='!'并没有传入。
现在让我们回顾一下之前说的装饰器工作原理,如下:
target == decorator(target) == restructure。
target() == decorator(target)() == restructure()
target(x) == decorator(target)(x) == restructure(x)
所以,你可以很清楚地了解到,作为target的参数,其实不是传给decorator,而是传给初始化完decorator之后return的restructure。因此,如果装饰器写成如下代码:&
def decorator(func):
def restructure():
#不带参数的方法
print('this is decorator %s'%x)
return restructure
此时你输入target('!'),编译器会告诉你restructure没有参数,但被传入了1个参数,这个被传入的参数就是x='!'。
所以你现在明白了被装饰的方法target(x),方法target和参数x是如何被传入的,所以你必须保证初始化decorator之后返回的对象restructure方法形参与被装饰的target方法形参相匹配,即:
如果定义为:def target(x)
则装饰器中:def restructure(x)
如果定义为:def target(x,y)
则装饰器中:def restructure(x,y)
你也许会发现如果想装饰器同时装饰target(x)和newtarget(x,y),以上写法就无法满足要求了。因此为了让装饰器可以适用于更多的对象,我们最好让装饰器写成如下形式:
def decorator(func):
def restructure(*x):
print('this is decorator')
return restructure
@decorator
def target(x):
print('this is target %s'%x)
@decorator
def newtarget(x,y):
print('this is target %s%s'%(x,y))
target('!')
newtarget('!','?')
利用python的带星号参数语法(*arg),你便可以传入任意数量的参数,你也可以设置带双星号的形参(**arg),便可以传入字典形式的参数,单星形参和双星形参可以同时使用,如:def restructure(*arg, **arg)。
5. 带有参数的装饰器
只要记住以上的装饰器工作原理,你便可以知道如何写出带有参数的装饰器,如:
def newdecorator(i):
def decorator(func):
def restructure(x):
print('this is decorator %s%s'%(i,x))
return restructure
return decorator
@newdecorator('?')
def target(x):
print('this is target %s'%x)
target('!')
-------------------------------------------------------
this is target !
this is decorator ?!
以上代码实际上是:
target(x) == newdecorator(i)(target)(x) == decorator(target)(x) == reconstructure(x)
同理,为了满足不同数量的参数传入,你也可以将newdecorator(i)写成newdecorator(*i, **ii)。
只要明白装饰器的设计原理,便可以自如地写出想要的装饰器,哪怕是多重、多参数的装饰器。
已发表评论数()
请填写推刊名
描述不能大于100个字符!
权限设置: 公开
仅自己可见
正文不准确
标题不准确
排版有问题
主题不准确
没有分页内容
图片无法显示
视频无法显示
与原文不一致Python装饰器
中的装饰器的概念经常会让人搞得一头雾水,所以今天就好好来分析一下python中的装饰器.
先看一个简单的装饰器用法:
&1 def decorator(func):
&2 & & print(&this is wrapper&)
&4 & & def wrapper():
&5 & & & & func()
&7 & & return wrapper
10 @decorator
11 def func():
12 & & print('this is func')
14 # func()
15 # print(func.__name__)
运行一下上述代码,看看输出.再把14,15行的注释放开,看看输出.
我们发现当14,15行注释掉时,输出this is wrapper。
15行注释放开,你会发现先func()的名字变成了wrapper。
@其实就是python中的一个语法糖.装饰器的本质以上述代码为例:
@decorator
def func():
& & print('this is func')
其实解释器执行了这么一句:func = decorator(func).这就是装饰器最本质最核心的东西了.
func作为参数传递给decorator,decorator的返回值是wrapper(),赋给func.就这样被修饰的函数func其实已经变成了另一个函数wrapper了.
下面我们就来看看几个例子:
1.你想写这样一个装饰器:
@decorator
def func(user):
& & print(&this&,user,& func&)
要怎么写呢?
我们来看看func = decorator(func).
第一步:我们知道decorator(func)应该返回一个函数.所以有如下代码.
def decorator(func):
& & def wrapper():
& & & & pass
& & return wrapper & &
第二步:装饰器是用来装饰函数的,你不能把原有的要装饰的func(user)的功能给弄没了啊.所以我们补全wrapper()
def decorator(func):
& & def wrapper(user):
& & & & print(&start decorate&,user)
& & & & func(user)
& & return wrapper
这时候你要装饰的func(user)就变成wrapper(user)啦.
完整代码:
2.一个装饰器想装饰好几个函数.
@decorator
def func(user):
& & print(user)
@decorator
def func2(user1,user2):
& & print(user1,&and&,user2)
要装饰的函数的参数你不确定有几个.可以用*args,**args表示任意参数就可以了.
def decorator(func):
& & def wrapper(*args,**kwargs):
& & & & print(&start decorate&)
& & & & func(*args)
& & return wrapper
@decorator
def func(user):
& & print(user)
@decorator
def func2(user1,user2):
& & print(user1,&and&,user2)
func('tim')
func2('joe','jimmy')
3.多个装饰器修饰一个函数
@decorator0
@decorator1
def func():
& & print(&this is func&)
实际上解释器执行func = decorator0(decorator1(func))
可以分两步:
1.decorator1(func)返回一个函数
2.decorator0()接受一个函数作为参数,并返回一函数.
所以就有了:
def decorator1(func):
& & def wrapper():
& & & & print(&decorator1!&)
& & & & func()
& & return wrapper
def decorator0(func):
& & def wrapper():
& & & & print(&decorator0!&)
& & & & func()
& & return wrapper
#func = decorator0(decorator1(func))
def decorator0(func):
& & def wrapper():
& & & & print(&decorator0!&)
& & & & func()
& & return wrapper
def decorator1(func):
& & def wrapper():
& & & & print(&decorator1!&)
& & & & func()
& & return wrapper
@decorator0
@decorator1
def func():
& & print(&this is func&)
print(func.__name__)
4.装饰器带参数
@decorator(num)
def func(user):
& & print(user)
实际上解释器执行func = decorator(num)(func)
也就是说你可以认为1.decorator(num)返回一个函数.2.返回的函数的类型是:以func为参数,返回值是一个函数.
所以第一步:
def decorator(num):
& & def wrapper():
& & & & pass
& & return wrapper
def decorator(num):
& & def wrapper(func):
& & & & def wrapper2(user):
& & & & & & print(num)
& & & & & & func(user)
& & & & return wrapper2
& & return wrapper
(window.slotbydup=window.slotbydup || []).push({
id: '2467140',
container: s,
size: '1000,90',
display: 'inlay-fix'
(window.slotbydup=window.slotbydup || []).push({
id: '2467141',
container: s,
size: '1000,90',
display: 'inlay-fix'
(window.slotbydup=window.slotbydup || []).push({
id: '2467143',
container: s,
size: '1000,90',
display: 'inlay-fix'
(window.slotbydup=window.slotbydup || []).push({
id: '2467148',
container: s,
size: '1000,90',
display: 'inlay-fix'简单 12 步理解 Python 装饰器
简单 12 步理解 Python 装饰器
英文:Simeon Franklin译文:伯乐在线 - 7even链接:/85056/好吧,我标题党了。作为 Python 教师,我发现理解装饰器是学生们从接触后就一直纠结的问题。那是因为装饰器确实难以理解!想弄明白装饰器,需要理解一些函数式编程概念,并且要对Python中函数定义和函数调用语法中的特性有所了解。使用装饰器非常简单(见步骤10),但是写装饰器却很复杂。虽然我没法让装饰器变得简单,但也许通过将问题进行一步步的讲解,可以帮助你更容易理解装饰器。由于装饰器较为复杂,文章会比较长,请坚持住!我会尽量使每个步骤简单明了,这样如果你理解了各个步骤,就能理解装饰器的原理。本文假定你具备最基础的 Python 知识,另外本文对工作中大量使用 Python 的人将大有帮助。此外需要说明的是,本文中 Python 代码示例是用 doctest 模块来执行的。代码看起来像是交互式 Python 控制台会话(>>> 和 … 表示 Python 语句,输出则另起一行)。偶然有以“doctest”开头的“奇怪”注释——那些只是 doctest 的指令,可以忽略。1. 函数在 Python 中,使用关键字 def 和一个函数名以及一个可选的参数列表来定义函数。函数使用 return 关键字来返回值。定义和使用一个最简单的函数例子:>>> def foo():...&&&& return 1>>> foo()1函数体(和 Python 中所有的多行语句一样)由强制性的缩进表示。在函数名后面加上括号就可以调用函数。2. 作用域在 Python 函数中会创建一个新的作用域。Python 高手也称函数有自己的命名空间。也就是说,当在函数体中遇到变量时,Python 会首先在该函数的命名空间中寻找变量名。Python 有几个函数用来查看命名空间。下面来写一个简单函数来看看局部变量和全局变量的区别。>>> a_string = 'This is a global variable'>>> def foo():...&&&& print locals()>>> print globals() # doctest: +ELLIPSIS{..., 'a_string': 'This is a global variable'}>>> foo() # 2{}内建函数 globals 返回一个包含所有 Python 能识别变量的字典。(为了更清楚的描述,输出时省略了 Python 自动创建的变量。)在注释 #2 处,调用了 foo 函数,在函数中打印局部变量的内容。从中可以看到,函数 foo 有自己单独的、此时为空的命名空间。3. 变量解析规则当然,以上并不意味着我们不能在函数内部使用全局变量。Python 的作用域规则是, 变量的创建总是会创建一个新的局部变量但是变量的访问(包括修改)在局部作用域查找然后是整个外层作用域来寻找匹配。所以如果修改 foo 函数来打印全部变量,结果将是我们希望的那样:>>> a_string = 'This is a global variable'>>> def foo():...&&&& print a_string # 1>>> foo()This is a global variable在 #1 处,Python 在函数 foo 中搜索局部变量 a_string,但是没有找到,然后继续搜索同名的全局变量。另一方面,如果尝试在函数里给全局变量赋值,结果并不是我们想要的那样:>>> 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 处,实际上是创建了一个和全局变量相同名字的局部变量,并且“覆盖”了全局变量。通过在函数 foo 中打印局部命名空间可以印证这一点,并且发现局部命名空间有了一项数据。在 #2 处的输出可以看到,全局命名空间里变量 a_string 的值并没有改变。4. 变量生命周期值得注意的是,变量不仅是在命名空间中有效,它们也有生命周期。思考下面的代码:>>> def foo():...&&&& x = 1>>> foo()>>> print x # 1Traceback (most recent call last):&&...NameError: name 'x' is not defined这个问题不仅仅是因为 #1 处的作用域规则(虽然那是导致 NameError 的原因),也与 Python 和很多其他语言中函数调用的实现有关。没有任何语法可以在该处取得变量 x 的值——它确确实实不存在!函数 foo 的命名空间在每次函数被调用时重新创建,在函数结束时销毁。5. 函数的实参和形参Python 允许向函数传递参数。形参名在函数里为局部变量。>>> def foo(x):...&&&& print locals()>>> foo(1){'x': 1}Python 有一些不同的方法来定义和传递函数参数。想要深入的了解,请参考 Python 文档关于函数的定义。来说一个简单版本:函数参数可以是强制的位置参数或者可选的有默认值的关键字参数。>>> def foo(x, y=0): # 1...&&&& return x - y>>> foo(3, 1) # 22>>> foo(3) # 33>>> foo() # 4Traceback (most recent call last):&&...TypeError: foo() takes at least 1 argument (0 given)>>> foo(y=1, x=3) # 52在 #1 处,定义了有一个位置参数 x 和一个关键字参数 y的函数。接着可以看到,在 #2 处通过普通传参的方式调用该函数——实参值按位置传递给了 foo 的参数,尽管其中一个参数是作为关键字参数定义的。在 #3 处可以看到,调用函数时可以无需给关键字参数传递实参——如果没有给关键字参数 y 传值,Python 将使用声明的默认值 0 为其赋值。当然,参数 x (即位置参数)的值不能为空——在 #4 示范了这种错误异常。都很清楚简单,对吧?接下来有些复杂了—— Python 支持在函数调用时使用关键字实参。看 #5 处,虽然函数是用一个关键字形参和一个位置形参定义的,但此处使用了两个关键字实参来调用该函数。因为参数都有名称,所以传递参数的顺序没有影响。反过来也是对的。函数 foo 的一个参数被定义为关键字参数,但是如果按位置顺序传递一个实参——在 #2 处调用 foo(3, 1),给位置形参 x 传实参 3 并给第二个形参 y 传第二个实参(整数 1),尽管 y 被定义为关键字参数。哇哦!说了这么多看起来可以简单概括为一点:函数的参数可以有名称或位置。也就是说这其中稍许的不同取决于是函数定义还是函数调用。可以对用位置形参定义的函数传递关键字实参,反过来也可行!如果还想进一步了解请查看 Python 文档。6. 内嵌函数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. 函数是 Python 中的一级对象在 Python 中有个常识:函数和其他任何东西一样,都是对象。函数包含变量,它并不那么特殊。>>> issubclass(int, object) # all objects in Python inherit from a common baseclassTrue>>> 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) # 33>>> apply(sub, 2, 1)1这个示例对你来说应该不陌生——add 和 sub 是标准的 Python 函数,都是接受两个值并返回一个计算的值。在 #1 处可以看到变量接收一个就像其他普通变量一样的函数。在 #2 处调用了传递给 apply 的函数 fun——在 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。也没有什么特殊语法——函数 outer 返回了并没有被调用的函数 inner。还记得变量的生命周期吗?每次调用函数 outer 的时候,函数 inner 会被重新定义,但是如果函数 ouer 没有返回 inner,当 inner 超出 outer 的作用域,inner 的生命周期将结束。在 #2 处将获得返回值即函数 inner,并赋值给新变量 foo。可以看到如果鉴定 foo,它确实包含函数 inner,通过使用调用操作符(双括号,还记得吗?)来调用它。虽然看起来可能有点怪异,但是目前为止并没有什么很难理解的,对吧?hold 住,因为接下来会更怪异!8. 闭包先不着急看闭包的定义,让我们从一段示例代码开始。如果将上一个示例稍微修改下:>>> 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 中的一个局部变量,当函数 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 # 1...&&&& 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 参数了。但是这么用闭包太没意思了!让我们再次从头开始,写一个装饰器。9. 装饰器装饰器其实就是一个以函数作为参数并返回一个替换函数的可执行函数。让我们从简单的开始,直到能写出实用的装饰器。>>> 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_func2请仔细看这个装饰器示例。首先,定义了一个带单个参数 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): # 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}装饰器和之前一样正常运行——返回了一个修改版函数,但在这次示例中通过检测和修正输入参数和返回值,将任何负值的 x 或 y 用 0 来代替,实现了上面的需求。是否这么做是见仁见智的,它让代码更加简洁:通过将边界检测从函数本身分离,使用装饰器包装它们,并应用到所有需要的函数。可替换的方案是:在每个数学运算函数返回前,对每个输入参数和输出结果调用一个函数,不可否认,就对函数应用边界检测的代码量而言,使用装饰器至少是较少重复的。事实上,如果要装饰的函数是我们自己实现的,可以使装饰器应用得更明确一点。10. 函数装饰器 @ 符号的应用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 的实用装饰器比较难,但用起来仅仅需要在函数前添加 @装饰器名 即可!11. args 和 *kwargs上面我们写了一个实用的装饰器,但它是硬编码的,只适用于特定类型的函数——带有两个参数的函数。内部函数 checker 接收两个参数,然后继续将参数传给闭包中的函数。如果我们想要一个能适用任何函数的装饰器呢?让我们来实现一个为每次被装饰函数的调用添加一个计数器的装饰器,但不改变被装饰函数。这意味着这个装饰器必须接收它所装饰的任何函数的调用信息,并且在调用这些函数时将传递给该装饰器的任何参数都传递给它们。碰巧,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',)第一个函数 one 简单的打印了传给它的任何位置参数(如果有)。在 #1 处可以看到,在函数内部只是简单的用到了变量 args —— *args 只在定义函数时用来表示位置参数将会保存在变量 args 中。Python 也允许指定一些变量,并捕获任何在 args 里的额外参数,如 #2 处所示。* 符号也可以用在函数调用时,在这里它也有类似的意义。在调用函数时,以 * 开头的变量表示该变量内容需被取出用做位置参数。再举例如下:>>> def add(x, y):...&&&& return x + y>>> lst = [1,2]>>> add(lst[0], lst[1]) # 13>>> add(*lst) # 23在 #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)312. 更通用的装饰器用学到的新知识,可以写一个记录函数参数的装饰器。为简单起见,仅打印到标准输出:>>> 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每一个函数的调用会有一行日志输出和预期的返回值。再聊装饰器如果你一直看到了最后一个实例,祝贺你,你已经理解了装饰器!你可以用新掌握的知识做更多的事了。你也许考虑需要进一步的学习:Bruce Eckel 有一篇很赞的关于装饰器文章(/weblogs/viewpost.jsp?thread=240808),他使用了对象而非函数来实现了装饰器。你会发现 OOP 代码比纯函数版的可读性更好。Bruce 还有一篇后续文章 providing arguments to decorators,用对象实现装饰器也许比用函数实现更简单。最后,你可以去研究一下内建包装函数 functools,它是一个在装饰器中用来修改替换函数签名的装饰器,使得这些函数更像是被装饰的函数。
发表评论:
TA的最新馆藏

我要回帖

更多关于 python 多个装饰器 的文章

 

随机推荐