【问题标题】:Passing Modified State to Outer Nested Decorator将修改后的状态传递给外部嵌套装饰器
【发布时间】:2019-07-16 20:38:45
【问题描述】:

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

例如,我们可能有由

定义的装饰器
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

(注意num_calls属性始终为0。)

为了澄清,我完全理解为什么会发生这种情况,但我想找出解决这个问题的最佳方法是什么(这样上面的代码就可以做到你希望的那样,并且更新包装器中的num_calls)。

【问题讨论】:

  • 运行后计数不应该为零
  • 另外,你可能想要交换装饰器的顺序,除非你想计时计数增量。
  • @JohnDoe。请运行您发布的代码。这就是 MCVE 的全部意义所在。我们必须谈论同一个实际的事情,而不是我猜你的意思。
  • 我无法运行代码。在num_calls 上获取一个AttributeError,无论它是否带有functools。顺便说一下,我把它改成了wraps
  • 对 functools 不太熟悉,但我找到了this section,我猜这就是他想要的

标签: python python-decorators


【解决方案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

【讨论】:

    【解决方案2】:

    如您所知,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 提供方便一样。

    【讨论】:

      猜你喜欢
      • 2021-12-23
      • 2021-11-15
      • 2014-11-17
      • 2021-02-11
      • 1970-01-01
      • 2011-02-22
      • 2016-08-31
      • 2021-11-21
      相关资源
      最近更新 更多