Python: 类似super()的代理对象,从指定类开始MRO搜索

4 投票
2 回答
1006 浏览
提问于 2025-04-17 10:07

根据文档,super(cls, obj) 返回的是

一个代理对象,它会把方法调用转发给类型为 cls 的父类或兄弟类。

我明白为什么 super() 提供这样的功能,但我需要的稍微有点不同:我想创建一个代理对象,它会把方法调用(和属性查找)转发给类 cls 本身;而且就像 super 一样,如果 cls 没有实现这个方法或属性,我的代理应该继续按照 MRO(方法解析顺序)查找(是新的类,而不是原来的类)。有没有什么函数可以实现这个?

举个例子:

class X:
  def act():
    #...

class Y:
  def act():
    #...

class A(X, Y):
  def act():
    #...

class B(X, Y):
  def act():
    #...

class C(A, B):
  def act():
    #...

c = C()
b = some_magic_function(B, c)
# `b` needs to delegate calls to `act` to B, and look up attribute `s` in B
# I will pass `b` somewhere else, and have no control over it

当然,我可以用 b = super(A, c),但这需要我知道确切的类层次结构并且要确保 B 在 MRO 中跟在 A 后面。如果这两个假设中的任何一个将来发生变化,它就会悄悄地出错。(注意,super 并不做任何这样的假设!)

如果我只需要调用 b.act(),我可以用 B.act(c)。但我把 b 传给别人,而我不知道他们会怎么处理它。我需要确保它不会背叛我,某个时候开始表现得像 class C 的实例。

还有一个问题,super() 的文档(在 Python 3.2 中)只谈到了它的方法代理,并没有说明代理的属性查找也是以同样的方式进行的。这是一个意外的遗漏吗?

编辑

更新后的 Delegate 方法在以下示例中也能正常工作:

class A:
    def f(self):
        print('A.f')
    def h(self):
        print('A.h')
        self.f()

class B(A):
    def g(self):
        self.f()
        print('B.g')
    def f(self):
        print('B.f')
    def t(self):
        super().h()


a_true = A()
# instance of A ends up executing A.f
a_true.h()

b = B()
a_proxy = Delegate(A, b)
# *unlike* super(), the updated `Delegate` implementation would call A.f, not B.f
a_proxy.h()

注意,更新后的 class Delegate 更接近我想要的功能,原因有两个:

  1. super() 只在第一次调用时进行代理;后续的调用会正常进行,因为到那时对象已经被使用,而不是它的代理。

  2. super() 不允许访问属性。

因此,我的问题在 Python 中几乎有一个完美的答案。

结果发现,从更高的层面来看,我试图做一些不该做的事情(请看我在这里的评论)。

2 个回答

2

这是一个单独的问题,关于Python 3.2中super()的文档,只提到了它的方法委托,并没有说明代理的属性查找也是以同样的方式进行的。这是个意外的遗漏吗?

不是的,这并不是意外。super() 对属性查找没有任何作用。原因是,实例上的属性并不和某个特定的类关联,它们只是存在于那儿。想想下面这个例子:

class A:
    def __init__(self):
        self.foo = 'foo set from A'

class B(A):
    def __init__(self):
        super().__init__()
        self.bar = 'bar set from B'

class C(B):
    def method(self):
        self.baz = 'baz set from C'

class D(C):
    def __init__(self):
        super().__init__()
        self.foo = 'foo set from D'
        self.baz = 'baz set from D'

instance = D()
instance.method()
instance.bar = 'not set from a class at all'

哪个类“拥有”foobarbaz

如果我想把instance看作是C类的实例,那么在调用method之前,它应该有一个baz属性吗?之后呢?

如果我把instance看作是A类的实例,foo应该是什么值?bar应该不可见,因为它只是在B类中添加的,还是应该可见,因为它是在类外部设置的?

在Python中,这些问题都是无意义的。没有办法设计一个像Python这样的系统,能给出合理的答案。__init__在给类的实例添加属性方面并没有特别之处;它只是一个普通的方法,恰好在实例创建过程中被调用。任何方法(或者说来自其他类的代码,甚至不属于任何类的代码)都可以在它有引用的任何实例上创建属性。

实际上,instance的所有属性都存储在同一个地方:

>>> instance.__dict__
{'baz': 'baz set from C', 'foo': 'foo set from D', 'bar': 'not set from a class at all'}

没有办法判断这些属性最初是哪个类设置的,或者最后是哪个类设置的,或者你想要的任何所有权标准。也没有办法像在C++中那样获取“被D.foo遮蔽的A.foo”,它们是同一个属性,任何一个类(或其他地方)的写入都会覆盖另一个类留下的值。

因此,super()在属性查找时并不会像方法查找那样工作;它不能这样做,你写的代码也不能。


事实上,通过一些实验发现,super和Sven的Delegate实际上根本不支持直接的属性获取!

class A:
    def __init__(self):
        self.spoon = 1
        self.fork = 2

    def foo(self):
        print('A.foo')

class B(A):
    def foo(self):
        print('B.foo')

b = B()

d = Delegate(A, b)
s = super(B, b)

但它们在方法调用时表现正常:

>>> d.foo()
A.foo
>>> s.foo()
A.foo

但是:

>>> d.fork
Traceback (most recent call last):
  File "<pyshell#43>", line 1, in <module>
    d.fork
  File "/tmp/foo.py", line 6, in __getattr__
    x = getattr(self._delegate_cls, name)
AttributeError: type object 'A' has no attribute 'fork'
>>> s.spoon
Traceback (most recent call last):
  File "<pyshell#45>", line 1, in <module>
    s.spoon
AttributeError: 'super' object has no attribute 'spoon'

所以它们实际上只适用于调用某些方法,而不适用于传递给任意第三方代码,以假装是你想要委托的类的实例。

不幸的是,在多重继承的情况下,它们的行为并不相同。假设:

class Delegate:
    def __init__(self, cls, obj):
        self._delegate_cls = cls
        self._delegate_obj = obj
    def __getattr__(self, name):
        x = getattr(self._delegate_cls, name)
        if hasattr(x, "__get__"):
            return x.__get__(self._delegate_obj)
        return x


class A:
    def foo(self):
        print('A.foo')

class B:
    pass

class C(B, A):
    def foo(self):
        print('C.foo')

c = C()

d = Delegate(B, c)
s = super(C, c)

那么:

>>> d.foo()
Traceback (most recent call last):
  File "<pyshell#50>", line 1, in <module>
    d.foo()
  File "/tmp/foo.py", line 6, in __getattr__
    x = getattr(self._delegate_cls, name)
AttributeError: type object 'B' has no attribute 'foo'
>>> s.foo()
A.foo

因为Delegate忽略了_delegate_obj实例的完整方法解析顺序(MRO),只使用_delegate_cls的MRO。而super则按照你在问题中要求的那样工作,但行为似乎相当奇怪:它并不是在包装C的实例以假装成B的实例,因为B的直接实例并没有定义foo

这是我的尝试:

class MROSkipper:
    def __init__(self, cls, obj):
        self.__cls = cls
        self.__obj = obj

    def __getattr__(self, name):
        mro = self.__obj.__class__.__mro__
        i = mro.index(self.__cls)
        if i == 0:
            # It's at the front anyway, just behave as getattr
            return getattr(self.__obj, name)
        else:
            # Check __dict__ not getattr, otherwise we'd find methods
            # on classes we're trying to skip
            try:
                return self.__obj.__dict__[name]
            except KeyError:
                return getattr(super(mro[i - 1], self.__obj), name)

我依赖于类的__mro__属性来正确确定起始位置,然后我就使用super。如果回退一步使用super的奇怪性让你觉得不舒服,你也可以从那个点开始自己遍历MRO链,检查类的__dict__来查找方法。

我没有尝试处理不寻常的属性;那些用描述符实现的属性(包括属性),或者那些Python在后台查找的魔法方法,通常是从类而不是直接从实例开始的。但这样做的行为在你要求的情况下表现得还算不错(有个警告在我帖子前面已经说得很清楚;以这种方式查找属性不会给你与直接在实例中查找不同的结果)。

3

这个类应该涵盖最常见的情况:

class Delegate:
    def __init__(self, cls, obj):
        self._delegate_cls = cls
        self._delegate_obj = obj
    def __getattr__(self, name):
        x = getattr(self._delegate_cls, name)
        if hasattr(x, "__get__"):
            return x.__get__(self._delegate_obj)
        return x

使用方法如下:

b = Delegate(B, c)

(用你示例代码中的名称。)

限制条件:

  1. 你不能通过这个代理从构造函数中传入的类获取一些特殊属性,比如 __class__ 等等。(这个限制同样适用于 super。)

  2. 如果你想获取的属性是某种奇怪的描述符,这可能会表现得很奇怪。

编辑:如果你希望更新后的问题中的代码能够按预期工作,可以使用以下代码:

class Delegate:
    def __init__(self, cls):
        self._delegate_cls = cls
    def __getattr__(self, name):
        x = getattr(self._delegate_cls, name)
        if hasattr(x, "__get__"):
            return x.__get__(self)
        return x

这段代码将代理对象作为 self 参数传递给任何被调用的方法,并且完全不需要原始对象,因此我从构造函数中删除了它。

如果你还想让实例属性可以访问,可以使用这个版本:

class Delegate:
    def __init__(self, cls, obj):
        self._delegate_cls = cls
        self._delegate_obj = obj
    def __getattr__(self, name):
        if name in vars(self._delegate_obj):
            return getattr(self._delegate_obj, name)
        x = getattr(self._delegate_cls, name)
        if hasattr(x, "__get__"):
            return x.__get__(self)
        return x

撰写回答