作为程序员,起码要知道的 Python 修饰器!
Python修饰器是个非常强大的概念,可以用一个函数去“包装”另一个函数。修饰器的思想,就是把函数中除了正常行为之外的部分抽象出去。这样有很多好处,如很容易进行代码复用,并且能遵守科里定律(即一次只做一件事)。
学习怎样编写修饰器,可以大幅度增加代码的可读性。它能改变函数的行为,而无需实际去改变函数的代码(如添加日志行等)。
而修饰器是Python中非常常用的工具,用过Flask、Click等框架的人,都应该很熟悉修饰器,但许多人只知道怎么用,却不知道该怎么写。如果你也不知道怎么写,那这篇文章,正是为你准备的!
修饰器的原理
首先我们来看一个Python修饰器的例子。下面是个非常简单的例子,演示了修饰器的使用方法。
@my_decoratordef hello(): print('hello')
在Python中定义的函数实际上是个对象。
上面的函数Hello是个函数对象。@my_decorator实际上也是个函数,它接受hello对象,并返回另一个对象给解释器。修饰器返回的对象会成为实际的hello函数。
本质上这跟正常的函数一样,如hello = decorate(hello)。我们传给函数decorate一个函数,decorate可以随便怎样去用这个函数,然后返回另一个对象。修饰器可以干脆吞掉那个函数,或者如果需要,还可以返回某个不是函数的东西。
编写自己的修饰器
前面说过,修饰器就是个简单的函数,它接受函数作为输入,返回一个对象。因此,编写修饰器实际上只需要定义一个函数。
def my_decorator(f):
return 5
任何函数都可以用作修饰器。这个例子中,修饰器被传入一个函数,然后返回一个不同的东西。它完全吃掉了输入的函数,并且永远返回5。
@my_decorator
def hello():
print('hello')
>>> hello()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: 'int' object is not callable
'int' object is not callable
由于修饰器返回的是INT,INT不是Callable,因此不能作为函数调用。别忘了,修饰器的返回值会替换掉Hello。
>>> hello
5
绝大多数情况下,我们希望修饰器返回的对象能够模拟被修饰的函数。这就是说,修饰器返回的对象应该也是个函数。
例如,假设我们希望在每次函数被调用时输出一行文字,我们可以写个函数输出信息,然后再调用输入的函数。但这个函数必须由修饰器返回。因此我们的函数得写成嵌套的形式,如:
def mydecorator(f): # f is the function passed to us from python
def log_f_as_called():
print(f'{f} was called.')
f()
return log_f_as_called
从上面的代码可以看出,我们定义了个嵌套的函数,该嵌套函数被修饰器返回。这样,Hello函数依然可以被当做函数调用,调用者并不知道Hello被修饰过了。现在Hello函数可以这样定义:
@mydecorator
def hello():
print('hello')
输出如下:
>>> hello()
<function hello at 0x7f27738d7510> was called.
hello
(注意:<function hello at 0x7f27738d7510>中的数字代表内存地址,因此每个人的都会不一样。)
正确包装函数
如果有必要,函数可以被修饰多次。这种情况下,修饰器会引起连锁反应。本质上,每个修饰器的返回值都会传递给上一层的修饰器,直到最顶层。例如,下面的代码:
@a
@b
@c
def hello():
print('hello')
解释器实际上执行的是hello = a(b(c(hello))),所有修饰器都会互相包装。你可以用之前定义的修饰器测试这一点,使用两次就好:
@mydecorator
@mydecorator
def hello():
print('hello')
>>> hello()
<function mydec.<locals>.a at 0x7f277383d378> was called.
<function hello at 0x7f2772f78ae8> was called.
hello
你会注意到,第一个修饰器包装了第二个,然后产生了不同的输出。
有意思的是,第一行输出的结果是<function mydec.<locals>.a at 0x7f277383d378>,而不是像第二行那样输出我们期待的信息:<function hello at 0x7f2772f78ae8>。
这是因为修饰器返回的是个新函数,这个新函数不叫Hello。作为例子来说这无所谓,但实际上这可能会让测试失败,或者让试图自省函数属性的过程失败。
所以,如果修饰器的思想是模拟被修饰的函数的行为,那么它也应该模拟被修饰函数的样子。幸运的是,有个Python标准库functools模块提供的修饰器wraps能做到这一点:
import functools
def mydecorator(f):
@functools.wraps(f) # we tell wraps that the function we are wrapping is f
def log_f_as_called():
print(f'{f} was called.')
f()
return log_f_as_called
@mydecorator
@mydecorator
def hello():
print('hello')
>>> hello()
<function hello at 0x7f27737c7950> was called.
<function hello at 0x7f27737c7f28> was called.
hello
现在,新的函数看起来跟它修饰的函数一模一样。但是,我们这个修饰器依然只能修饰不返回任何值,并且不接受任何输入的函数。如果想让它更通用,就必须负责传递函数参数,并且返回同样的值。可以这样修改:
import functools
def mydecorator(f):
@functools.wraps(f) # wraps is a decorator that tells our function to act like f
def log_f_as_called(*args, **kwargs):
print(f'{f} was called with arguments={args} and kwargs={kwargs}')
value = f(*args, **kwargs)
print(f'{f} return value {value}')
return value
return log_f_as_called
现在每次调用都会产生输出,包含函数接收到的所有输入,以及函数的返回值。现在可以用它来修饰任意函数,获得关于函数的输入和输出的调试信息,而用不着手动编写日志代码了。
给修饰器增加变量
如果你写的修饰器不是只给自己用,而是想在产品代码里使用,那你可能需要把所有print语句换成日志输出语句。那样的话就需要定义日志的级别。
都定义成DEBUG级别也许没问题,但还是能根据函数选择级别最好。我们可以给修饰器提供变量,以改变修饰器的行为。例如:
@debug(level='info')
def hello():
print('hello')
上面的代码可以指定,被修饰的函数应该以info级别输出日志,而不是DEBUG级别。这个功能的实现方法是写个函数,返回修饰器。
没错,修饰器也是个函数。所以这段代码实质上是hello = debug('info')(hello)。两对括号看起来很奇怪,不过本质上说,DEBUG是个返回函数的函数。因此,修改我们之前的修饰器,我们还需要一层嵌套,这样代码如下所示:
import functools
def debug(level):
def mydecorator(f)
@functools.wraps(f)
def log_f_as_called(*args, **kwargs):
logger.log(level, f'{f} was called with arguments={args} and kwargs={kwargs}')
value = f(*args, **kwargs)
logger.log(level, f'{f} return value {value}')
return value
return log_f_as_called
return mydecorator
上面的修改将DEBUG变成了一个返回修饰器的函数,返回的修饰器会使用正确的日志级别。这段代码看起来不太好看,而且嵌套太多了。
有个很酷的小技巧我非常喜欢,就是给DEBUG添加默认的level参数,返回一个部分函数。部分函数是“不完整的函数调用”,它包含一个函数和一些参数,这样部分函数可以作为一个整体来传递,而无需调用实际的函数。
import functools
def debug(f=None, *, level='debug'):
if f is None:
return functools.partial(debug, level=level)
@functools.wraps(f) # we tell wraps that the function we are wrapping is f
def log_f_as_called(*args, **kwargs):
logger.log(level, f'{f} was called with arguments={args} and kwargs={kwargs}')
value = f(*args, **kwargs)
logger.log(level, f'{f} return value {value}')
return value
return log_f_as_called
现在修饰器可以正常工作了:
@debug
def hello():
print('hello')
这样就会使用DEBUG级别。或者可以覆盖log级别:
@debug('warning')
def hello():
print('hello')
原文:https://timber.io/blog/decorators-in-python/
作者:Nick Humrich
译者:弯月,责编:胡巍巍
作为程序员,起码要知道的 Python 修饰器!相关推荐
- 有哪些事情是你成为程序员之后才知道的?
来源 | 三太子敖丙(ID:JavaAudition) 昨天我教练问我:"有哪些事情是你成为程序员之后才知道的."我就写下来了. 身穿一件微微起球的格子衫,背着工整的双肩包,头发乱 ...
- 使用Mono平台前,请牢记产品观点(所有.Net程序员都建议知道的)
技术领域有很多让人深感困惑的地方,不管是架构师.设计师还是程序员,在完成任务之余,偶尔都有自责的地方:程序员在使用新技术完成任务的时候,有时会觉的自己旧技术都没有完全掌握,使用新技术有些好高骛远:设计 ...
- 每个程序员都必须知道的 8 种数据结构
点击上方"小白学视觉",选择加"星标"或"置顶" 重磅干货,第一时间送达 快速介绍8种数据结构 数据结构是一种特殊的组织和存储数据的方式,可 ...
- 有什么事情是你当了程序员之后才知道的?
以前总认为所谓的 IT 精英,都是光鲜亮丽的.他们是真正可能改变世界的人,大学毅然而然的选了当时很火的计算机科学专业.毕业后也顺利的成了一名IT从业者(程序员),才知道原来并没有那么光鲜亮丽反倒是大家 ...
- 程序员一定要知道的11个实用工具网站
目录 1.搜索引擎 2.PPT 3.图片操作 4.文件共享 5.应届生招聘 6.程序员面试题库 7.办公.开发软件 8.高清图片.视频素材网站 9.项目开源 10.算法 11.在线工具宝典大全 程序员 ...
- 每个程序员都应该知道的10件事!
如果你已经编程了一段时间,并且想学习编程,那么你可能在想什么才是一个好的程序员?计算机科学与技术专业毕业生能做些什么,来为软件开发职业生涯做准备? 在本文中,我将分享10件我认为每个程序员都应该知道的 ...
- 有哪些道理是我当了程序员后才知道的?
1.当你明白了技术永远是为了业务服务的时候,不再技术至上的时候,你就成长到程序员的下一个阶段了. 2.业务第一,产品第二,技术第三. 3.盈利了,是业务部门把钱搞来的,技术部门是支出部门. 4.年轻的 ...
- 一文了解程序员必须要知道的JVM和性能优化知识点
目录 JVM和性能优化 1.Java内存区域 虚拟机的历史 未来的Java技术一览 运行时数据区域 站在线程角度来看堆和栈 深入辨析堆和栈 方法的出入栈 虚拟机中的对象 堆参数设置和内存溢出实战 2. ...
- 刚入门的程序员朋友需要知道的30件事
朋友们,我是床长! 如需转载请标明出处:http://blog.csdn.net/jiangjunshow 如果你想成为一名程序员,这个建议可以帮助你走上正确的道路. 程序员不是一个容易的职业,每年都 ...
最新文章
- SpringBoot - 优雅的实现【参数分组校验】高级进阶
- 基于实时深度学习的推荐系统架构设计和技术演进
- 机器学习(十七)——决策树, 推荐系统进阶
- Android开发之Dialog对话框(弹框)工具类
- yii---where or该如何使用
- oracle传date参数十二小时,Oracle数据库中 to_date()与24小时制表示法及mm分钟的显示...
- springdata jpa单表操作crud
- python中matlab函数图像处理,MATLAB图像处理--同态滤波(代码及示例)
- 五年26个版本:Linux系统内核全程回顾
- SpringBoot原理-SpringBoot核心运行原理
- virtualenv 安装不同版本的虚拟环境的办法
- java语法格式整理
- microsoft sql server无法删除_分享一则生产数据库sql优化案例:从无法删除到耗时20秒
- 前言:电商产品经理必修课-如何打造实战型商品系统
- 这篇文章让你轻松实现流动图片
- 第三章 sysrepo-plugind源码分析
- 蓝牙架构(6)—— 3 数据传输架构(3.1 核心传输载体)
- “公正”与“公平”之辨
- 2021-2022学年广州中学九年级第一学期期中考试英语试题
- JavaScript手机号码号段校验
热门文章
- 机器学习的训练数据(Training Dataset)、测试数据(Testing Dataset)和验证数据(Validation Dataset)
- Tensorflow中与张量形状有关的操作
- 读卡器行业调研报告 - 市场现状分析与发展前景预测(2021-2027年)
- java正则匹配下划线_java验证,”支持6-20个字母、数字、下划线或减号,以字母开头“这个的正则表达式怎么写?...
- 快速入门上手第一课 | 从云计算到 Serverless
- 解决vim编译后的乱码问题
- 像 C 一样快,Ruby 般丝滑的 Crystal 发布 1.0 版本,你看好吗?
- 大量数据丢失且无法恢复!欧洲云服务巨头数据中心起火
- 老将回归,英特尔的复兴之路
- 简单理解计算机内存乱序