深入理解Python装饰器(二)
概要
这个续篇主要说明装饰器被解释器调用的顺序,以及一些常见的装饰器场景
关于装饰器的上篇地址链接
https://www.jianshu.com/p/f3c160bed955
2. 经典写法
[code frame1]
def deco(name=""):
#if inspect.isroutine(name):
# return deco()(name)
def decorator(func):
func.tag1 = "abc"
func.tag2 = "567"
return func
return decorator
问题
1. 为什么def decorator要包在里面?可不可以写在外面?
2. 可不可以直接对func操作,例如写成这样:
[code frame2]
def maybesimple_deco(func):
func.tag1 = "abc"
func.tag2 = "567"
return func
功能上看没什么问题?都能给被修饰的函数打上两个tag,对比这两种写法,形式上第二种更简单,但是在项目中基本上比较少用,实际项目的修饰器更复杂,涉及一些条件判断,通常会对函数的修饰逻辑封装在一个内部函数里面,像[code frame 1]那样写。
类修饰
一段来自真实的项目代码:源码来自 https://github.com/robotframework/robotframework
[code frame3]
@not_keyword
def library(scope=None, version=None, doc_format=None, listener=None,
auto_keywords=False):
"""Class decorator to control keyword discovery and other library settings.
By default disables automatic keyword detection by setting class attribute
``ROBOT_AUTO_KEYWORDS = False`` to the decorated library. In that mode
only methods decorated explicitly with the :func:`keyword` decorator become
keywords. If that is not desired, automatic keyword discovery can be
enabled by using ``auto_keywords=True``.
Arguments ``scope``, ``version``, ``doc_format`` and ``listener`` set the
library scope, version, documentation format and listener by using class
attributes ``ROBOT_LIBRARY_SCOPE``, ``ROBOT_LIBRARY_VERSION``,
``ROBOT_LIBRARY_DOC_FORMAT`` and ``ROBOT_LIBRARY_LISTENER``, respectively.
These attributes are only set if the related arguments are given and they
override possible existing attributes in the decorated class.
Examples::
@library
class KeywordDiscovery:
@keyword
def do_something(self):
# ...
def not_keyword(self):
# ...
@library(scope='GLOBAL', version='3.2')
class LibraryConfiguration:
# ...
The ``@library`` decorator is new in Robot Framework 3.2.
"""
if inspect.isclass(scope):
return library()(scope)
def decorator(cls):
if scope is not None:
cls.ROBOT_LIBRARY_SCOPE = scope
if version is not None:
cls.ROBOT_LIBRARY_VERSION = version
if doc_format is not None:
cls.ROBOT_LIBRARY_DOC_FORMAT = doc_format
if listener is not None:
cls.ROBOT_LIBRARY_LISTENER = listener
cls.ROBOT_AUTO_KEYWORDS = auto_keywords
return cls
return decorator
类装饰器对比函数装饰器,几乎没有太大的分别,实际上code frame2也可以修饰类
[code frame4]
@maybesimple_deco
class FooClass(object):
def __init__(self):
pass
foo = FooClass()
print(foo.tag1)
四、装饰器的代码调用顺序
4.1 多层语法糖 @@ ... 的求值顺序
@deco1(arg1)
@deco2
def myfunc()
...
python有关函数与类的函数执行,大体上是按照栈顺序来的,函数定义可以看成是一个入栈过程,函数实体调用,则是出栈顺序。
装饰器@语法的求值顺序是先下后上(先里后外),越靠近被修饰函数(类)的函数(定义部分)越先执行
1 import inspect
2
3
4 def deco1(ff):
5 print("enter ff, ff's name is : %s" % ff.__name__)
6 return ff
7
8
9 def deco(name=""):
10 print("enter deco")
11 if inspect.isroutine(name):
12 return deco()(name)
13
14 def decorator(func):
15 func.tag1 = "abc"
16 func.tag2 = "567"
17 return func
18
19 return decorator
20
21
22 class Foo(object):
23 def __init__(self):
24 print("enter Foo object")
25
26 @deco1
27 def foo_func(self):
28 print("enter foo_func")
29
30
31 @deco1
32 @deco("abc")
33 def myfunc():
34 print("enter myfunc")
35
36
37 if __name__ == '__main__':
38 myfunc()
39
40 f = Foo()
41 f.foo_func()
以上代码的执行顺序(忽略空行)是
[1 4 9 22 23 26 4 5 6 26 22 31 32 9 10 11 14 19 32 14 15 16 17 32 4 5 6 32 37 38 31 34 38 40 23 24 40 26 28 41]
函数调用完成返回调用点这个特征无须赘述,比较重要的是31行~34行,这里的顺序是先检查定义,31顺序入栈到32为止,如果有多层@语法,依次类推;语法定义完毕后开始装饰器求值,此时从里向外(从下到上)依次运行函数体内的逻辑,所以32行结束后会跳到第9行开始求值,运行到14行是一个内部函数定义,忽略函数体然后运行完19行返回到32行,这时decorator继续求值,参数为myfunc,跳转到14行开始运行内部函数的定义,14~17行, 然后跳回32行,继续向上求值,运行4~6行的装饰器内部逻辑,跳转回32行... 如果有多个装饰器,直到对所有装饰器求值完毕;
对于类定义的成员函数被修饰的情形,首先类的定义行22运行,23行定义初始化函数__init__,然后26行遇到装饰器,4~6行进行装饰器求值,运行完成后回到26行,22行。
所有装饰器求值在正式的函数调用(main模块的运行)之前,所以可以理解为,装饰器的应用场景是一种想在被修饰函数(类)的实体调用之前进行操作的行为
保存为run_order.py 输出
$ python run_order.py
enter ff, ff's name is : foo_func
enter deco
enter ff, ff's name is : myfunc
enter myfunc
enter Foo object
enter foo_func
装饰器的实用场景
统计一个函数的调用耗时。这是非常经典的使用场景,怎么写这个装饰器?
我们知道,被修饰函数具有不确定的参数,所以装饰器需要包装一个变参类型。本例子说明变参函数用统一装饰器修饰的方法
代码:
def waste_time(func):
def decorator(*args, **kwargs):
start = time.time()
res = func(*args, **kwargs)
end = time.time()
print("Spend %s %f" % (func.__name__, end - start))
return res
return decorator
print语句实际应用可以用异步线程做一个上报统计的逻辑代替。
使用如下:
@waste_time
def foo(name, age):
time.sleep(1.5)
foo("Xiaoming", 15)
输出
$ Spend foo 1.502235
如果我们只统计 超过 200ms的函数应当如何,300ms呢?这就需要将超时阈值设计为一个参数,装饰器wase_time 的使用方式类似这样子:
@waste_time(200)
def foo(name, age):
time.sleep(1.5)
装饰器应该如何设计?
首先 waste_time 的第一层参数变成了一个整型参数,被修饰函数作为参数在第二层传递,所以可在嵌套包装进去,轮廓:
def waste_time(millesecs: int):
def wrapper(f):
# do something
pass
return wrapper
这样的话 @waste_time(200) 首先调用 waste_time(200) 返回 wrapper对象,然后计算 wrapper(foo) 运行wrapper内部的逻辑。
可以看到,装饰器的设计需要抓住两点
- 保证装饰器计算完以后返回的结果是一个能够接收被修饰函数为参数的可调用对象
- 内部包装器的声明有如变量声明一样随便,其参数可以层层向下传递
最终变参计时装饰器的设计如下
def waste_time(millesecs: int):
def decorator(func):
def wrapper(*args, **kwargs):
start = time.time()
res = func(*args, **kwargs)
end = time.time()
elapse_time = (end - start) * 1000
if elapse_time >= millesecs:
print("Spend %s %d" % (func.__name__, elapse_time))
return res
return wrapper
return decorator
搞清楚装饰器的调用顺序是按需设计装饰器的关键
@waste_time(200)
def foo(name, age):
time.sleep(0.55)
foo("zhangsan", 100)
# 输出 Spend foo 554
以上装饰器的调用顺序
- 首先按计算优先级,不是可调用对象就地计算,所以 waste_time(200) 首先被计算,它返回一个函数对象,decorator,这个函数对象的参数设计成一个可调用对象
- 紧接着计算 decorator(foo) , 这个函数内部返回了一个包装器 wrapper,它乐意接受任何形状的参数,然后运行 wrapper("zhangsan", 10)
- 注意,“拆包”拆到这里 "zhangsan", 10 这对实参才传进去,此时运行耗时判断的核心逻辑
- wrapper 一定要把 func(*args, **kwargs) 的计算结果透传回来
耗时网络调用的缓存装饰器
有一些耗时查库操作,为了减低对数据库的压力,建立缓存机制:
当有命中 cache 时,并且没有超时过期,直接返回上一次请求的结果,不查询db
缓存的存储结构一般是 k-v 形式,与上例有所不同,内部wrapper 的变参之需要抽出一个 key就行
装饰器可以设计成这样:
"""
secs 是缓存过期时间
"""
import time
cache_table = {} # 用内存缓存历史查询时间和结果
def cache(secs: int):
def decorator(func):
def wrapper(key, *args, **kwargs):
now = time.time()
if key not in cache_table:
content = func(key, *args, **kwargs)
cache_table[key] = {"last_time": now, "content": content}
return content
else:
if (now - cache_table[key]["last_time"]) < secs:
return cache_table[key]["content"]
else:
content = func(key, *args, **kwargs)
cache_table[key]["last_time"] = now
cache_table[key]["content"] = content
return content
return wrapper
return decorator
使用如下
@cache(3000)
def get_data(key: str) ->str:
# sql
return content