Python: 类似super()的代理对象,从指定类开始MRO搜索
根据文档,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
更接近我想要的功能,原因有两个:
super()
只在第一次调用时进行代理;后续的调用会正常进行,因为到那时对象已经被使用,而不是它的代理。super()
不允许访问属性。
因此,我的问题在 Python 中几乎有一个完美的答案。
结果发现,从更高的层面来看,我试图做一些不该做的事情(请看我在这里的评论)。
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'
哪个类“拥有”foo
、bar
和baz
?
如果我想把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在后台查找的魔法方法,通常是从类而不是直接从实例开始的。但这样做的行为在你要求的情况下表现得还算不错(有个警告在我帖子前面已经说得很清楚;以这种方式查找属性不会给你与直接在实例中查找不同的结果)。
这个类应该涵盖最常见的情况:
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)
(用你示例代码中的名称。)
限制条件:
你不能通过这个代理从构造函数中传入的类获取一些特殊属性,比如
__class__
等等。(这个限制同样适用于super
。)如果你想获取的属性是某种奇怪的描述符,这可能会表现得很奇怪。
编辑:如果你希望更新后的问题中的代码能够按预期工作,可以使用以下代码:
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