python:super() - 类似于在指定类中启动MRO搜索的代理对象

时间:2012-01-12 18:12:51

标签: python python-3.x multiple-inheritance superclass super

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

  

将方法调用委托给父级或兄弟级的代理对象   类型cls

我理解为什么super()提供了这个功能,但我需要稍微不同的东西:我需要创建一个代理对象,将方法调用(和属性查找)委托给类cls本身;和super一样,如果cls没有实现方法/属性,我的代理应该继续查看MRO订单( new 而不是原始版本) 类)。我能写出什么功能可以达到这个目的吗?

示例:

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),但这依赖于知道确切的类层次结构 {M}中B跟随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 Delegatesuper()更接近我想要的内容,原因有两个:

  1. super()仅代理第一个电话;后续调用将正常进行,因为到那时使用对象,而不是其代理。

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

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

    事实证明,在更高的层面上,我试图做一些我不应该做的事情(see my comments here)。

2 个答案:

答案 0 :(得分: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. 如果您要检索的属性是某种类型的描述符,则可能会出现这种情况。

  3. 修改:如果您希望问题更新中的代码能够按预期运行,您可以使用以下代码:

    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
    

答案 1 :(得分:2)

  

一个单独的问题,super()的文档(在Python 3.2中)   只讨论其方法授权,并没有澄清   代理的属性查找也以相同的方式执行。是吗   意外遗漏?

不,这不是偶然的。 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的实例,那么在调用baz之前它是否应该具有method属性?之后怎么样?

如果我将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 ++所期望的那样,当然没有办法让“A.fooD.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。如果返回一步使用__dict__的奇怪之处,那么你可以自己检查类super来检查类{{1}}的MRO链。

我没有试图处理不寻常的属性;那些用描述符(包括属性)实现的,或者那些魔术方法在Python后面查找,这些方法通常从类而不是直接从实例开始。但是这种行为就像你问得适中一样(在我帖子的第一部分中就广告恶意阐述了警告;以这种方式查找属性不会给你任何不同的结果,而不是直接在实例中查找它们。)