软件开发的过程中,最基本的技能就是:不要重复自己的工作。也就是说,在任何时候,当需要创建高度重复的代码时,通常都需要寻找一个更加快捷的解决方案。在python中,这类问题常常会归为“元编程”。
简而言之,元编程的主要目标是创建函数和类,并用他们来操纵代码(通常的行为有生成、修改、包装已有的代码)。Python中基于这个目的的方法有装饰器、类装饰器、元类以及有用的主题(常见的有对象签名、用exec()来执行代码以及检查函数和类的内部结构)。
元编程系列主要内容是探讨各种元编程技术,通过实例来讲解如何利用这些技术来自定义python的行为,使其能满足我们不同寻常的需求。
1 装饰器
1.1 用装饰器给函数添加一个包装
装饰器的本质就是一个函数,它可以接受一个函数作为输入并返回一个新的函数作为输出。我们可以利用给函数来建立一个装饰器(相当于给函数加上包装层)来添加额外的处理,例如记录日志、计时统计等。
最简单的一个例子是计时系统,我想记录函数的运行时间并打印到控制台,最通常的做法是:
def CutDown(n):
import time
start = ()
while n > 0:
n -= 1
end = ()
print(end-start)
CutDown(10000000)
屏幕输出:1.1250569820404053
下面引入装饰器方法:
def GetRunTime(func):
import time
from functools import wraps
@wraps(func)
def wrapper(*args, **kwargs):
start = ()
result = func(*args, **kwargs)
end = ()
print(func.__name__, end - start)
return result
return wrapper
@GetRunTime
def cutdown(n):
while n > 0:
n -= 1
cutdown(10000000)
屏幕输出:cutdown 1.1274616718292236
用第二种方法的好处就是,加入我需要在很多的函数上记录其运行时间,则只需要在需要记录时间的函数上面添加@GetRunTime即可,调试非常方便,代码也简洁易懂。
装饰器内部的代码一般会涉及创建一个新的函数,利用*args和**kwargs
来接受任意的参数,在示例中wrapper()函数就是这么操作的。在这个函数的内部,我们需要调用原来的输入参数(即被包装的函数的输入参数)并返回它的结果。
其实运行过程就是将被装饰函数打包到装饰器中运行并返回运行结果,作为代价肯定是需要牺牲一定的效率的。在示例中还有一个是需要注意的,那就是@wraps(func),它用来保存函数的元数据。其实也可以不要这个,但是就会丢失被装饰函数的一些元数据,例如函数名、文档字符串、函数注解以及调用签名。1.2就会讲解如何保存元数据。
1.2 运用装饰器时保存函数的元数据
在1.1中也提到过,即使没有@wraps(func)函数也能运行,代价是丢失一些函数的元数据:
def GetRunTime(func):
import time
#from functools import wraps
# @wraps(func)
def wrapper(*args, **kwargs):
"""
:param args:
:param kwargs:
:return:
"""
start = ()
result = func(*args, **kwargs)
end = ()
print(func.__name__, end - start)
return result
return wrapper
@GetRunTime
def cutdown(n):
"""
:param n:
:return:
"""
while n > 0:
n -= 1
cutdown(10000000)
print(cutdown.__name__)
print(cutdown.__doc__)
控制台打印信息:
cutdown 1.4844520092010498
wrapper
:param args:
:param kwargs:
:return:
我们发现打印信息不是cundown这个函数的,而是装饰器函数的。再使用@wraps(func)这个技术:
(代码略)
控制台打印信息:
cutdown 1.193265676498413
cutdown
:param n:
:return:
这个时候所有信息都属于cutdown函数的。
上面这个例子告诉我们,在我们自己编写装饰器的时候一定要记得使用@wraps(func),这样才不会丢失被装饰函数的元数据。同时,从这个例子中我们也可以看到被装饰函数的运行过程,装饰器会接管被装饰函数的所有,并返回结果。
@装饰器的重要特性就是它可以通过wrapped属性来访问被包装的函数,所以我们可以利用这个特性来解包。
1.3 对装饰器进行解包
在1.2的末尾也提到过,利用wrapped属性来进行解包。例如在1.2中可以直接使用:
@GetRunTime
def cutdown(n):
"""
:param n:
:return:
"""
print('cundown is running')
while n > 0:
n -= 1
cutdown.__wrapped__(10000000)
打印输出:
cundown is running
当有多个装饰器的函数也可以通过这种方法来访问。