Python培训
400-996-5531
今天我们python培训班主要收集了python装饰器方面的知识,为大家做一详细讲述,如果还有人不知道python装饰器是什么、有什么用等问题,请看本文。
Python 是一种强类型语言,但是变量的类型却是动态变化的。虽然这会带来很多好处,但是同时这也意味着更容易引入 bug。对于静态语言,例如 Java, 这些 bug 在编译阶段就可以被发现。因而,你可能希望在对传入或返回的数据进行一些自定义的的检查。装饰器就可以让你非常容易地实现这个需求,并一次性将其应用到多个函数上。一旦你掌握了如何写装饰器,你就能够从其使用的简单的语法中获益颇丰,你可以为语言添加新的语义使其使用更加简单。接下来最棒的就是你可以自己扩展 Python 语法。事实上,很多开源框架都是使用的这样的方式。
Python装饰器是非常不错的特性,熟练掌握装饰器会让你的编程思路更加宽广,程序也更加pythonic。下面就让我们一起来探讨一下python的装饰器吧。
装饰器的存在是为了适用两个场景,一个是增强被装饰函数的行为,另一个是代码重用。
先看一个例子,直观的感受一下:
import
time
def
out_wrapper(func):
def
inner_wrapper():
start_time = time.time()
func()
stop_time = time.time()
(
'Used time {}'
.format(stop_time-start_time))
return
inner_wrapper
@out_wrapper
def
test1():
time.sleep(
1
)
(
'I am test1!'
)
输出:
I am test1!
Used
time
1.0000572204589844
这个装饰器是用来计算函数执行时间的。原本test1函数只是休眠1秒,然后输出字符串,但是在使用装饰器(out_wrapper)后,它的功能多了一项:输出执行时间。 这是一个最简单的装饰器,实现了 “增强被装饰函数的行为”。而我们需要思考的是为什么装饰器是这个样子的? 那是因为行为良好的装饰器必须要遵守两个原则:
1、不能修改被装饰函数的代码;
2、不能修改被装饰函数的调用方式;
这并不难以理解,因为在生产环境中如果我们要给某个函数添加功能,最好不要修改该函数的源码,因为可能造成意想不到的影响,或者这个代码是一个大神写的,你根本不知从何改起;同时你也不能修改其调用方式,因为你不知道程序中有多少地方调用了此函数。
那么我们从函数和函数名说起吧。
def
func(name):
(
'I am {}!'
.format(name))
func(
'li'
)
y = func
y(
'liu'
)
输出:
I am li!
I am liu!
定义函数func,调用函数func,将函数名func赋值给y,调用y。y=func 表明:函数名可以赋值给变量,并且并不影响调用。
这其实和整数、数字是一样的:
a =
1
b = a
(a, b)
明白了这一点,下面再说说高阶函数: 高阶函数满足如下两个条件中的任意一个: a. 可以接收函数名作为实参; b. b.返回值中可以包含函数名;
其实python标准库中的map和filter等函数就是高阶函数。
l = [
1
,
2
,
4
]
r = map(
lambda
x: x*
3
, l)
for
i
in
r:
(i)
自定义一个能返回函数的函数,也是高阶函数
def
f(l):
return
map(
lambda
x: x*
5
, l)
a = f(l)
for
i
in
a:
(i)
有了这些基础,我们就可以尝试实现一下类似装饰器的功能了。
def
out
(func):
(
'Add a function.'
)
return
func
def
test1():
time.sleep(
1
)
(
'I am test1!'
)
temp =
out
(test1)
temp()
输出:
Add
a
function
.
I am test1!
还是第一个例子中的test1函数,我们定义了一个函数out,out接收一个函数名然后直接返回该函数名。这样,我们实现了不修改原函数test1,并且添加了一个新功能的需求,但是缺陷就是调用方式改变了。如何解决这个问题呢?其实很简单,相信 a = a * 3 这样的表达式我们都见过,那么上述代码中的temp = out(test1) 同样可以修改为 test1 = out(test1),这样我们就完美的解决了问题:既添加了新功能又没有修改原函数和其调用方式。修改后的代码如下:
def
out
(func):
(
'Add a function.'
)
return
func
def
test1():
time.sleep(
1
)
(
'I am test1!'
)
test1 =
out
(test1)
test1()
只是美中不足的事每次需要使用装饰器的时候,都要在写一句类似test1 = out(test1) 的代码。python为了简化这种情况,提供了一个语法糖@,在每个被装饰的函数上方使用这个语法糖就可以省掉这一句代码test1 = out(test1)。如下:
def
out
(func):
(
'Add a function.'
)
return
func
@out
def
test1():
time.sleep(
1
)
(
'I am test1!'
)
# test1 = out(test1)
test1()
至此,我们搞清楚了装饰器的工作原理,但是对比开篇的例子,还是有些不一样。这又是为什么呢? 开篇例子实现的是输出被装饰函数的执行时间,那么必须在函数执行之前记录一下时间,函数执行之后记录一下时间,这样才能计算出函数的执行时间,但是我们现在是直接返回了函数名,这样函数调用后我们就没办法做任何事情了,所以此时我们需要在嵌套一层函数,将实现额外功能的部分写在内层函数中,然后将这个内层函数返回即可。这也是为什么装饰器都是嵌套函数的原因。 另外,开篇的例子并没有返回值,也没有参数,要对既有参数又有返回值的函数进行装饰的话,还需要进一步完善。 能够处理返回值的装饰器:
import
time
def
out_wrapper(func):
def
inner_wrapper():
start_time = time.time()
result = func()
stop_time = time.time()
(
'Used time {}'
.format(stop_time - start_time))
return
result
return
inner_wrapper
@out_wrapper
def
test1():
time.sleep(
1
)
(
'I am {test1}!'
)
return
'test1 return'
x = test1()
(x)
输出:
I am {test1}!
Used
time
1.0000572204589844
test1
return
能够处理参数的装饰器:
def
out_wrapper(func):
def
inner_wrapper(*args, **kwargs):
start_time = time.time()
result = func(*args, **kwargs)
stop_time = time.time()
(
'Used time {}'
.format(stop_time - start_time))
return
result
return
inner_wrapper
@out_wrapper
def
test1(args):
time.sleep(
1
)
(
'I am {}!'
.format(args))
return
'test1 return'
x = test1(
'li'
)
y = test1(
'liu'
)
(x, y)
输出:
I am li!
Used
time
1.0000569820404053
I am liu!
Used
time
1.0000572204589844
test1
return
test1
return
装饰器的本质是函数,其参数是另一个函数(被装饰的函数)。 装饰器通常会额外处理被装饰的函数,然后把它返回,或者将其替换成另一个函数或可调用对象。行为良好的装饰器可以重用,以减少代码量。
以上主要以一步一步演进的方式介绍了装饰器的工作原理以及使用,就已经能够满足日常对装饰器的使用了。但是,若想真正理解装饰器,并进行更高阶的使用还要了解其他一些知识:
python中,函数是一等对象;
区分导入时执行和运行时执行;
闭包和 nonlocal 声明等。
下面我们逐个介绍:
第一点,在 Python 中,函数是一等对象,这在上一篇其实已经提到了。“一等对象”满足下述条件:
a.在运行时创建;
b.能赋值给变量或数据结构中的元素;
c.能作为参数传给函数;
d.能作为函数的返回结果;
Python 中的整数、字符串和字典等都是一等对象,大家对比着理解一下,在此不再过多介绍。
第二点,函数装饰器在导入模块时立即执行,而被装饰的函数只在明确调用时运行。看下面的例子:
al = []
def
deco(func):
(
'running deco and parm is{}'
.format(func))
al.append(func)
return
func
@deco
def
f1():
(
'running f1()'
)
@deco
def
f2():
(
'running f2()'
)
def
f3():
(
'running f3()'
)
def
main():
(
'running main()'
)
(
'al ->'
, al)
f1()
f2()
f3()
if
__name__==
'__main__'
:
main()
输出:
running deco
and
parm
is
<
function
f1 at
0x00000000006C2AE8
>
running deco
and
parm
is
<
function
f2 at
0x00000000011E6510
>
running main()
al -> [<
function
f1 at
0x00000000006C2AE8
>, <
function
f2 at
0x00000000011E6510
>]
running f1()
running f2()
running f3()
我们简单定义了一个装饰器,把传进来的参数(函数名)添加到列表,然后再返回该函数名。观察输出结果,在运行main函数之前,deco就已经运行了(输出了2次,因为f1和f2都用deco进行了装饰),之后对列表的输出也印证了这一点,而不管是被装饰的f1、f2还是未被装饰的f3都是在明确的调用之后才执行的。这就是Python 程序员所说的导入时和运行时之间的区别。
第三点,闭包可以说是行为良好的装饰器赖以生存的关键。闭包其实并不难以理解,因为它只存在于嵌套函数中。还是看例子:
def
get_averager():
nums = []
def
averager(new_value):
nums.append(new_value)
total = sum(nums)
return
total/len(nums)
return
averager
avg = get_averager()
(avg)
(avg(
10
))
(avg(
11
))
(avg(
12
))
输出:
<function get_averager.
<locals>
.averager at 0x0000000000672AE8>
10.0
10.5
11.0
定义一个嵌套函数,作用是计算累计传入参数的平均值。通过输出结果我们可以看到avg是getaverager()返回的averager,通过不断的调用avg(),返回当前的平均值。这里面有个问题是我们之前没有探讨的:nums是外层函数中的变量,那么在getaverager()返回完毕之后,它的本地作用域应该一并消失,那为什么avg中还可以使用呢?这就是闭包的作用了。其实,闭包就是指函数作用域延伸了(从外层函数延伸到内层函数)。延伸的值保存在内层函数的code属性中:
>>>
def
get_averager():
nums = []
def
averager(new_value):
nums.append(new_value)
total = sum(nums)
return
total/len(nums)
return
averager
>>> avg = get_averager()
>>> avg.__code__.co_freevars
(
'nums'
,)
我们注意到上面这个例子把所有值存储在历史列表中,然后在每次调用 averager 时使用 sum 求和。更好的实现方式是,只存储目前的总值和元素个数,然后使用这两个数计算均值。依照这个思路我们可以对代码进行优化,但是在此之前我们需要看一个简单的例子:
>>> b =
99
>>>
def
f(t):
(t)
(b)
b =
2
>>> f(
10
)
各位可以想象一下,这个输出会是什么?
10
99
是不是这个?其实不然,真实的结果是这样:
>>> f(
10
)
10
Traceback
(most recent call
last
):
File
"<pyshell#42>"
, line
1
,
in
<module>
f(
10
)
File
"<pyshell#41>"
, line
3
,
in
f
(b)
UnboundLocalError
:
local
variable
'b'
referenced before assignment
>>>
这个结果可能让你惊讶,但事实就是如此。因为Python 编译函数的定义体时,由于b在函数中给它赋值了,因此它判断 b 是局部变量。后面调用 f(10) 时, f 的定义体会获取并打印局部变量 b的值,但是尝试获取局部变量 b的值时,发现 b 没有绑定值。这不是缺陷,而是设计选择:Python 不要求声明变量,但是假定在函数定义体中赋值的变量是局部变量。了解了这一点,我们来优化一下之前计算平均值的例子:
def
get_averager():
count =
0
total =
0
def
averager(new_value):
count +=
1
total += new_value
return
total / count
return
averager
逻辑上看没啥问题,但是有了之前的铺垫,你可能会发现一些问题:内层函数对外层函数中的变量进行了重新赋值。我们来运行一下代码,就会发现报错:
UnboundLocalError
:
local
variable
'count'
referenced before assignment
而优化前的例子没遇到这个问题,因为nums是列表,我们只是调用 ums.append,也就是说,我们利用了列表是可变的对象这一事实。但是对数字、字符串、元组等不可变类型来说,只能读取,不能更新。如果尝试重新绑定,例如 count = count + 1,其实会隐式创建局部变量 count。 为了解决这个问题,Python 3 引入了 nonlocal 声明,如果为 onlocal 声明的变量赋予新值,闭包中保存的绑定会更新。
>>>
def
get_averager():
count =
0
total =
0
def
averager(new_value):
nonlocal
count, total
count +=
1
total += new_value
return
total / count
return
averager
>>> avg = get_averager()
>>> avg(
10
)
10.0
>>> avg(
11
)
10.5
>>> avg(
12
)
11.0
以上这三点就是对前文装饰器基础知识的补充,希望对大家有所帮助。
恭喜你阅读完了本文,相信通过本文的阅读,你已经知道了python装饰器是什么、有什么用、怎么用等问题,掌握python装饰器相关的问题,可以让你的编程工作事半功倍,不信的话就快点试试吧。如果你还有python相关的问题,欢迎随时来达内python培训机构进行咨询。
免责声明:内容和图片源自网络,版权归原作者所有,如有侵犯您的原创版权请告知,我们将尽快删除相关内容。
填写下面表单即可预约申请免费试听! 怕学不会?助教全程陪读,随时解惑!担心就业?一地学习,可全国推荐就业!
Copyright © 京ICP备08000853号-56 京公网安备 11010802029508号 达内时代科技集团有限公司 版权所有
Tedu.cn All Rights Reserved