【问题标题】:Wrapping objects to extend/add functionality while working around isinstance在解决 isinstance 时包装对象以扩展/添加功能
【发布时间】:2009-02-23 22:17:28
【问题描述】:

在 Python 中,我看到了使用保持或包装来扩展对象或类的功能而不是继承的建议。特别是,我认为 Alex Martelli 在他的 Python Design Patterns 演讲中谈到了这一点。我已经在库中看到这种模式用于依赖注入,例如pycontainer

我遇到的一个问题是,当我必须与使用 isinstance anti-pattern,此模式失败,因为持有/包装对象未通过 isinstance 测试。如何设置保持/包装对象以绕过不必要的类型检查?这可以通用吗?从某种意义上说,我需要类似于保留签名的函数装饰器的类实例(例如,simple_decorator 或 Michele Simionato 的 decorator)。

限定条件:我并不是说所有isinstance 的使用都是不恰当的;几个答案对此提出了很好的观点。也就是说,应该认识到isinstance 的使用对对象交互造成了重大限制——它迫使继承成为多态性的来源,而不是行为

对于究竟是如何/为什么会出现问题似乎有些困惑,所以让我提供一个简单的例子(大致摘自pycontainer)。假设我们有一个类 Foo,以及一个 FooFactory。为了这个例子,假设我们希望能够实例化记录每个函数调用的 Foo 对象,或者不考虑 AOP。此外,我们希望在不以任何方式修改 Foo 类/源的情况下做到这一点(例如,我们实际上可能正在实现一个通用工厂,它可以动态地将日志记录功能添加到任何类实例)。对此的第一次尝试可能是:

class Foo(object):
    def bar():
        print 'We\'re out of Red Leicester.'

class LogWrapped(object):
    def __init__(self, wrapped):
        self.wrapped = wrapped
    def __getattr__(self, name):
        attr = getattr(self.wrapped, name)
        if not callable(attr):
            return attr
        else:
            def fun(*args, **kwargs):
                print 'Calling ', name
                attr(*args, **kwargs)
                print 'Called ', name
            return fun

class FooFactory(object):
    def get_foo(with_logging = False):
        if not with_logging:
            return Foo()
        else:
            return LogWrapped(Foo())

foo_fact = FooFactory()
my_foo = foo_fact.get_foo(True)
isinstance(my_foo, Foo) # False!

您可能希望完全以这种方式做事(使用装饰器等),但请记住:

  • 我们不想接触 Foo 类。假设我们正在编写可供我们还不了解的客户使用的框架代码。
  • 重点是返回一个本质上是 Foo 的对象,但具有附加功能。对于任何其他期望 Foo 的客户端代码,它应该尽可能地显示为 Foo。因此希望在isinstance 周围工作。
  • 是的,我知道我不需要工厂类(在这里先发制人地为自己辩护)。

【问题讨论】:

  • 面对实例,我无助地愤怒地握着我的拳头。你不能用 isinstance 修复代码吗? stackoverflow.com/questions/423823/…
  • 无助的愤怒 :) 在某些情况下,我可以访问 isinstance。但另一个用例是想要编写与其他人相处得很好的“框架”代码(例如,上面的 pycontainer)。

标签: python design-patterns


【解决方案1】:

如果您依赖的库代码使用isinstance 并依赖于继承,为什么不遵循这条路线?如果您无法更改库,那么最好保持一致。

我也认为isinstance 有合法用途,并且随着2.6 中abstract base classes 的引入,这已得到官方承认。在某些情况下,isinstance 确实是正确的解决方案,而不是使用 hasattr 或使用异常进行鸭式输入。

如果出于某种原因您真的不想使用继承,则有一些肮脏的选择:

  • 您只能使用实例方法修改类实例。使用new.instancemethod,您可以为您的实例创建包装方法,然后调用原始类中定义的原始方法。这似乎是唯一既不修改原始类也不定义新类的选项。

如果你可以在运行时修改类,有很多选择:

  • 使用运行时 mixin,即只需将一个类添加到类的 __base__ 属性中。但这更多用于添加特定功能,而不是用于不知道需要包装什么的不加选择的包装。

  • Dave 回答中的选项(Python 中的类装饰器 >= 2.6 或元类)。

编辑:对于您的具体示例,我想只有第一个选项有效。但我仍然会考虑创建LogFoo 的替代方案,或者为特定的事情(如日志记录)选择完全不同的解决方案。

【讨论】:

    【解决方案2】:

    要记住的一件事是,如果您采用继承路线,则不必使用基类中的任何内容。您可以创建一个从不添加任何具体实现的存根类继承。我已经多次这样做了:

    class Message(object):
         pass
    
    class ClassToBeWrapped(object):
        #...
    
    class MessageWithConcreteImplementation(Message):
        def __init__(self):
            self.x = ClassToBeWrapped()
        #... add concrete implementation here
    
    x = MessageWithConcreteImplementation()
    isinstance(x, Message)
    

    如果你需要从其他东西继承,我想你可能会遇到多重继承的一些问题,但如果你不提供任何具体的实现,这应该是相当小的。

    我遇到的一个问题是,当我必须与使用 isinstance 反模式的代码交互时

    我同意尽可能避免使用 isinstance,但我不确定是否将其称为反模式。使用 isinstance 有一些正当理由。例如,有一些消息传递框架使用它来定义消息。例如,如果你得到一个继承自 Shutdown 的类,那么就该关闭子系统了。

    【讨论】:

      【解决方案3】:

      【讨论】:

        【解决方案4】:

        我的第一个冲动是尝试修复使用isinstance 的违规代码。否则,您只是将其设计错误传播到您自己的设计中。有什么不能修改的原因吗?

        编辑: 所以你的理由是你正在编写你希望人们能够在所有情况下使用的框架/库代码,即使他们想使用 isinstance?

        我认为这有几个问题:

        • 您正在尝试支持一个损坏的范式
        • 您是定义库及其接口的人,正确使用它取决于用户。
        • 您无法预料图书馆用户会进行所有糟糕的编程,因此尝试支持糟糕的编程实践几乎是徒劳的

        我认为您最好编写惯用的、设计良好的代码。好的代码(和坏的代码)有传播的趋势,所以让你的代码成为一个例子。希望它将导致代码质量的整体提高。走另一条路只会继续质量下降。

        【讨论】:

        • 假设 isinstance 总是一个坏主意。有使用 isinstance 的有效时间。
        【解决方案5】:

        如果您正在编写一个需要接受来自 API 用户的某种输入的框架,那么我没有理由想到使用 isinstance。尽管它可能很丑,但我总是只是检查它是否真的提供了我想要使用的接口:

        def foo(bar):
            if hasattr(bar, "baz") and hasattr(bar, "quux"):
                twiddle(bar.baz, bar.quux())
            elif hasattr(bar, "quuux"):
                etc...
        

        如果 API 用户想要使用它,我还经常提供一个很好的类来继承默认功能:

        class Bar:
            def baz(self):
                return self.quux("glu")
        
            def quux(self):
                raise NotImplemented
        

        【讨论】:

          猜你喜欢
          • 2012-03-17
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2022-01-23
          • 2021-10-29
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多