彻底理解Python中的闭包和装饰器(下)
上篇讲了Python中的闭包,本篇要讲的装饰器就是闭包的一个重要应用。
如果你还不知道什么是闭包,猛戳这里阅读:彻底理解Python中的闭包和装饰器(上)
什么是装饰器
装饰器的作用是在不修改函数定义的前提下增加现有函数的功能,比如打印函数名称、计算函数运行时间等。装饰器的本质是一个闭包。
下面是一段典型的装饰器定义代码:
def log(func):
def wrapper(*args, **kw):
print('call %s():' % func.__name__)
return func(*args, **kw)
return wrapper
还记得闭包的两个特征吗?两个嵌套的函数,内函数引用了外函数的局部变量,外函数返回了内函数的引用。
在上面的代码中,内函数wrapper()引用了外部函数的变量func,这个变量是一个函数的引用。func.__name__的意义是获得__name__属性,即函数的名称。所以该装饰器的功能是在原函数调用时输出函数名称。
我们随便写一个函数在shell中输出一句话:
def myfunc():
print('This is original function!')
按照通常的闭包使用方式,要用一个变量保存内函数的引用,然后使用这个函数指针变量调用内函数。所以上面的装饰器可以这样使用:
f = log(myfunc)
f()
运行结果如下:
call myfunc():
This is original function!
仍然牢记闭包的性质,定义函数指针时内函数仍未执行,直到用指针调用内函数。调用时,首先print句输出函数名称,接着return句中实际调用了func函数,即参数传入的myfunc函数。
这里调用内函数时没有传入任何参数,但将内函数定义为(*args, **kw)就可以接受任意类型的参数,具有通用性。
上面的用法完全正确,但装饰器的通常用法并不是这样,而是有更简单的写法。
装饰器的@语法
上面的用法是闭包的一般用法,但在装饰器中,我们一般使用@语法。
写法十分简单,只要在“被装饰”的函数定义前加上@闭包名称即可,如上面的装饰器应当这样写:
def log(func):
def wrapper(*args, **kw):
print('call %s():' % func.__name__)
return func(*args, **kw)
return wrapper
@log
def myfunc():
print('This is original function!')
其中的@log产生了如下效果:
myfunc = log(myfunc)
这表示myfunc()函数本身的指针(引用)代替了通常闭包中创建函数指针的一步。因此,每当直接调用myfunc函数时就会执行装饰器的闭包,从而产生了对原函数进行“装饰”的效果。
当调用一次原函数时:
myfunc()
输出为:
call myfunc():
This is original function!
读者一开始看到@语法时可能会觉得难以理解,但是将它还原成函数调用的形式,然后像通常的闭包一样理清执行顺序,就会简单许多。
注意:如果一个函数定义前使用了装饰器,那么装饰器本身会完全代替原函数。所以在使用装饰器时必须注意两点:
- 函数功能不变。
- 函数返回值不变。
为了做到第1点,装饰器必须接收一个函数引用的变量,并在闭包中调用且只调用一次。
为了做到第2点,装饰器的内函数必须返回原函数的返回值。
在本文最后的例子中我们还将继续探讨第2点。
自定义装饰器参数
正如上文所说,为了使原函数功能不变,装饰器必须接收一个原函数引用的变量作为参数。那么如果想传入其他参数怎么办呢?比如说,我想对不同的函数打印不同的字符串日志,该怎么办?
这时就必须再嵌套一层函数闭包,外层接收自定义参数,内层接收函数引用参数。代码如下:
import functools
def log(text):
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kw):
print('%s call %s:' % (text, func.__name__))
return func(*args, **kw)
return wrapper
return decorator
@log('execute func1')
def func1():
print('This is function 1')
@log('execute func2')
def func2():
print('This is function 2')
func1()
func2()
输出为:
execute func1 call func1:
This is function 1
execute func2 call func2:
This is function 2
@语句和调用func1()和func2()的语句相当于:
f1 = log('execute func1')(func1)
f1()
f2 = log('execute func2')(func2)
f2()
这实际上是两层嵌套的函数调用。'execute func1'是传给log的参数,log('execute func1')的返回值返回值是内层函数的引用,因此后面还可以再加一个括号,将参数传入内层函数decorator(func)。
这里还多做了一件事:
@functools.wraps(func)
这是Python内置的一个装饰器,作用是将外层函数的属性复制给内层函数,这样输出的函数名称也会是传进来的参数func的值。
然而在测试时发现,不加这句话输出结果并没有变化。其中原因暂时没有搞清楚。为了避免问题,通常还是需要加上这个装饰器。
两个例子
再举两个例子,补充说明一下其他问题。这两个都是廖雪峰教程中的例子。
计时器
设计一个计时器装饰器,计算函数的运行时间。读者不妨自己尝试一下,再来看答案。
Tips:Python中计时可以使用time模块
代码如下:
def metric(func):
@functools.wraps(func)
def decorator(*args, **kw):
start_time = time.time()
ret = func(*args, **kw)
end_time = time.time()
print('%s execution time is %f seconds' % (func.__name__, end_time - start_time) )
return ret
return decorator
@metric
def fsum(x, y):
time.sleep(5.0)
return x + y
res = fast(11, 22)
print('res = ', res)
在之前的例子中,我们的内函数返回值是这样写的:
return func(*args, **kw)
物理上,执行这句代码实际上创建了中间的临时变量来保存返回值。为了免去繁杂的底层机理,我们可以直观理解为,首先调用了func()函数,将func()的返回值返回给这句return“后面”的地方,再由这句return返回给调用内函数处。
这样写保证了之前所要求的原函数返回值不变,同时节约了代码。
然而在计时器中这样写遇到了麻烦。因为代码的缩略,一旦返回,函数就结束了呀,我们怎么读取函数结束时的时间呢?笔者一开始也被困扰许久,后来发现其实解决方法十分简单,只要用一个变量先保存返回值,最后再return这个变量不就行了嘛。
究其原因,是因为一开始没有透彻理解代码的意思,只是遵从书写的“规则”而已。所以学习语言不能死板,而是要深入理解原理。
支持加/不加自定义参数
设计一个装饰器,使其既支持@log,又支持@log('execute')。
看到题目的第一反应是使用默认参数,但仔细一想就会发现这与默认参数不是一回事。当使用@log时,传入的第一个参数是函数引用本身,当使用@log('execute')时,传入的第一个参数是自定义的字符串。因此不能简单地使用默认参数解决。
笔者的解决方案是:判断参数类型,从而决定返回哪一个函数。完整代码如下:
def log(logarg):
def decorator(func):
@functools.wraps(func)
def wrapper1(*args, **kw):
print('%s call %s:' % (logarg, func.__name__))
return func(*args, **kw)
return wrapper1
def wrapper2(*args, **kw):
print('call %s():' % logarg.__name__)
return logarg(*args, **kw)
if isinstance(logarg, str):
return decorator
else:
return wrapper2
@log
def func1():
print('This is function 1')
@log('execute func2')
def func2():
print('This is function 2')
func1()
func2()
小结
- 装饰器是一个闭包,其作用是增强“被装饰”函数的功能
- 为了保证原函数功能不变,必须在装饰器中调用且只调用一次原函数,且返回原函数的返回值
- 使用嵌套的闭包设计自定义参数的装饰器,并使用
@functools.wraps(func)将函数属性赋值给内层闭包。
彻底理解Python中的闭包和装饰器(下)的更多相关文章
- 轻松理解python中的闭包和装饰器 (下)
在 上篇 我们讲了python将函数做为返回值和闭包的概念,下面我们继续讲解函数做参数和装饰器,这个功能相当方便实用,可以极大地简化代码,就让我们go on吧! 能接受函数做参数的函数我们称之为高阶函 ...
- 轻松理解python中的闭包和装饰器(上)
继面向对象编程之后函数式编程逐渐火起来了,在python中也同样支持函数式编程,我们平时使用的map, reduce, filter等都是函数式编程的例子.在函数式编程中,函数也作为一个变量存在,对应 ...
- python中的闭包和装饰器
重新学习完了函数,是时候将其中的一些重点重新捋一捋了,本次总结的东西只有闭包和装饰器 1.闭包 闭包是python函数中的一个比较重要功能,一般闭包都是用在装饰器上,一般学完闭包就会去学习装饰器,这俩 ...
- 21.python中的闭包和装饰器
python中的闭包从表现形式上定义(解释)为:如果在一个内部函数里,对在外部作用域(但不是在全局作用域)的变量进行引用,那么内部函数就被认为是闭包(closure). 以下说明主要针对 python ...
- Python 中的闭包与装饰器
闭包(closure)是函数式编程的重要的语法结构.闭包也是一种组织代码的结构,它同样提高了代码的可重复使用性. 如果在一个内嵌函数里,对在外部函数内(但不是在全局作用域)的变量进行引用,那么内嵌函数 ...
- python中的闭包与装饰器
#原创,转载请留言联系 装饰器的本质就是闭包,所以想知道装饰器是什么,首先要理解一下什么是闭包. 闭包 1. 外部函数返回内部函数的引用.2. 内部函数使用外部函数的变量或者参数. def outer ...
- 聊聊Python中的闭包和装饰器
1. 闭包 首先我们明确一下函数的引用,如下所示: def test1(): print("--- in test1 func----") # 调用函数 test1() # 引用函 ...
- python中函数总结之装饰器闭包
1.前言 函数也是一个对象,从而可以增加属性,使用句点来表示属性. 如果内部函数的定义包含了在外部函数中定义的对象的引用(外部对象可以是在外部函数之外),那么内部函数被称之为闭包. 2.装饰器 装饰器 ...
- 理解Python中的闭包
1.定义 闭包是函数式编程的一个重要的语法结构,函数式编程是一种编程范式 (而面向过程编程和面向对象编程也都是编程范式).在面向过程编程中,我们见到过函数(function):在面向对象编程中,我们见 ...
- 第十七篇 Python函数之闭包与装饰器
一. 装饰器 装饰器:可以拆解来看,器本质就是函数,装饰就是修饰的意思,所以装饰器的功能就是为其他函数添加附加功能. 装饰器的两个原则: 1. 不修改被修饰函数的源代码 2. 不修改被修饰函数的调用方 ...
随机推荐
- 安装Alertmanager,nginx配置二级路径代理访问
安装配置 Alertmanager wget https://github.com/prometheus/alertmanager/releases/download/v0.20.0/alertman ...
- Jenkins配置项目构建的钉钉通知
在任意一个钉钉群里创建自定义的钉钉机器人,然后能够看到钉钉开放的webhook,复制webhook. Jenkins中安装钉钉插件,然后在项目的配置当中,构建后操作里添加钉钉报警. 安装钉钉通知插件 ...
- linux系统安装Confluence
转载网址:https://blog.yupaits.com/blog/record/linux-confluence.html#安装步骤 Confluence简介 Confluence是一个专业的企业 ...
- CentOS obs直播RTMP协议推流到Nginx流媒体服务器到VUE使用HLS直播
1. 发布打包前端管理平台(ant-design-vue) 2. 发布打包后端(.NET6-WebApi) .NET6 webAPI orm: free sql 数据库:mysql5.7 3. 发布打 ...
- POJ2955 Brackets (区间DP)
很好的区间DP题. 需要注意第一种情况不管是否匹配,都要枚举k来更新答案,比如: "()()()":dp[0][5]=dp[1][4]+2=4,枚举k,k=1时,dp[0][1]+ ...
- 驱动开发:内核枚举ShadowSSDT基址
在笔者上一篇文章<驱动开发:Win10枚举完整SSDT地址表>实现了针对SSDT表的枚举功能,本章继续实现对SSSDT表的枚举,ShadowSSDT中文名影子系统服务描述表,SSSDT其主 ...
- 魔改editormd组件,优化ToC渲染效果
前言 我的StarBlog博客目前使用 editor.md 组件在前端渲染markdown文章,但这个组件自动生成的ToC(内容目录)不是很美观,我之前魔改过一个树形组件 BootStrap-Tree ...
- Linux实战笔记_ 如何远程访问Kali?
注:基于2018年安装的kali版本. 启动ssh服务 /etc/init.d/ssh start 或 service ssh start #启动ssh服务 /etc/init.d/ssh statu ...
- C语言经典编程100题
程序1] 题目:古典问题:有一对兔子,从出生后第3个月起每个月都生一对兔子,小兔子长到第三个月 后每个月又生一对兔子,假如兔子都不死,问每个月的兔子总数为多少? 1.程序分析: 兔子的规律为数列1,1 ...
- 齐博x1如何调试查找全站的表单提交接口参数
H5.PC.WAP端的所有提交POST表单操作都是可以当作接口来用的. 比如我们通过PC或WAP浏览器打开相应要修改的界面,然后浏览器进入开发者模式,就可以追踪到所提交的变量参数.你在APP里边只要指 ...