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

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

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

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

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

2983
Imran 11 Апр 2009 в 11:05

15 ответов

Лучший ответ

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

from functools import wraps

def makebold(fn):
    @wraps(fn)
    def wrapped(*args, **kwargs):
        return "<b>" + fn(*args, **kwargs) + "</b>"
    return wrapped

def makeitalic(fn):
    @wraps(fn)
    def wrapped(*args, **kwargs):
        return "<i>" + fn(*args, **kwargs) + "</i>"
    return wrapped

@makebold
@makeitalic
def hello():
    return "hello world"

@makebold
@makeitalic
def log(s):
    return s

print hello()        # returns "<b><i>hello world</i></b>"
print hello.__name__ # with functools.wraps() this returns "hello"
print log('hello')   # returns "<b><i>hello</i></b>"
2881
Hondros 3 Апр 2019 в 16:23

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

Декораторы просто синтаксический сахар.

Этот

@decorator
def func():
    ...

Расширяется до

def func():
    ...
func = decorator(func)
116
Unknown 11 Апр 2009 в 08:00

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

С участием:

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

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

func(*args, **kwargs)

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

decor1(decor2(func))(*args, **kwargs)
11
changyuheng 7 Фев 2020 в 01:46

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

Вы хотите следующую функцию при вызове:

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

Возвращать:

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

Простое решение

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

def makeitalic(fn):
    return lambda: '<i>' + fn() + '</i>'

def makebold(fn):
    return lambda: '<b>' + fn() + '</b>'

Теперь используйте их по желанию:

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

И сейчас:

>>> say()
'<b><i>Hello</i></b>'

Проблемы с простым решением

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

>>> say
<function <lambda> at 0x4ACFA070>

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

>>> say.__closure__[0].cell_contents
<function <lambda> at 0x4ACFA030>
>>> say.__closure__[0].cell_contents.__closure__[0].cell_contents
<function say at 0x4ACFA730>

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

Полнофункциональное решение - преодоление большинства из этих проблем

У нас есть декоратор wraps из модуля functools в стандартной библиотеке!

from functools import wraps

def makeitalic(fn):
    # must assign/update attributes from wrapped function to wrapper
    # __module__, __name__, __doc__, and __dict__ by default
    @wraps(fn) # explicitly give function whose attributes it is applying
    def wrapped(*args, **kwargs):
        return '<i>' + fn(*args, **kwargs) + '</i>'
    return wrapped

def makebold(fn):
    @wraps(fn)
    def wrapped(*args, **kwargs):
        return '<b>' + fn(*args, **kwargs) + '</b>'
    return wrapped

К сожалению, есть еще какой-то пример, но это настолько просто, насколько мы можем это сделать.

В Python 3 вы также назначаете __qualname__ и __annotations__ по умолчанию.

А сейчас:

@makebold
@makeitalic
def say():
    """This function returns a bolded, italicized 'hello'"""
    return 'Hello'

И сейчас:

>>> say
<function say at 0x14BB8F70>
>>> help(say)
Help on function say in module __main__:

say(*args, **kwargs)
    This function returns a bolded, italicized 'hello'

Вывод

Итак, мы видим, что wraps заставляет функцию-обертку делать почти все, кроме того, чтобы сказать нам точно, что функция принимает в качестве аргументов.

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

19
Aaron Hall 5 Дек 2016 в 17:33

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

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)), и переменная counter останется закрытой для каждой.

6
marqueed 2 Мар 2012 в 21:47

И, конечно же, вы можете вернуть лямбды из функции декоратора:

def makebold(f): 
    return lambda: "<b>" + f() + "</b>"
def makeitalic(f): 
    return lambda: "<i>" + f() + "</i>"

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

print say()
64
Rune Kaagaard 25 Окт 2010 в 06:18

Декораторы Python добавляют дополнительную функциональность к другой функции

Курсив декоратор может быть как

def makeitalic(fn):
    def newFunc():
        return "<i>" + fn() + "</i>"
    return newFunc

Обратите внимание, что функция определена внутри функции. Что он в основном делает, так это заменяет функцию на новую. Например, у меня есть этот класс

class foo:
    def bar(self):
        print "hi"
    def foobar(self):
        print "hi again"

Теперь, скажем, я хочу, чтобы обе функции печатали «---» после и до того, как они будут выполнены. Я мог бы добавить печать «---» до и после каждого оператора печати. Но поскольку я не люблю повторяться, я сделаю декоратор

def addDashes(fn): # notice it takes a function as an argument
    def newFunction(self): # define a new function
        print "---"
        fn(self) # call the original function
        print "---"
    return newFunction
    # Return the newly defined function - it will "replace" the original

Так что теперь я могу изменить свой класс на

class foo:
    @addDashes
    def bar(self):
        print "hi"

    @addDashes
    def foobar(self):
        print "hi again"

Для больше на декораторах, проверьте http://www.ibm.com/developerworks/linux/library/l -cpdecor.html

61
Abhinav Gupta 11 Апр 2009 в 07:19
#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")
8
martineau 24 Июл 2017 в 14:14

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

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  
6
Bibhas Debnath 15 Фев 2014 в 19:09

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

############################################################
#
#    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>
7
resigned 17 Июн 2013 в 04:43

На этот ответ уже давно дан ответ, но я подумал, что поделюсь своим классом 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 в экземпляре декоратора, поэтому в заключительном операторе print метод применяется к самому списку, а не к элементам списка.

8
halfer 11 Ноя 2018 в 15:02

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

from functools import wraps

def wrap_in_tag(tag):
    def factory(func):
        @wraps(func)
        def decorator():
            return '<%(tag)s>%(rv)s</%(tag)s>' % (
                {'tag': tag, 'rv': func()})
        return decorator
    return factory

Это позволяет вам написать:

@wrap_in_tag('b')
@wrap_in_tag('i')
def say():
    return 'hello'

Или

makebold = wrap_in_tag('b')
makeitalic = wrap_in_tag('i')

@makebold
@makeitalic
def say():
    return 'hello'

Лично я бы написал декоратору несколько иначе:

from functools import wraps

def wrap_in_tag(tag):
    def factory(func):
        @wraps(func)
        def decorator(val):
            return func('<%(tag)s>%(val)s</%(tag)s>' %
                        {'tag': tag, 'val': val})
        return decorator
    return factory

Что даст:

@wrap_in_tag('b')
@wrap_in_tag('i')
def say(val):
    return val
say('hello')

Не забудьте конструкцию, для которой синтаксис декоратора является сокращением:

say = wrap_in_tag('b')(wrap_in_tag('i')(say)))
144
2 revsTrevor 11 Апр 2009 в 09:29

Еще один способ сделать то же самое:

class bol(object):
  def __init__(self, f):
    self.f = f
  def __call__(self):
    return "<b>{}</b>".format(self.f())

class ita(object):
  def __init__(self, f):
    self.f = f
  def __call__(self):
    return "<i>{}</i>".format(self.f())

@bol
@ita
def sayhi():
  return 'hi'

Или, более гибко:

class sty(object):
  def __init__(self, tag):
    self.tag = tag
  def __call__(self, f):
    def newf():
      return "<{tag}>{res}</{tag}>".format(res=f(), tag=self.tag)
    return newf

@sty('b')
@sty('i')
def sayhi():
  return 'hi'
20
ROMANIA_engineer 19 Окт 2014 в 18:47

Декоратор берет определение функции и создает новую функцию, которая выполняет эту функцию и преобразует результат.

@deco
def do():
    ...

Эквивалентно:

do = deco(do)

Примере:

def deco(func):
    def inner(letter):
        return func(letter).upper()  #upper
    return inner

Этот

@deco
def do(number):
    return chr(number)  # number to letter

Эквивалентно этому

def do2(number):
    return chr(number)

do2 = deco(do2)

65 <=> 'а'

print(do(65))
print(do2(65))
>>> B
>>> B

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

13
galfisher 19 Сен 2019 в 14:33

Вы можете создать два отдельных декоратора, которые будут делать то, что вы хотите, как показано ниже. Обратите внимание на использование *args, **kwargs в объявлении функции wrapped(), которая поддерживает декорированную функцию с несколькими аргументами (что на самом деле не является обязательным для функции примера say(), но включено для общности ) .

По тем же причинам декоратор functools.wraps используется для изменения мета-атрибутов обернутой функции, чтобы они соответствовали атрибутам декорируемой функции. Это делает сообщения об ошибках и документацию встроенных функций (func.__doc__) такими же, как у оформленной функции, а не wrapped().

from functools import wraps

def makebold(fn):
    @wraps(fn)
    def wrapped(*args, **kwargs):
        return "<b>" + fn(*args, **kwargs) + "</b>"
    return wrapped

def makeitalic(fn):
    @wraps(fn)
    def wrapped(*args, **kwargs):
        return "<i>" + fn(*args, **kwargs) + "</i>"
    return wrapped

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

print(say())  # -> <b><i>Hello</i></b>

Уточнения

Как вы можете видеть, в этих двух декораторах много повторяющегося кода. Учитывая это сходство, вам лучше вместо этого создать общий, который на самом деле был бы фабрикой декораторов - другими словами, функцией декоратора, которая создает другие декораторы. Таким образом, количество повторений кода будет меньше, и можно будет следовать принципу DRY.

def html_deco(tag):
    def decorator(fn):
        @wraps(fn)
        def wrapped(*args, **kwargs):
            return '<%s>' % tag + fn(*args, **kwargs) + '</%s>' % tag
        return wrapped
    return decorator

@html_deco('b')
@html_deco('i')
def greet(whom=''):
    return 'Hello' + (' ' + whom) if whom else ''

print(greet('world'))  # -> <b><i>Hello world</i></b>

Чтобы сделать код более читабельным, вы можете назначить более описательное имя сгенерированным на заводе декоратором:

makebold = html_deco('b')
makeitalic = html_deco('i')

@makebold
@makeitalic
def greet(whom=''):
    return 'Hello' + (' ' + whom) if whom else ''

print(greet('world'))  # -> <b><i>Hello world</i></b>

Или даже объединить их так:

makebolditalic = lambda fn: makebold(makeitalic(fn))

@makebolditalic
def greet(whom=''):
    return 'Hello' + (' ' + whom) if whom else ''

print(greet('world'))  # -> <b><i>Hello world</i></b>

КПД

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

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

Для этого требуется больше кода в самом декораторе, но он выполняется только тогда, когда он применяется к определениям функций, а не позже, когда они сами вызываются. Это также применяется при создании более читаемых имен с использованием функций lambda, как показано ранее. Образец:

def multi_html_deco(*tags):
    start_tags, end_tags = [], []
    for tag in tags:
        start_tags.append('<%s>' % tag)
        end_tags.append('</%s>' % tag)
    start_tags = ''.join(start_tags)
    end_tags = ''.join(reversed(end_tags))

    def decorator(fn):
        @wraps(fn)
        def wrapped(*args, **kwargs):
            return start_tags + fn(*args, **kwargs) + end_tags
        return wrapped
    return decorator

makebolditalic = multi_html_deco('b', 'i')

@makebolditalic
def greet(whom=''):
    return 'Hello' + (' ' + whom) if whom else ''

print(greet('world'))  # -> <b><i>Hello world</i></b>
37
martineau 17 Окт 2019 в 12:14