Как пишутся эти типы Python-декораторов? - PullRequest
6 голосов
/ 10 июля 2009

Я хотел бы написать декоратор, который бы ограничивал количество раз, которое может быть выполнена функция, что-то вроде следующего синтаксиса:


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

Я думаю, что можно написать этот тип декоратора, но я не знаю как. Я думаю, что функция не будет первым аргументом этого декоратора, верно? Мне нужна реализация "простого декоратора", а не какой-то класс с вызовом метода.

Причина этого в том, чтобы узнать, как они написаны. Пожалуйста, объясните синтаксис и как этот декоратор работает.

Ответы [ 6 ]

12 голосов
/ 10 июля 2009

Это то, что я взбил. Он не использует класс, но он использует атрибуты функции:

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, которая, в свою очередь, возвращает wrapped. decoration хранит максимальное количество execs и текущее количество execs в двух атрибутах функции, которые затем проверяются в wrapped.

Перевод: При использовании декоратора вот так:

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

Вы в основном делаете что-то вроде этого:

f = max_execs(5)(f)
4 голосов
/ 10 июля 2009

Декоратор - это просто вызываемый объект, который превращает функцию во что-то еще. В вашем случае 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)
3 голосов
/ 10 июля 2009

Есть два способа сделать это. Объектно-ориентированный способ - создать класс:

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.

Я предпочитаю описанный выше подход ООП для этого типа декоратора, поскольку у вас в основном есть частная переменная count, отслеживающая количество выполнений. Однако другой подход заключается в использовании замыкания, такого как

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 или более ранних версиях нам нужно обернуть число выполнений в список, чтобы его можно было изменить.

2 голосов
/ 10 июля 2009

Не полагаясь на состояние в классе, вы должны сохранить состояние (количество) в самой функции:

def max_execs(count):
    def new_meth(meth):
        meth.count = count
        def new(*a,**k):
            meth.count -= 1
            print meth.count            
            if meth.count>=0:
                return meth(*a,**k)
        return new
    return new_meth

@max_execs(5)
def f():
    print "invoked"

[f() for _ in range(10)]

Это дает:

5
invoked
4
invoked
3
invoked
2
invoked
1
invoked
0
-1
-2
-3
-4
1 голос
/ 10 июля 2009

Этот метод не изменяет внутренние функции, а превращает его в вызываемый объект.

Использование класса замедляет выполнение на ~ 20% по сравнению с использованием исправленной функции!

def max_execs(n=1):
    class limit_wrapper:
        def __init__(self, fn, max):
            self.calls_left = max
            self.fn = fn
        def __call__(self,*a,**kw):
            if self.calls_left > 0:
                self.calls_left -= 1
                return self.fn(*a,**kw)
            raise Exception("max num of calls is %d" % self.i)


    def decorator(fn):
        return limit_wrapper(fn,n)

    return decorator

@max_execs(2)
def fun():
    print "called"
0 голосов
/ 10 июля 2009

Я знаю, что вы сказали, что не хотите занятий, но, к сожалению, это единственный способ, которым я могу придумать, как сделать это на макушке.

class mymethodwrapper:
    def __init__(self):
        self.maxcalls = 0
    def mymethod(self):
        self.maxcalls += 1
        if self.maxcalls > 5:
            return
        #rest of your code
        print "Code fired!"

Запустите его вот так

a = mymethodwrapper
for x in range(1000):
    a.mymethod()

Вывод будет:

>>> Code fired!
>>> Code fired!
>>> Code fired!
>>> Code fired!
>>> Code fired!
...