Как сделать цепочку функциональных декораторов? - PullRequest
2562 голосов
/ 11 апреля 2009

Как я могу сделать два декоратора в Python, которые будут делать следующее?

@makebold
@makeitalic
def say():
   return "Hello"

... который должен вернуть:

"<b><i>Hello</i></b>"

Я не пытаюсь сделать HTML таким способом в реальном приложении - просто пытаюсь понять, как работают декораторы и связывание декораторов.

Ответы [ 17 ]

8 голосов
/ 20 марта 2015

Чтобы объяснить декоратор проще:

С:

@decor1
@decor2
def func(*args, **kwargs):
    pass

Когда делать:

func(*args, **kwargs)

Вы действительно делаете:

decor1(decor2(func))(*args, **kwargs)
5 голосов
/ 03 апреля 2014
#decorator.py
def makeHtmlTag(tag, *args, **kwds):
    def real_decorator(fn):
        css_class = " class='{0}'".format(kwds["css_class"]) \
                                 if "css_class" in kwds else ""
        def wrapped(*args, **kwds):
            return "<"+tag+css_class+">" + fn(*args, **kwds) + "</"+tag+">"
        return wrapped
    # return decorator dont call it
    return real_decorator

@makeHtmlTag(tag="b", css_class="bold_css")
@makeHtmlTag(tag="i", css_class="italic_css")
def hello():
    return "hello world"

print hello()

Вы также можете написать декоратор в классе

#class.py
class makeHtmlTagClass(object):
    def __init__(self, tag, css_class=""):
        self._tag = tag
        self._css_class = " class='{0}'".format(css_class) \
                                       if css_class != "" else ""

    def __call__(self, fn):
        def wrapped(*args, **kwargs):
            return "<" + self._tag + self._css_class+">"  \
                       + fn(*args, **kwargs) + "</" + self._tag + ">"
        return wrapped

@makeHtmlTagClass(tag="b", css_class="bold_css")
@makeHtmlTagClass(tag="i", css_class="italic_css")
def hello(name):
    return "Hello, {}".format(name)

print hello("Your name")
4 голосов
/ 06 ноября 2018

На этот ответ уже давно дан ответ, но я подумала, что поделюсь своим классом Decorator, который делает написание новых декораторов простым и компактным.

from abc import ABCMeta, abstractclassmethod

class Decorator(metaclass=ABCMeta):
    """ Acts as a base class for all decorators """

    def __init__(self):
        self.method = None

    def __call__(self, method):
        self.method = method
        return self.call

    @abstractclassmethod
    def call(self, *args, **kwargs):
        return self.method(*args, **kwargs)

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

class MakeBold(Decorator):
    def call():
        return "<b>" + self.method() + "</b>"

class MakeItalic(Decorator):
    def call():
        return "<i>" + self.method() + "</i>"

@MakeBold()
@MakeItalic()
def say():
   return "Hello"

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

class ApplyRecursive(Decorator):
    def __init__(self, *types):
        super().__init__()
        if not len(types):
            types = (dict, list, tuple, set)
        self._types = types

    def call(self, arg):
        if dict in self._types and isinstance(arg, dict):
            return {key: self.call(value) for key, value in arg.items()}

        if set in self._types and isinstance(arg, set):
            return set(self.call(value) for value in arg)

        if tuple in self._types and isinstance(arg, tuple):
            return tuple(self.call(value) for value in arg)

        if list in self._types and isinstance(arg, list):
            return list(self.call(value) for value in arg)

        return self.method(arg)


@ApplyRecursive(tuple, set, dict)
def double(arg):
    return 2*arg

print(double(1))
print(double({'a': 1, 'b': 2}))
print(double({1, 2, 3}))
print(double((1, 2, 3, 4)))
print(double([1, 2, 3, 4, 5]))

Какие отпечатки:

2
{'a': 2, 'b': 4}
{2, 4, 6}
(2, 4, 6, 8)
[1, 2, 3, 4, 5, 1, 2, 3, 4, 5]

Обратите внимание, что в этом примере не был указан тип list в экземпляре декоратора, поэтому в последнем операторе печати метод применяется к самому списку, а не к элементам списка.

4 голосов
/ 17 июня 2013

Вот простой пример создания цепочек декораторов. Обратите внимание на последнюю строку - она ​​показывает, что происходит под одеялом.

############################################################
#
#    decorators
#
############################################################

def bold(fn):
    def decorate():
        # surround with bold tags before calling original function
        return "<b>" + fn() + "</b>"
    return decorate


def uk(fn):
    def decorate():
        # swap month and day
        fields = fn().split('/')
        date = fields[1] + "/" + fields[0] + "/" + fields[2]
        return date
    return decorate

import datetime
def getDate():
    now = datetime.datetime.now()
    return "%d/%d/%d" % (now.day, now.month, now.year)

@bold
def getBoldDate(): 
    return getDate()

@uk
def getUkDate():
    return getDate()

@bold
@uk
def getBoldUkDate():
    return getDate()


print getDate()
print getBoldDate()
print getUkDate()
print getBoldUkDate()
# what is happening under the covers
print bold(uk(getDate))()

Вывод выглядит так:

17/6/2013
<b>17/6/2013</b>
6/17/2013
<b>6/17/2013</b>
<b>6/17/2013</b>
3 голосов
/ 03 марта 2012

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

def counter(func):
    def wrapped(*args, **kws):
        print 'Called #%i' % wrapped.count
        wrapped.count += 1
        return func(*args, **kws)
    wrapped.count = 0
    return wrapped

Таким образом, ваш декоратор может быть повторно использован для различных функций (или использован для декорирования одной и той же функции несколько раз: func_counter1 = counter(func); func_counter2 = counter(func)), и переменная счетчика останется закрытой для каждой.

2 голосов
/ 11 марта 2019

Ответ Паоло Бергантино обладает большим преимуществом только использования stdlib и работает для этого простого примера, где нет декоратора аргументов или декорированной функции аргументов ,

Однако у него есть 3 основных ограничения, если вы хотите заняться более общими случаями:

  • Как уже отмечалось в нескольких ответах, вы не можете легко изменить код, чтобы добавить необязательные аргументы декоратора . Например, создание декоратора makestyle(style='bold') нетривиально.
  • кроме того, оболочки, созданные с помощью @functools.wraps , не сохраняют подпись , поэтому, если предоставлены неверные аргументы, они начнут выполняться и могут вызвать ошибку другого типа, чем обычная TypeError.
  • наконец, в оболочках, созданных с @functools.wraps по , довольно сложно получить доступ к аргументу, основанному на его имени . Действительно, аргумент может появляться в *args, в **kwargs или может не появляться вообще (если это необязательно).

Я написал decopatch, чтобы решить первую проблему, и написал makefun.wraps, чтобы решить две другие. Обратите внимание, что makefun использует тот же трюк, что и знаменитый decorator lib.

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

from decopatch import function_decorator, DECORATED
from makefun import wraps

@function_decorator
def makestyle(st='b', fn=DECORATED):
    open_tag = "<%s>" % st
    close_tag = "</%s>" % st

    @wraps(fn)
    def wrapped(*args, **kwargs):
        return open_tag + fn(*args, **kwargs) + close_tag

    return wrapped

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

from decopatch import function_decorator, WRAPPED, F_ARGS, F_KWARGS

@function_decorator
def makestyle(st='b', fn=WRAPPED, f_args=F_ARGS, f_kwargs=F_KWARGS):
    open_tag = "<%s>" % st
    close_tag = "</%s>" % st
    return open_tag + fn(*f_args, **f_kwargs) + close_tag

В обоих случаях вы можете проверить работоспособность декоратора:

@makestyle
@makestyle('i')
def hello(who):
    return "hello %s" % who

assert hello('world') == '<b><i>hello world</i></b>'    

Подробнее см. В документации .

2 голосов
/ 05 апреля 2013

Украсить функции с различным количеством аргументов:

def frame_tests(fn):
    def wrapper(*args):
        print "\nStart: %s" %(fn.__name__)
        fn(*args)
        print "End: %s\n" %(fn.__name__)
    return wrapper

@frame_tests
def test_fn1():
    print "This is only a test!"

@frame_tests
def test_fn2(s1):
    print "This is only a test! %s" %(s1)

@frame_tests
def test_fn3(s1, s2):
    print "This is only a test! %s %s" %(s1, s2)

if __name__ == "__main__":
    test_fn1()
    test_fn2('OK!')
    test_fn3('OK!', 'Just a test!')

Результат:

Start: test_fn1  
This is only a test!  
End: test_fn1  


Start: test_fn2  
This is only a test! OK!  
End: test_fn2  


Start: test_fn3  
This is only a test! OK! Just a test!  
End: test_fn3  
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...