functools.wraps 等价于类装饰器

Posted

技术标签:

【中文标题】functools.wraps 等价于类装饰器【英文标题】:functools.wraps equivalent for class decorator 【发布时间】:2015-04-21 17:36:46 【问题描述】:

当我们装饰函数时,我们使用functools.wraps 使装饰的函数看起来像原来的一样。

当我们想装饰班级时,有什么可以做的吗?

def some_class_decorator(cls_to_decorate):
    class Wrapper(cls_to_decorate):
        """Some Wrapper not important doc."""
        pass
    return Wrapper


@some_class_decorator
class MainClass:
    """MainClass important doc."""
    pass


help(MainClass)

输出:

class Wrapper(MainClass)
 |  Some Wrapper not important doc.
 |  
 |  Method resolution order:
 |      Wrapper
 |      MainClass
 |      builtins.object
 |  
 # ... no MainClass important doc below.

我尝试为基于functools.wraps 源代码的类装饰器编写等效包装,但我的实现不正确:

import functools


def wraps_cls(original_cls):
    def wrapper(wrapper_cls):
        """Update wrapper_cls to look like original_cls."""
        for attr in functools.WRAPPER_ASSIGNMENTS:
            try:
                value = getattr(original_cls, attr)
            except AttributeError:
                pass
            else:
                setattr(wrapper_cls, attr, value)
        return wrapper_cls
    return wrapper


def some_class_decorator(cls_to_decorate):
    @wraps_cls(cls_to_decorate)
    class Wrapper(cls_to_decorate):
        """Some Wrapper not important doc."""
        pass
    return Wrapper


@some_class_decorator
class MainClass:
    """MainClass important doc."""
    pass


help(MainClass)

输出:

class MainClass(MainClass)
 |  MainClass important doc.
 |  
 |  Method resolution order:
 |      MainClass
 |      MainClass
 |      builtins.object
 |  
 # ... MainClass doc is here but "Method resolution order" is broken.

有没有用未修饰的 MainClass 帮助输出完全替换修饰的 MainClass 帮助输出?

【问题讨论】:

您的装饰器返回一个新类,它是原始(包装)类的子类。您无法获得相同的方法解析顺序,因为您在层次结构中添加了一个额外的类。 谢谢,你的 wraps_cls 方法帮助我用参数包装一个类,同时保留类名。 使用@functools.wraps(cls, updated=()) 可以让您免于编写自己的包装器。但它也遇到了同样的问题。 【参考方案1】:

不,没有,假设你的装饰器真的像some_class_decorator 那样子类化了包装类。 help 的输出由 pydoc.Helper 类定义,对于类最终调用 pydoc.text.docclass,其中包含以下代码:

# List the mro, if non-trivial.
mro = deque(inspect.getmro(object))
if len(mro) > 2:
    push("Method resolution order:")
    for base in mro:
        push('    ' + makename(base))
    push('')

您可以看到它是硬编码的,以显示班级的真实 MRO。这是应该的。您上一个示例中显示的 MRO 没有“损坏”,它是正确的。通过使您的包装类从被包装类继承,您向继承层次结构中添加了一个附加类。向 MRO 展示忽略了这一点会产生误导,因为那里确实有一个类。在您的示例中,此包装器类不提供其自身的任何行为,但实际的包装器类会(或者您为什么要进行包装?),并且您想知道包装器的哪些行为类和包装类中的哪个。

如果您愿意,您可以制作一个装饰器,使用从原始名称派生的名称动态重命名包装类,因此 MRO 会在适当的位置显示类似 DecoratorWrapper_of_MainClass 的内容。这是否比仅仅拥有Wrapper 更具可读性还有待商榷。

【讨论】:

"并且您想知道哪些行为来自包装类,哪些来自包装类" - 是的,这是真的,但与装饰函数 (functools.wraps) 一样,原始 MainClass 的doc 比 Wrapper 的 doc 更重要。事实证明,如果我装饰类,我无法通过 help() 看到 MainClass 的文档。但是我明白你的解释,谢谢。可能最好的方法是将 MainClass 的文档添加到 Wrapper 的 wraps_cls 文档中。 @GerasimovMikhail:但这是一个单独的问题。编写一个显示 MainClass 的 __doc__ 的包装器并不难;如您所见,您现有的解决方案已经这样做了。但是help 向您展示的不仅仅是__doc__,而且您不能干扰其他输出;你只控制__doc__【参考方案2】:

哦,我想我现在明白你想要达到什么目的了。

您想使用类装饰器从“包装器”附加新方法。

这是一个工作示例:

class Wrapper(object):
    """Some Wrapper not important doc."""
    def another_method(self):
        """Another method."""
        print 'Another method'


def some_class_decorator(cls_to_decorate):
    return type(cls_to_decorate.__name__, cls_to_decorate.__bases__, dict
        (cls_to_decorate.__dict__, another_method=Wrapper.__dict__['another_method']))


class MainClass(object):
    """MainClass important doc."""
    def method(self):
        """A method."""
        print "A method"


help(MainClass)
_MainClass = some_class_decorator(MainClass)
help(_MainClass)
_MainClass().another_method()
MainClass().another_method()

这个例子创建了一个新的类而不修改旧的类。

但我猜你也可以只在旧类中注入给定的方法,并对其进行适当的修改。

【讨论】:

谢谢。这种方法可以解决。但我认为 BrenBarn 是正确的,help() 应该显示真正的 MRO。我认为修改 Wrapper 的类名(可能是 doc)会比我想先尝试完全替换 help() 输出更正确。 这个解决方案展示了真正的 MRO,因为您没有从包装器继承“包装”类。您只需采取几种方法形成“捐助者”。但是你为什么需要这个?为什么不创建一个 mixin 类而不是使用装饰器?

以上是关于functools.wraps 等价于类装饰器的主要内容,如果未能解决你的问题,请参考以下文章

适用于类方法和实例方法的装饰器

Python 中实现装饰器时使用 @functools.wraps 的理由

学习笔记2-functools.wraps 装饰器

[转] functools.wraps定义函数装饰器

python functools.wraps装饰器模块

装饰器中的@functools.wraps的作用