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

python: super()-like proxy object that starts the MRO search at a specified class

根据文件,super(cls, obj)退货

a proxy object that delegates method calls to a parent or sibling
class of type cls

我理解为什么super()提供了这个功能,但我需要一些稍微不同的东西:我需要创建一个代理对象,它将方法调用(和属性查找)委托给类cls本身;和super一样,如果cls不实现方法/属性,我的代理应该继续按mro顺序查找(of新的而不是原来的类)。我能写什么函数来实现这个目的吗?

例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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中)只讨论了它的方法委托,并没有说明代理的属性查找也是以同样的方式执行的。这是偶然的疏忽吗?

编辑

更新的委托方法也适用于以下示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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()更接近我想要的,原因有两个:

  • super()只对第一个调用进行代理;随后的调用将正常进行,因为到那时该对象已被使用,而不是其代理。

  • super()不允许属性访问。

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

    事实证明,在更高的层次上,我试图做一些我不应该做的事情(见我在这里的评论)。


    此类应涵盖最常见的情况:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    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

    这样使用:

    1
    b = Delegate(B, c)

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

    限制:

  • 您不能从通过该代理传递给构造函数的类中检索一些特殊属性,如__class__等。(此恢复也适用于super。)

  • 如果要检索的属性是某种特殊类型的描述符,则这可能表现为异常。

  • 编辑:如果希望问题更新中的代码按需工作,可以使用以下代码:

    1
    2
    3
    4
    5
    6
    7
    8
    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参数传递给任何调用的方法,它根本不需要原始对象,因此我将其从构造函数中删除。

    如果还希望可以访问实例属性,则可以使用此版本:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    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


    A separate question, the documentation for super() (in Python 3.2)
    only talks about its method delegation, and does not clarify that
    attribute lookups for the proxy are also performed the same way. Is it
    an accidental omission?

    不,这不是偶然的。super()对属性查找没有任何作用。原因是实例上的属性与特定的类没有关联,它们就在那里。考虑以下内容:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    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的所有属性都存储在同一个位置:

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

    无法判断它们中的哪一个最初是由哪个类设置的,或者最后一个是由哪个类设置的,或者您想要的任何所有权度量。当然,没有办法让"EDOCX1 12"被EDOCX1"13"所遮蔽,正如你所期望的C++一样,它们是同一个属性,任何一个类(或来自别处)对它的写入都会破坏另一个类中的一个值。

    这样做的结果是,super()执行属性查找的方式与方法查找的方式不同;它不能执行,您编写的任何代码也不能执行。

    事实上,从运行一些实验来看,无论是super还是sven的Delegate实际上都不支持直接属性检索!

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    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)

    然后这两种方法都按预期工作:

    1
    2
    3
    4
    >>> d.foo()
    A.foo
    >>> s.foo()
    A.foo

    但是:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    >>> 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'

    因此,它们都只适用于调用某些方法,而不适用于传递给任意第三方代码来假装是要委托给的类的实例。

    不幸的是,在存在多个继承的情况下,它们的行为并不相同。鉴于:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    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)

    然后:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    >>> 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

    这是我的尝试:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    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的一步太奇怪的话。

    我没有尝试处理异常的属性;那些用描述符(包括属性)实现的属性,或者那些用Python在后台查找的神奇方法,这些方法通常从类而不是直接从实例开始。但这是你所要求的适度的行为(在我的文章的第一部分中有关于恶心的警告;用这种方式查找属性与直接在实例中查找属性相比,不会给你任何不同的结果)。