我如何在Python中制作两个装饰器来完成以下操作?

@make_bold
@make_italic
def say():
   return "Hello"

调用say()应返回:

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

当前回答

您可以制作两个独立的装饰器,如下图所示。请注意,在wrapped()函数的声明中使用了*args,**kwargs,该函数支持具有多个参数的修饰函数(对于示例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>

效率

虽然上面的示例都能完成所有工作,但当同时应用多个修饰符时,生成的代码会以外部函数调用的形式产生大量开销。这可能无关紧要,具体取决于确切的用法(例如,可能是I/O绑定的)。

如果修饰函数的速度很重要,那么可以通过编写一个稍微不同的修饰器工厂函数来保持单个额外函数调用的开销,该函数实现一次添加所有标记,从而可以生成代码,避免对每个标记使用单独的修饰器而导致的额外函数调用。

这需要在decorator本身中有更多的代码,但这只在将其应用于函数定义时运行,而不是在稍后调用它们本身时运行。这也适用于通过使用前面所示的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>

其他回答

用于绘制图像的嵌套装饰器的又一示例:

import matplotlib.pylab as plt

def remove_axis(func):
    def inner(img, alpha):
        plt.axis('off')
        func(img, alpha)
    return inner

def plot_gray(func):
    def inner(img, alpha):
        plt.gray()
        func(img, alpha)
    return inner

@remove_axis
@plot_gray
def plot_image(img, alpha):
    plt.imshow(img, alpha=alpha)
    plt.show()

现在,让我们先使用嵌套的装饰器显示一个没有轴标签的彩色图像:

plot_image(plt.imread('lena_color.jpg'), 0.4)

接下来,让我们使用嵌套的装饰器remove_axis和plot_gray显示一个没有轴标签的灰度图像(我们需要cmap='gray',否则默认的颜色映射是viridis,因此除非明确指定,否则默认情况下灰度图像不会以黑白色显示)

plot_image(plt.imread('lena_bw.jpg'), 0.8)

上述函数调用缩减为以下嵌套调用

remove_axis(plot_gray(plot_image))(img, alpha)

查看文档以了解装饰器是如何工作的。以下是您的要求:

from functools import wraps

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

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

@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>"

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

或者,您可以编写一个工厂函数,该函数返回一个装饰器,该装饰器将装饰函数的返回值包装在传递给工厂函数的标记中。例如:

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'

or

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')

不要忘了decorator语法是一种简写的构造:

say = wrap_in_tag('b')(wrap_in_tag('i')(say)))

您可以制作两个独立的装饰器,如下图所示。请注意,在wrapped()函数的声明中使用了*args,**kwargs,该函数支持具有多个参数的修饰函数(对于示例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>

效率

虽然上面的示例都能完成所有工作,但当同时应用多个修饰符时,生成的代码会以外部函数调用的形式产生大量开销。这可能无关紧要,具体取决于确切的用法(例如,可能是I/O绑定的)。

如果修饰函数的速度很重要,那么可以通过编写一个稍微不同的修饰器工厂函数来保持单个额外函数调用的开销,该函数实现一次添加所有标记,从而可以生成代码,避免对每个标记使用单独的修饰器而导致的额外函数调用。

这需要在decorator本身中有更多的代码,但这只在将其应用于函数定义时运行,而不是在稍后调用它们本身时运行。这也适用于通过使用前面所示的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>