Python декоратор с опциями - PullRequest
       27

Python декоратор с опциями

6 голосов
/ 20 декабря 2010

У меня есть модуль с функцией, прототип которой аналогичен классу потока.

def do(fn, argtuple=(), kwargdict={}, priority=0,
            block=False, timeout=0, callback=None, daemon=False)

    # do stuff

fn является вызываемым, а argtuple и kwargdict являются позиционными и словарными аргументами, которые будут переданы вызываемому fn при вызове.

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

Так, например:

@do(priority=2)
def decoratedTask(arg, dic=3):
    #do stuff

decoratedTask(72)

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

Имеет ли это смысл?

Ответы [ 4 ]

16 голосов
/ 20 декабря 2010

Что делает декоратор, так это то, что он принимает функцию в качестве аргумента и также возвращает функцию, обычно новую функцию, которая создается в декораторе.

Эта новая функция должна принимать те же параметры, что и декорируемая вами функция, а также вызывать исходную функцию.

Теперь, когда у вас есть декоратор с аргументом, происходит дополнительный уровень. Этот декоратор должен принять аргумент и вернуть декоратор . Используемая вами функция на самом деле не декоратор, а создатель декоратора!

Вот пример:

>>> def mydeco(count):
...     def multipass(fn):
...         def caller(*args, **kw):
...             return [fn(*args, **kw) for x in range(count)]
...         return caller
...     return multipass
... 
>>> @mydeco(5)
... def printer(text):
...     print(text)
... 
>>> printer("Yabbadabbadoo!")
Yabbadabbadoo!
Yabbadabbadoo!
Yabbadabbadoo!
Yabbadabbadoo!
Yabbadabbadoo!
[None, None, None, None, None]

Вы увидите примеры этих создателей декораторов, реализованных в виде классов. Это то, что я тоже предпочитаю (хотя я обычно вообще не имею декоратора), но функция в функции в функции работает. :)

4 голосов
/ 20 декабря 2010

Не совсем так работает синтаксис декоратора.Когда вы пишете @do(priority=2), Python оценивает do(priority=2) и использует результат этого вызова в качестве декоратора.Это сокращение для

decorator=do(priority=2)
@decorator

Так что вы действительно хотите сделать do декоратором фабрикой : вы хотите, чтобы он принял все позиционные аргументы и возвратил декоратор.

def do(args=(), kwargs={}, ...):
    def _decorator(fn):
        def newfn(*args, **kwargs):
            return fn(*args, **kwargs)
        return newfn
    return _decorator

Обратите внимание, что на самом деле здесь есть три функции!

  • do - это функция, которую мы вызываем, получают наш декоратор (например, do(priority=2) является примеромdecorator)
  • _decorator - это фактический возвращенный декоратор, который зависит от аргументов do
  • Поскольку декоратор принимает функцию в качестве входного и возвращает функцию в качестве выходного, нам нужноопределить функцию, которую возвращает декоратор.newfn является этой функцией.

Пример:

>>> def rename(name):
...     def _decorator(fn):
...             def renamed(*args, **kwargs):
...                     return fn(*args, **kwargs)
...             renamed.__name__ = name
...             return renamed
...     return _decorator
...
>>> @rename('I like omelettes in the morning.')
... def foo():
...     return 'bar'
...
>>> foo()
'bar'
>>> foo.__name__
'I like omelettes in the morning.'

или эквивалентно

>>> omeletter = rename('I like omelettes in the morning.')
>>> @omeletter
... def foo():
...     return 'bar'
...
>>> foo()
'bar'
>>> foo.__name__
'I like omelettes in the morning.'

Кстати, позаботьтесьс изменяемыми аргументами по умолчанию () и {};Я уверен, что вы знаете опасности!

3 голосов
/ 20 декабря 2010

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

    @deco
    def somefunc(...): pass

Что делает то же самое, что и:

    def somefunc(...): pass
    somefunc = deco(somefunc)

Аргумент в этой ситуации представляет собой скомпилированную версию определения функции, которая следует сразу после. В таких случаях декоратор возвращает вызываемый объект, который присваивается имени функции вместо тела скомпилированной функции, как это обычно бывает.

Однако, если функции-декоратору при вызове явно дается один или несколько аргументов, например:

    @deco(args)
    def somefunc(...): pass

Становится эквивалентным:

    def somefunc(...): pass
    somefunc = deco(args)(somefunc)

Как видите, вещи в этом случае работают несколько иначе. Функция декоратора по-прежнему возвращает вызываемый объект, только на этот раз , который ожидается для «нормальной» функции декоратора с неявным аргументом, которая затем вызывается с объектом функции из следующего определения функции, как и раньше. *

Снова & ndash; как уже отмечали другие - & ndash; это делает декораторам явно переданные аргументы, фабрики декораторов в том смысле, что они конструируют и возвращают «обычные» функции декоратора.

В большинстве, если не во всех случаях, декораторы могут быть реализованы либо как функция, либо как класс, так как оба они могут вызываться в Python. Лично я нахожу функции немного более понятными и поэтому буду использовать этот подход в следующем. С другой стороны, функциональный подход может стать хитрым, поскольку он часто включает одно или несколько определений вложенных функций.

Вот как вы можете кодировать декоратор для функции do() в вашем модуле. В приведенном ниже коде я определил, что он делает - выводит аргументы функции перед ее вызовом.

def do(fn, args=tuple(), kwargs={}, priority=0,
       block=False, timeout=0, callback=None, daemon=False):
    # show arguments
    print ('in do(): fn={!r}, args={}, kwargs={}, priority={},\n'
           '         block={}, timeout={}, callback={}, daemon={}'
           .format(fn.__name__, args, kwargs, priority,
                   block, timeout, callback, daemon))
    # and call function 'fn' with its arguments
    print ('  calling {}({}, {})'.format(
               fn.__name__,
               ', '.join(map(str, args)) if args else '',
               ', '.join('{}={}'.format(k, v) for k,v in kwargs.items())
                    if kwargs else '')
          )
    fn(*args, **kwargs)

def do_decorator(**do_kwargs):
    def decorator(fn):
        def decorated(*args, **kwargs):
            do(fn, args, kwargs, **do_kwargs)
        return decorated
    return decorator

@do_decorator(priority=2)
def decoratedTask(arg, dic=42):
    print 'in decoratedTask(): arg={}, dic={}'.format(arg, dic)

decoratedTask(72, dic=3)

Выход:

in do(): fn='decoratedTask', args=(72,), kwargs={'dic': 42}, priority=2,
         block=False, timeout=0, callback=None, daemon=False
  calling decoratedTask(72, dic=3)
in decoratedTask(): arg=72, dic=3

Вот краткий отчет о том, как работает этот сложный на вид материал:

Функция внешнего декоратора do_decorator(), определяет другую функцию внутреннего декоратора, которую она возвращает, здесь с креативным названием decorator.

Что делает decorator, так это определяет, что происходит с функциями, которые оформлены в простом сценарии «без аргументов» - здесь определяется и возвращается еще один & ndash; но окончательно - вложенная функция с именем decorated, которая просто вызывает функцию do() модуля и передает ей оба аргумента, если таковые имеются, с точки вызова, наряду с аргументами, предназначенными для использования функции do().

Этот вариант использования несколько усложняется тем, что как внешний декоратор, так и декорируемые функции имеют аргументы с ключевыми словами. Требуется особая осторожность, чтобы гарантировать, что имена ключевых слов для каждого уникальны, чтобы они не конфликтовали (и чтобы значение изменяемого аргумента kwargs по умолчанию не было случайно изменено чем-то в функции do()).

0 голосов
/ 13 февраля 2014

Мне нравятся ответы @Lennart Regebro, @katrielalex и @martineau выше, но с риском звучания очень банально , я рискну написать пример на основе истории .

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

def self_taught_teacher(fn):
    ''' I teach students, because I had no teacher to guide me.'''
    def a_student(*real_life_issues, **details_about_my_world):
        ''' I'm a student who has been trained by a teacher 
        who has taught me about the problems I may encounter in real life.

        Thanks to my teacher for giving me extra knowledge.    
        '''
        print 'I have to remember what my teacher taught me.'
        my_answer = fn(*real_life_issues, **details_about_my_world)
        print 'Ah yes, I made the right decision.'
        #
        return my_answer
    #
    return a_student

@self_taught_teacher
def student_named_Lisa_practicing_maths(length, width, height):
    print 'Im consulting my powers of maths...'
    return length * width * height

Посмотрим, что знает ученик ...

>>> answer = student_named_Lisa_practicing_maths(10, 20, height=3)
I have to remember what my teacher taught me.
Im consulting my powers of maths...
Ah yes, I made the right decision.
>>> answer
600

Очень хорошо. Во второй части истории 1018 * мы представляем профессора , который учит других становиться учителями . Эти учителя затем делятся тем, что они узнали, со своими учениками .

def professor_who_trains_teachers(*subjects, **to_train_teachers):
    '''I am a profeseur. I help train teachers.  ''' 
    #
    def a_teacher_who_gets_trained(fn): 
        ''' I learn subjects I should teach to my students.'''
        knowledge = [s for s in subjects]
        #
        def a_student(*real_life_issues, **details_about_my_world):
            ''' I'm a student who has been trained by a teacher 
            who has taught me about the problems I may encounter in real life.

            Thanks to my teacher for giving me extra knowledge.    
            '''
            print '(I know %s that i learned from my teacher,...)' % \
                    [information for information in knowledge]
            my_answer = fn(*real_life_issues, **details_about_my_world)
            print 'Ah yes, I made the right decision.'
            #
            return my_answer
        #
        return a_student
        #
        #
    return a_teacher_who_gets_trained 

Итак, мы можем обучить учителя и позволить ему учить ученика ...

>>> teacher1 = professor_who_trains_teachers('math','science')
>>> teacher1
<function a_teacher_who_gets_trained at 0x104a7f500>
>>> teacher1.__name__
'a_teacher_who_gets_trained'
>>> 

@teacher1
def student_named_Lisa_practicing_maths(length, width, height):
    print 'Im consulting my powers of maths...'
    return length * width * height

Этот студент знает свою математику ..

>>> answer = student_named_Lisa_practicing_maths(20, 10, 2)
(I know ['math', 'science'] that i learned from my teacher,...)
Im consulting my powers of maths...
Ah yes, I made the right decision.
>>> 
>>> answer
400

И мы тоже можем создать учителя.

@professor_who_trains_teachers('math', 'science', remember='patience')
def student_named_Lisa_practicing_maths(length, width, height):
    print 'Im consulting my powers of maths...'
    return length * width * height

Опять новый студент может делать математику ...

>>> answer = student_named_Lisa_practicing_maths(10, 20, height=3)
(I know ['math', 'science'] that i learned from my teacher,...)
Im consulting my powers of maths...
Ah yes, I made the right decision.
>>> 
>>> answer
600
...