Python 装饰器计数函数调用

Posted

技术标签:

【中文标题】Python 装饰器计数函数调用【英文标题】:Python decorators count function call 【发布时间】:2017-12-11 14:07:08 【问题描述】:

我正在刷新我对一些我还没有得到的 python 特性的记忆,我正在向this python tutorial 学习,并且有一个我不完全理解的例子。这是关于一个装饰器计算对函数的调用,这里是代码:

def call_counter(func):
    def helper(x):
        helper.calls += 1
        return func(x)
    helper.calls = 0
    return helper

@call_counter
def succ(x):
    return x + 1

if __name__ == '__main__':
    print(succ.calls)
    for i in range(10):
        print(succ(i))
    print(succ.calls)

我在这里不明白的是,为什么我们要增加函数包装器的调用 (helper.calls += 1) 而不是函数调用本身,为什么它实际上有效?

【问题讨论】:

@call_counter succ == succ = call_counter(succ) = helper 【参考方案1】:

关于装饰器要记住的重要一点是,装饰器是一个函数,它接受一个函数作为参数,并返回另一个函数。返回的值 - 又一个函数 - 将在调用原始函数的名称时调用。

这个模型可以很简单:

def my_decorator(fn):
    print("Decorator was called")
    return fn

在这种情况下,返回的函数与传入的函数相同。但这通常不是你所做的。通常,您要么返回一个完全不同的函数,要么返回一个以某种方式链接或包装原始函数的函数。

在您的示例中,这是一个非常常见的模型,您有一个返回的内部函数:

def helper(x):
    helper.calls += 1
    return func(x)

这个内部函数调用原始函数 (return func(x)),但它也会增加调用计数器。

这个内部函数被插入作为任何正在装饰的函数的“替换”。因此,当查找您的模块foo.succ() 函数时,结果是对装饰器返回的内部辅助函数的引用。该函数增加调用计数器,然后调用最初定义的succ 函数。

【讨论】:

【参考方案2】:

当你装饰一个你“替换”的函数时,你就是使用包装器的函数。

在这个例子中,在修饰之后,当你调用succ时,你实际上是在调用helper。因此,如果您计算调用次数,则必须增加 helper 调用次数。

您可以通过检查装饰函数的属性 _name_ 来检查装饰函数后,名称是否已绑定到包装器:

def call_counter(func):
    def helper(*args, **kwargs):
        helper.calls += 1
        print(helper.calls)
        return func(*args, **kwargs)
    helper.calls = 0
    return helper

@call_counter
def succ(x):
    return x + 1

succ(0)
>>> 1
succ(1)
>>> 2
print(succ.__name__)
>>> 'helper'
print(succ.calls)
>>> 2

【讨论】:

我相信 **args 实际上应该是 **kwargs 就像在关键字参数中一样。 我正在做一个记录器装饰器。有没有办法知道它里面有多少层,比如说,装饰的 func A 调用装饰的 func B,有一个变量告诉我 func A 是 1 级而 func B 是 2 级?【参考方案3】:

我在这里不明白的是,为什么我们要增加函数包装器的调用 (helper.calls += 1) 而不是函数调用本身,为什么它实际上可以工作?

我认为让它成为一个通用的装饰器。你可以这样做

def succ(x):
    succ.calls += 1
    return x + 1

if __name__ == '__main__':
    succ.calls = 0
    print(succ.calls)
    for i in range(10):
        print(succ(i))
    print(succ.calls)

它工作得很好,但是您需要将.calls +=1 放入您想要应用它的每个函数中,并在运行它们之前初始化为 0。如果你有一大堆想要计算的函数,这肯定会更好。另外,它在定义时将它们初始化为 0,这很好。

据我了解,它之所以有效,是因为它将函数 succ 替换为装饰器中的 helper 函数(每次装饰函数时都会重新定义)所以 succ = helpersucc.calls = helper.calls。 (当然,名称助手只在装饰器的命名空间中定义)

这有意义吗?

【讨论】:

【参考方案4】:

据我了解(如果我错了,请纠正我)您的程序执行顺序是:

    注册call_function。 注册succ。 在注册 succ 函数解释器时发现一个装饰器,因此它执行 call_function。 您的函数返回一个作为函数的对象 (helper)。并添加到此对象字段calls现在您的函数succ 已分配给helper。所以当你调用你的函数时,你实际上是在调用 helper 函数,封装在一个装饰器中。因此,您添加到辅助函数的每个字段都可以通过寻址succ 在外部访问,因为这两个变量指的是同一件事。 所以当你打电话给succ()时,如果你打电话helper(*args, **argv)基本上是一样的

看看这个:

def helper(x):
    helper.calls += 1
    return 2
helper.calls = 0

def call_counter(func):
    return helper

@call_counter
def succ(x):
    return x + 1

if __name__ == '__main__':
    print(succ == helper)  # prints true.

【讨论】:

【参考方案5】:

类装饰器示例

当您使用 类装饰器 装饰函数时,每个函数都有自己的 call_count。这就是 OOP 的简单性。每次调用CallCountDecorator对象,都会增加自己的call_count属性并打印出来。

class CallCountDecorator:
    """
    A decorator that will count and print how many times the decorated function was called
    """

    def __init__(self, inline_func):
        self.call_count = 0
        self.inline_func = inline_func

    def __call__(self, *args, **kwargs):
        self.call_count += 1
        self._print_call_count()
        return self.inline_func(*args, **kwargs)

    def _print_call_count(self):
        print(f"The self.inline_func.__name__ called self.call_count times")


@CallCountDecorator
def function():
    pass


@CallCountDecorator
def function2(a, b):
    pass


if __name__ == "__main__":
    function()
    function2(1, b=2)
    function()
    function2(a=2, b=3)
    function2(0, 1)

# OUTPUT
# --------------
# The function called 1 times
# The function2 called 1 times
# The function called 2 times
# The function2 called 2 times
# The function2 called 3 times

【讨论】:

正如目前所写,您的答案尚不清楚。请edit 添加其他详细信息,以帮助其他人了解这如何解决所提出的问题。你可以找到更多关于如何写好答案的信息in the help center。

以上是关于Python 装饰器计数函数调用的主要内容,如果未能解决你的问题,请参考以下文章

python装饰器实现周期性函数调用

python装饰函数调用

python再议装饰器

python 多个装饰器的调用顺序

Python-4 函数和装饰器

使用 python 装饰器调用递归函数的次数