在 Python 中实现装饰器模式

Posted

技术标签:

【中文标题】在 Python 中实现装饰器模式【英文标题】:Implementing the decorator pattern in Python 【发布时间】:2011-03-08 07:55:46 【问题描述】:

我想在 Python 中实现decorator pattern,我想知道是否有一种方法可以编写一个只实现它想要修改的函数的装饰器,而无需为刚刚转发到的所有函数编写样板文件被装饰的对象。像这样:

class foo(object):
    def f1(self):
        print "original f1"
    def f2(self):
        print "original f2"

class foo_decorator(object):
    def __init__(self, decoratee):
        self._decoratee = decoratee
    def f1(self):
        print "decorated f1"
        self._decoratee.f1()
    def f2(self):              # I would like to leave that part out
        self._decoratee.f2()

我希望自动将呼叫foo_decorator.f2 转接到decoratee.f2。有没有办法编写一个通用方法,将所有未实现的函数调用转发到decoratee

【问题讨论】:

你能举一个简单的子类化不起作用的例子吗?您也可以动态子类化 - 这种模式似乎是一种解决方法,适用于无法做到这一点或不支持多重继承的语言。 我想在运行时装饰对象。我想将不同的装饰器应用于一个对象并能够再次删除它们。子类化不能在创建实例后更改它,或者可以吗? 如果你有 A 类并更改 A,即添加一个新方法 A.foo = lambda self: self 这将反映在 A .. 的所有实例上,因为 everything 是在运行时确定。生成绝对不可维护的代码的好方法。 @THC4K:装饰器模式(与 python 装饰器相反)用于在运行时向 object 添加行为。如果正确完成,这实际上是非常可维护的,这就是我发布这个问题的原因。我想在 Python 中找到解决方法。 【参考方案1】:

你可以使用__getattr__:

class foo(object):
    def f1(self):
        print "original f1"
    def f2(self):
        print "original f2"

class foo_decorator(object):
    def __init__(self, decoratee):
        self._decoratee = decoratee
    def f1(self):
        print "decorated f1"
        self._decoratee.f1()
    def __getattr__(self, name):
        return getattr(self._decoratee, name)

u = foo()
v = foo_decorator(u)
v.f1()
v.f2()

【讨论】:

这是个好主意。但是需要注意的是,如果你使用抽象基类(即 import abc 并使用 metaclass = abc.ABCMeta 来定义抽象方法和属性),那么它将不起作用。 这不适用于像 str 这样的函数。那里发生了什么? @JohnKitchin:重定向不适用于双下划线方法,因为这些必须(出于性能原因)在类而不是实例上定义。 (more info) 我相信唯一的解决方法是显式定义和重定向方法,如 __str__ 这真的是设置装饰器模式的“公认”方式吗?覆盖“双下划线”方法感觉很hacky。【参考方案2】:

作为 Philipp 答案的补充;如果您不仅需要装饰,还需要保留对象的 type,Python 允许您在运行时对实例进行子类化:

class foo(object):
    def f1(self):
        print "original f1"

    def f2(self):
        print "original f2"


class foo_decorator(object):
    def __new__(cls, decoratee):
        cls = type('decorated',
                   (foo_decorator, decoratee.__class__),
                   decoratee.__dict__)
        return object.__new__(cls)

    def f1(self):
        print "decorated f1"
        super(foo_decorator, self).f1()


u = foo()
v = foo_decorator(u)
v.f1()
v.f2()
print 'isinstance(v, foo) ==', isinstance(v, foo)

对于您的示例来说,这比绝对必要的要复杂一些,因为您事先知道要装饰的类。

可能就足够了:

class foo_decorator(foo):
    def __init__(self, decoratee):
        self.__dict__.update(decoratee.__dict__)

    def f1(self):
        print "decorated f1"
        super(foo_decorator, self).f1()

【讨论】:

这似乎再次执行了被装饰者的 __init__(),传递给它decoratee,这可能是不希望的。【参考方案3】:

这可能不是最佳实践,但您可以向实例添加功能,正如我所做的那样,帮助将我的代码从 Django 的 ORM 转换为 SQLAlachemy,如下所示:

def _save(self):
    session.add(self)
    session.commit()
setattr(Base,'save',_save)

【讨论】:

我也考虑过这个,但我觉得很不对劲。也许是因为我来自 C++? 感觉不对是公平的。它可以被认为是猴子补丁。然而,它适用于很少的代码,这是一个非常不同的世界,当一切都是动态的并且通过引用时,它与 C 相比有更多的可能性。 我喜欢它。对于这个例子,其他任何事情都会增加 IMO 的复杂性。【参考方案4】:

链接的 Wikipedia 文章中的 UML 图是错误的,您的代码也是如此。

如果您遵循“装饰器模式”,则装饰器类派生自基装饰类。 (在 UML 图中,缺少从 WindowDecorator 到 Window 的继承箭头)。

class foo_decorator(foo):

您不需要实现未修饰的方法。

顺便说一句:在强类型语言中还有一个原因,为什么装饰器必须从装饰类派生:否则你将无法链接装饰器。

【讨论】:

UML 图显示了基类的继承和聚合(装饰器模式的两个重要部分)。你从基类继承,所以你看起来像原来的,你持有对基类实例的引用,你可以隔离对它的访问。***文章在第 1 步和第 2 步中这样说:“(1) 将原始组件子类化,(2) 添加一个组件指针作为字段”。所以最初的问题实际上是关于 Python 鸭子类型,而不是关于装饰器模式,也不是关于 Python 装饰器!【参考方案5】:

在我的一个项目中,我还需要做一件事,那就是即使是底层对象也应该实际执行在装饰器中重新实现的方法。如果您知道目标位置,实际上很容易做到。

用例是:

我有一个带有方法 A 和 B 的对象 X。 我创建了一个覆盖 A 的装饰器类 Y。 如果我实例化 Y(X) 并调用 A,它将按预期使用修饰后的 A。 如果 B 调用 A,那么如果我实例化 Y(X) 并在装饰器上调用 B,则从 B 内部的调用会转到原始对象上的旧 A,这是不希望的。我希望旧 B 也调用新 A。

有可能达到这样的行为:

import inspect
import six      # for handling 2-3 compatibility

class MyBaseDecorator(object):
    def __init__(self, decorated):
        self.decorated = decorated

    def __getattr__(self, attr):
       value = getattr(self.decorated, attr)
       if inspect.ismethod(value):
           function = six.get_method_function(value)
           value = function.__get__(self, type(self))
       return value

class SomeObject(object):
    def a(self):
        pass

    def b(self):
        pass

class MyDecorator(MyBaseDecorator):
    def a(self):
        pass

decorated = MyDecorator(SomeObject())

这可能无法开箱即用,因为我从头顶输入了除了 getattr 方法之外的所有其他内容。

代码在装饰对象中查找请求的属性,如果它是一个方法(现在不适用于属性,但支持它们的更改应该不会太难),然后代码拉取实际函数在方法之外并使用描述符接口调用它将函数“重新绑定”为方法,但在装饰器上。然后它被返回并很可能被执行。

这样做的效果是,如果b 曾经在原始对象上调用a,那么当你装饰了对象并且有来自装饰器的任何方法调用时,装饰器确保所有访问的方法都是而是绑定到装饰器,因此使用装饰器而不是原始对象查找事物,因此装饰器中指定的方法优先。

P.S.:是的,我知道它看起来很像继承,但这是在多个对象的组合意义上完成的。

【讨论】:

【参考方案6】:

补充@Alec Thomas 的回复。我修改了他的答案以遵循装饰者模式。这样你就不需要提前知道你要装饰的班级。

class Decorator(object):
    def __new__(cls, decoratee):
        cls = type('decorated',
                   (cls, decoratee.__class__),
                   decoratee.__dict__)
        return object.__new__(cls)

然后,您可以将其用作:

class SpecificDecorator(Decorator):
    def f1(self):
        print "decorated f1"
        super(foo_decorator, self).f1()

class Decorated(object):
    def f1(self):
        print "original f1"


d = SpecificDecorator(Decorated())
d.f1()

【讨论】:

【参考方案7】:

在 Python 3 中,Philipp 接受的答案提出了RuntimeError: maximum recursion depth exceeded

对我有用的方式:

class Foo(object):
    def f1(self):
        print("original f1")

    def f2(self):
        print("original f2")

class FooDecorator(object):
    def __init__(self, decoratee):
        self._decoratee = decoratee

    def f1(self):
        print("decorated f1")
        return self._decoratee.f1()

    def __getattr__(self, name):
        if name in ['f1', '_decoratee']:
            raise AttributeError()
        return getattr(self._decoratee, name)

f = FooDecorator(Foo())
f.f1()
# decorated f1
# original f1
f.f2()
# original f2

解决方法的灵感来自Ned Batchelder's blog

【讨论】:

以上是关于在 Python 中实现装饰器模式的主要内容,如果未能解决你的问题,请参考以下文章

Python中实现装饰模式的三种方式

装饰器实现单例模式

设计模式——装饰器模式

设计模式之装饰器模式

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

在 PHP 中构造装饰器