如何编写这种类型的Python装饰器?

6 投票
6 回答
729 浏览
提问于 2025-04-15 12:47

我想写一个装饰器,用来限制一个函数可以执行的次数,类似下面这样的语法:


@max_execs(5)
def my_method(*a,**k):
   # do something here
   pass

我觉得可以写出这种类型的装饰器,但我不知道怎么做。我想知道这个装饰器的第一个参数不会是一个函数,对吧?我希望能看到一个“普通装饰器”的实现,而不是一个带有call方法的类。

这样做的原因是为了学习如何写装饰器。请解释一下语法,以及这个装饰器是怎么工作的。

6 个回答

3

有两种方法可以实现这个功能。面向对象的方法是创建一个类:

class max_execs:
    def __init__(self, max_executions):
        self.max_executions = max_executions
        self.executions = 0

    def __call__(self, func):
        @wraps(func)
        def maybe(*args, **kwargs):
            if self.executions < self.max_executions:
                self.executions += 1
                return func(*args, **kwargs)
            else:
                print "fail"
        return maybe

想了解 wraps 的解释,可以查看 这个问题

我更喜欢上面这种面向对象的方法来做这个装饰器,因为这样你就有了一个私有的计数变量来跟踪执行的次数。不过,另一种方法是使用闭包,比如:

def max_execs(max_executions):
    executions = [0]
    def actual_decorator(func):
        @wraps(func)
        def maybe(*args, **kwargs):
            if executions[0] < max_executions:
                executions[0] += 1
                return func(*args, **kwargs)
            else:
                print "fail"
        return maybe
    return actual_decorator

这个方法涉及到三个函数。max_execs 函数接受一个参数,表示允许的执行次数,并返回一个装饰器,这个装饰器会限制你只能调用那么多次。这个函数,actual_decorator,和我们在面向对象示例中的 __call__ 方法做的事情是一样的。唯一有点奇怪的是,由于我们没有类和私有变量,我们需要修改在闭包外部作用域中的 executions 变量。Python 3.0 用 nonlocal 语句支持这个功能,但在 Python 2.6 或更早的版本中,我们需要把执行次数放在一个列表里,这样才能修改它。

4

装饰器就是一个可以被调用的东西,它可以把一个函数变成其他的东西。在你的例子中,max_execs(5)应该是一个可以被调用的东西,它会把一个函数变成另一个可以被调用的对象,这个对象会计算调用的次数并转发这些调用。

class helper:
    def __init__(self, i, fn):
        self.i = i
        self.fn = fn
    def __call__(self, *args, **kwargs):
        if self.i > 0:
            self.i = self.i - 1
            return self.fn(*args, **kwargs)

class max_execs:
    def __init__(self, i):
        self.i = i
    def __call__(self, fn):
        return helper(self.i, fn)

我不明白你为什么要限制自己只用函数(而不使用类)。不过如果你真的想这样做的话……

def max_execs(n):
    return lambda fn, i=n: return helper(i, fn)
12

这是我搞出来的。它没有使用类,但用了函数属性:

def max_execs(n=5):
    def decorator(fn):
        fn.max = n
        fn.called = 0
        def wrapped(*args, **kwargs):
            fn.called += 1
            if fn.called <= fn.max:
                return fn(*args, **kwargs)
            else:
                # Replace with your own exception, or something
                # else that you want to happen when the limit
                # is reached
                raise RuntimeError("max executions exceeded")
        return wrapped
    return decorator

max_execs 这个函数返回了一个叫 decorator 的函数,而 decorator 又返回了 wrappeddecoration 在两个函数属性中存储了最大执行次数和当前执行次数,然后在 wrapped 中进行检查。

翻译: 当你像这样使用装饰器时:

@max_execs(5)
def f():
    print "hi!"

你基本上是在做这样的事情:

f = max_execs(5)(f)

撰写回答