Как я могу сделать два декоратора в Python, которые будут делать следующее?
@makebold
@makeitalic
def say():
return "Hello"
... который должен вернуть:
"<b><i>Hello</i></b>"
Я не пытаюсь сделать HTML
таким способом в реальном приложении - просто пытаюсь понять, как работают декораторы и цепочки декораторов.
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>"
Похоже, другие люди уже сказали вам, как решить проблему. Я надеюсь, что это поможет вам понять, что такое декораторы.
Декораторы просто синтаксический сахар.
Этот
@decorator
def func():
...
Расширяется до
def func():
...
func = decorator(func)
Чтобы объяснить декоратор простым способом:
С участием:
@decor1
@decor2
def func(*args, **kwargs):
pass
Когда делать:
func(*args, **kwargs)
Вы действительно делаете:
decor1(decor2(func))(*args, **kwargs)
Как я могу сделать два декоратора в 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
заставляет функцию-обертку делать почти все, кроме того, чтобы сказать нам точно, что функция принимает в качестве аргументов.
Существуют и другие модули, которые могут пытаться решить проблему, но решения пока нет в стандартной библиотеке.
Говоря о примере счетчика - как указано выше, счетчик будет разделен между всеми функциями, которые используют декоратор:
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 останется закрытой для каждой.
И, конечно же, вы можете вернуть лямбды из функции декоратора:
def makebold(f):
return lambda: "<b>" + f() + "</b>"
def makeitalic(f):
return lambda: "<i>" + f() + "</i>"
@makebold
@makeitalic
def say():
return "Hello"
print say()
Декораторы 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
#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")
Украсьте функции с различным количеством аргументов:
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
Вот простой пример создания цепочек декораторов. Обратите внимание на последнюю строку - она показывает, что происходит под одеялом.
############################################################
#
# 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>
На этот ответ уже давно дан ответ, но я подумал, что поделюсь своим классом 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 метод применяется к самому списку, а не к элементам списка.
В качестве альтернативы, вы можете написать фабричную функцию, которая возвращает декоратор, который оборачивает возвращаемое значение декорированной функции в тег, передаваемый фабричной функции. Например:
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)))
Еще один способ сделать то же самое:
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'
Декоратор берет определение функции и создает новую функцию, которая выполняет эту функцию и преобразует результат.
@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, которая является внутренней, которая выполняет функцию и преобразует результат.
Вы можете создать два отдельных декоратора, которые будут делать то, что вы хотите, как показано ниже. Обратите внимание на использование *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>
Похожие вопросы
Новые вопросы
python
Python - это многопарадигмальный, динамически типизированный, многоцелевой язык программирования. Он разработан для быстрого изучения, понимания и использования, а также для обеспечения чистого и единообразного синтаксиса. Обратите внимание, что Python 2 официально не поддерживается с 01.01.2020. Тем не менее, для вопросов о Python, связанных с версией, добавьте тег [python-2.7] или [python-3.x]. При использовании варианта Python (например, Jython, PyPy) или библиотеки (например, Pandas и NumPy) включите его в теги.