通过共享一个对象的__dict__属性来创建视图

7

我目前正在寻找一种可维护且易于使用的方式,在 Python 对象上创建“视图”。具体而言,我有一个非常大的类集合,它们共享几个常见方法,我想包装这些类的实例以修改这些方法的行为。

当然,我可以为每个类创建一个新类,遵循包装器模式,重新定义整个接口并将每个方法重定向到原始对象,除了我希望覆盖的那些方法。由于需要大量的代码和在任何类更改时所需的维护,这是不切实际的。

一些尝试表明,我可以通过大量使用元类和内省来生成包装器,以“重新创建”包装器对象中的接口,但这被证明是相当可怕的,特别是如果 A 具有属性(未包含代码)。

第二次尝试表明,可以通过共享__dict__属性并覆盖__class__实现较少的代码。这导致以下代码 (https://repl.it/repls/InfantileAshamedProjector):

##############################
# Existing code
##############################

class A1:
  def __init__(self, eggs):
    self.eggs = eggs

  # Lots of complicated functions and members

  def hello(self):
    print ("hello, you have %d eggs" % self.eggs)

  def meeting(self):
    self.hello()
    print ("goodbye")

  # Lots of complicated functions calling hello.

# Lots of A2, A3, A4 with the same pattern

##############################
# "Magic" code for view generation
##############################

class FutureView:
  pass

def create_view(obj, name):
  class View(obj.__class__):
    def hello(self):
      print ("hello %s, you have %d eggs" % (name, self.eggs))

  view = FutureView()
  view.__dict__ = obj.__dict__
  view.__class__ = View

  return view

##############################
# Sample of use
##############################

a = A1(3)
a.hello() # Prints hello, you have 3 eggs

v = create_view(a, "Bob")
v.hello() # Prints hello Bob, you have 3 eggs

a.eggs = 5
a.hello() # Prints hello, you have 5 eggs
v.hello() # Prints hello Bob, you have 5 eggs

a.meeting() # Prints hello, you have 5 eggs. Goodbye
v.meeting() # Prints hello Bob, you have 5 eggs. Goodbye

这样可以使代码非常简短,修改A1,A2等类不需要对补丁进行任何更改,这非常好。然而,我显然担心在多个类之间共享__dict__的影响。我的问题是:

  • 您是否看到其他实现目标的方法,无论是稍微改进上述方法还是完全不同的方法?(请注意,允许修改/添加类而不需要对修补机制进行任何更改是一项困难要求)
  • 当我有多个对象共享相同的__dict__时,应该注意哪些陷阱?
  • 通过显式提供__dict____class__,创建对象的最佳(少坏一点?)方式是什么?
  • 奖励:如上例所示,我需要将一个额外的信息附加到我的包装器中。因为我不能将其添加到对象的__dict__中,所以我被迫将其添加到类本身中,无论是作为类成员还是“捕获”变量。还有其他位置可以放置它吗?理想情况下,我想避免为每个名称实例都创建一个新类(我只想为每个原始类动态创建新类)。

其他考虑的解决方案:

代理对象(请参见juanpa.arrivillaga答案)是一种几乎完美的解决方案,但在补丁函数被另一个函数内部调用时,它表现不佳。具体来说,在上面发布的代码中,对meeting函数的最终调用将使用原始实现而不是已打补丁的实现。有关示例,请参见https://repl.it/repls/OrneryLongField


我认为这是一个很好的问题,但我有一个小的术语争议:“dict指针”- Python 没有指针 - juanpa.arrivillaga
你的对象的任何方法是否会改变它的状态?如果不会,重复使用__dict__可能并不重要,因为没有任何改变。但如果它们会改变,这可能会导致意想不到的行为(在各种可能的解决方案下)。 - BrenBarn
另外,你真的需要像那样交替使用原始对象和包装对象吗?我的直觉告诉我,你想要的是一种上下文管理器,在进入with块时简单地修改方法,然后在结束时将原始版本放回去。这会使按照你在示例中所做的方式交替调用方法变得麻烦(即先调用a,然后v,然后a,然后v等,而不是a,a,a,a,v,v,v,v),但它会在某种程度上隔离修改后的行为,这似乎更加清晰。 - BrenBarn
不好意思,我想同时使用具有不同“黑客式”设置的多个对象。 - Rémi Bonnet
我的感觉是你所做的是一种hack,但在这种情况下可能是最好的hack。如果你想通过一次性提供所有状态来创建一个实例,那么底层类(A1及其同类)应该被修改以支持这一点;这样你就可以通过向__init__提供参数来提供所有相关状态,而不必通过共享__dict__来复制所有状态。在我看来,你的解决方案基本上是对类没有足够能力复制和访问相关内部状态的问题的一种解决方法。 - BrenBarn
1个回答

2

我觉得你需要一个代理对象,这通常就是视图。简单来说,模式可以像这样(对于只读代理):

最初的回答:听起来你需要一个代理对象,这通常就是视图。简单来说,模式可以像这样(对于只读代理):

class View:
    def __init__(self, obj):
        self._obj = obj
    def __getattr__(self, attr):
        return getattr(self._obj, attr)
__getattr__的好处在于只有在找不到属性时才会调用它。如果您想要写访问权限,那么您需要更加小心,并实现__setattribute__,该方法始终被调用,容易无意中触发无限递归。
请注意,因为我们在代理对象上使用getattr,所以我们不必管理重新创建接口!方法解析、描述符协议(如property)、继承等都由通常的机制处理:
In [1]: class View:
   ...:     def __init__(self, obj):
   ...:         self._obj = obj
   ...:     def __getattr__(self, attr):
   ...:         return getattr(self._obj, attr)
   ...:

In [2]: class UrFoo:
   ...:     def __init__(self, value):
   ...:         self.value = value
   ...:     def foo(self):
   ...:         return self.value
   ...:

In [3]: class Foo(UrFoo):
   ...:     def frognicate(self):
   ...:         return self.value * 42
   ...:     @property
   ...:     def baz(self):
   ...:         return 0
   ...:

In [4]: foo = Foo(8)

In [5]: view = View(foo)

In [6]: view.foo()
Out[6]: 8

In [7]: view.frognicate()
Out[7]: 336

In [8]: view.baz
Out[8]: 0

我目前正在使用这个的变体,但是缺乏继承在对象内部调用修补函数时会导致问题。我将问题情况添加到原始帖子中。即使不是最大的问题,当使用逐步调试时,这也非常令人烦恼。 - Rémi Bonnet
@RémiBonnet 上述解决方案可以处理继承、描述符协议(如 property)等问题。我已经添加了一个使用示例。我能理解为什么逐步调试可能会变得烦人,但在这一点上,这将是实现简单性和可行性之间的权衡,我很抱歉。 - juanpa.arrivillaga
我可以忍受调试问题,但修补后的函数不被对象自身的函数使用是一个阻碍。请查看 https://repl.it/repls/OrneryLongField ,了解__dict__实现和代理实现之间的区别。 - Rémi Bonnet
@RémiBonnet 你可以给实例添加一个方法。 - juanpa.arrivillaga

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接