将修改后的状态传递给外部嵌套装饰器

时间:2019-07-16 20:38:45

标签: python python-decorators

当嵌套两个存储和更改状态信息的Python装饰器时,将内部装饰函数的状态传递给外部装饰器的最佳方法是什么?

例如,我们可能将装饰器定义为

SELECT * FROM city WHERE population > 200;

然后我们可以如下装饰一个函数

def time_this(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs) :
        start = time.process_time()
        rtn_val = func(*args, **kwargs) 
        wrapper.time_taken = time.process_time() - start
        return rtn_val
    return wrapper

def count_calls(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs) :
        wrapper.num_calls += 1
        return func(*args, **kwargs) 
    wrapper.num_calls = 0
    return wrapper

但是,如果我们尝试

@time_this
@count_calls
def my_func():
    time.sleep(0.5)
    print("Hello World!") 

我们得到的输出是

my_func()
print(my_func.time_taken)
print(my_func.num_calls)

(请注意,Hello World! 0.5007079059998887 0 属性始终为0。)

为澄清起见,我完全理解为什么发生这种情况,但是我想找出解决此问题的最佳方法是什么(以便上面的代码可以实现您希望的效果,并且更新包装器中的num_calls

2 个答案:

答案 0 :(得分:1)

您可以将一个带有结果的字典设置为包装器,该字典将在装饰器之间共享:

import functools
import time

def time_this(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs) :
        time1 = time.time()
        rtn_val = func(*args, **kwargs)
        time2 = time.time()
        wrapper.results['time_this'] += (time2 - time1) * 1000.0
        return rtn_val

    results = getattr(wrapper, 'results', {})
    results['time_this'] = 0
    setattr(wrapper, 'results', results)

    return wrapper

def count_calls(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs) :
        rtn_val = func(*args, **kwargs)
        wrapper.results['count_calls'] = wrapper.results['count_calls'] + 1
        return rtn_val

    results = getattr(wrapper, 'results', {})
    results['count_calls'] = 0
    setattr(wrapper, 'results', results)

    return wrapper


@time_this
@count_calls
def my_func():
    print("Hello World!")

for i in range(10):
    my_func()

print(my_func.results)
print('avg:', my_func.results['time_this'] / my_func.results['count_calls'])

打印:

Hello World!
Hello World!
Hello World!
Hello World!
Hello World!
Hello World!
Hello World!
Hello World!
Hello World!
Hello World!
{'count_calls': 10, 'time_this': 0.03719329833984375}
avg: 0.003719329833984375

答案 1 :(得分:0)

您知道,num_calls在外部包装器中为静态0的原因是,functools.wraps更新了包装器__dict__,将包装器冻结为原始值。有两种处理方法。

如果知道装饰器的顺序,则可以使用它在包装器中创建的__wrapped__属性来访问实际属性:

print(my_func.__wrapped__.num_calls)

另一个选择是查看wraps的其他参数。默认情况下,__dict__被更新。但是为什么不将其重新分配给新对象:

def mywraps(wrapped):
    return functools.wraps(wrapped, assigned=functools.WRAPPER_ASSIGNMENTS + ('__dict__',),
                           updated=())

def time_this(func):
    @mywraps(func)
    def wrapper(*args, **kwargs) :
        start = time.process_time()
        rtn_val = func(*args, **kwargs) 
        wrapper.time_taken = time.process_time() - start
        return rtn_val
    return wrapper

def count_calls(func):
    @mywraps(func)
    def wrapper(*args, **kwargs) :
        wrapper.num_calls += 1
        return func(*args, **kwargs) 
    wrapper.num_calls = 0
    return wrapper

现在,链中的所有功能对象都共享相同的手动分配的属性,因为

>>> my_func.__dict__ is my_func.__wrapped__.__dict__ is my_func.__wrapped__.__wrapped__.__dict__
True

您严格不需要mywraps装饰器。我只是为了避免每次都为wraps设置参数而提供的便利,就像wraps本身为update_wrapper带来便利一样。