不能继承多个定义了__slots__的类?

25
最近Python中的某种情况引起了我的注意,经过一些调查后,它的原因仍然不完全清楚。下面的类定义似乎运行得非常顺利,会产生预期的结果:
class A: __slots__ = 'a', 'b'
class B(A): __slots__ = ()
class C(A): __slots__ = ()
class D(B, C): __slots__ = ()

这是一个以钻石继承模式排列的四个类。然而,一个有点相似的模式是不允许的。以下类定义看起来应该与第一个函数相同:

class B: __slots__ = 'a', 'b'
class C: __slots__ = 'a', 'b'
class D(B, C): __slots__ = ()

Traceback (most recent call last):
  File "<pyshell#74>", line 1, in <module>
    class D(B, C): __slots__ = ()
TypeError: multiple bases have instance lay-out conflict

然而,在这个例子中会引发一个TypeError。因此,有三个问题产生:(1)考虑到槽名称,这是Python中的一个错误吗?(2)什么可以证明这样的答案?(3)最好的解决方法是什么?


参考资料:

4个回答

14

无法从定义了__slots__的多个类中继承?

不对。

当存在布局冲突时,您不能从定义了非空__slots__的多个类中继承。

Slots具有有序布局,创建在类中的描述符依赖于这些位置,因此在多重继承下不能有布局冲突。

您最简单的方法失败是因为每个ab都被视为不同的slots,并且布局算法不会检查它们是否在语义上相同:

class B: __slots__ = 'a', 'b' # creates descriptors in B for a, b
class C: __slots__ = 'a', 'b' # creates new, different descriptors in C
class D(B, C): __slots__ = () # B.a or C.a comes first?
你的第一个示例有效是因为多继承只获取 A 的插槽,因此所有情况都使用 A 的描述符和位置/布局。例如,以下内容是允许的:
class A: __slots__ = 'a', 'b' # shared parent, ok

class B(A): __slots__ = () # B or C must be empty

class C(A): __slots__ = 'c', # Since C is nonempty, B must be empty to inherit from both

class D(B, C): __slots__ = 'd', 'e'

实例化 D,并使用那些插槽:

d = D()
d.a = d.b = d.c = d.d = d.e = 'foo'

我们无法动态创建变量:

>>> d.f = 'foo'
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'D' object has no attribute 'f'

以上是解决您问题代码的一种方法,但它可能需要重写 - 如果您决定B需要另一个插槽,则必须将B的功能重构为抽象以实现D的代码重用(这很好,但可能会令人困惑)。

使用抽象是最佳实践,另一种解决方案是这样做,其中抽象类和/或mixin包含了您的具体类的功能:

class AbstractB: __slots__ = ()

class B(AbstractB): __slots__ = 'a', 'b'

class AbstractC: __slots__ = ()

class C(AbstractC): __slots__ = 'a', 'b'

class Mixin: __slots__ = ()

class D(AbstractB, AbstractC, Mixin): __slots__ = 'a', 'b'

你的第一个例子非常可行,因为它避免了布局冲突,这只是重新使用抽象来解决问题而不是具体实现。

最终问题:

(1) 考虑到槽名字,这是 Python 中的 bug 吗?

不是,尽管有很多混淆的地方,但它在某种程度上已经被记录下来,错误信息也试图让这种行为变得清晰明了。

(2) 什么能证明这样的答案是正确的?

定义槽的类会获得描述符,知道它们的数据以位置形式存储。如果布局更改,描述符将是错误的。

每个子类都可以创建自己的布局和自己的描述符吗?我想可能可以,但那需要重写它们的工作方式,并且需要一些政治意愿去做,而且有可能破坏其他在 C api 中探索并依赖当前行为的用户。

(3) 最佳解决方法是什么?

如何定义“最佳”?

最快编写且可能最少复杂的方法:避免像第一个例子中那样的布局冲突。

最佳实践:使用抽象继承树(一组定义语义但不打算被实例化的类),并在具体的实现中定义槽。虽然可能会有更多的类,但对于其他人和“未来的你”来说,这种方法可能会更简单。


请问您能解释一下“抽象继承树”是什么吗? - ניר
1
我现在已经在文本中用括号解释了它。 - Russia Must Remove Putin

1

通过强制约束,使得没有类定义 __slots__,可以构建一个特殊的对象类,具有所有子类所需的特性。该类被注册为常规对象的别名。

class _object: __slots__ = '_MetaSafe__exec', '__dict__'

class MetaSafe(type):

    __REGISTRY = {object: _object}

    @classmethod
    def clone(cls, old):
        return cls(old.__name__, old.__bases__, dict(old.__dict__), old)

    def __new__(cls, name, bases, classdict, old=None):
        # Check on a few classdict keys.
        assert '__new__' not in classdict, '__new__ must not be defined!'
        assert '__slots__' not in classdict, '__slots__ must not be defined!'
        assert '__module__' in classdict, '__module__ must be defined!'
        # Validate all the parent classes.
        valid = []
        for base in bases:
            if base in cls.__REGISTRY:
                valid.append(cls.__REGISTRY[base])
            elif base in cls.__REGISTRY.values():
                valid.append(base)
            else:
                valid.append(cls.clone(base))
        # Wrap callables without thread mark.
        for key, value in classdict.items():
            if callable(value):
                classdict[key] = cls.__wrap(value)
        # Fix classdict and create new class.
        classdict.update({'__new__': cls.__new, '__slots__': (), '__module__':
                          '{}.{}'.format(__name__, classdict['__module__'])})
        cls.__REGISTRY[old] = new = \
            super().__new__(cls, name, tuple(valid), classdict)
        return new

    def __init__(self, name, bases, classdict, old=None):
        return super().__init__(name, bases, classdict)

    @staticmethod
    def __wrap(func):
        @functools.wraps(func)
        def safe(self, *args, **kwargs):
            return self.__exec(func, self, *args, **kwargs)
        return safe

    @classmethod
    def __new(meta, cls, *args, **kwargs):
        self = object.__new__(cls, *args, **kwargs)
        if 'master' in kwargs:
            self.__exec = kwargs['master'].__exec
        else:
            array = tuple(meta.__REGISTRY.values())
            for value in args:
                if isinstance(value, array):
                    self.__exec = value.__exec
                    break
            else:
                self.__exec = Affinity()
        return self

这段代码可以作为构建块,通过克隆其类使tkinter线程安全。 Affinity类自动确保代码在单个线程上执行,防止GUI错误。


-1

我曾经遇到过这个错误,而我真的很想为我的自定义数据库节点使用插槽。这是我制作的测试套件(它是在Python 3.x中编写的):

import logging

A = None, 'attr1', 'attr2', 'attr3', 'attr4'

class C12(object):
    __slots__ = (A[1], A[2])

class C1234(object):
    __slots__ = (A[1], A[2], A[3], A[4])

class C34(object):
    __slots__ = (A[3], A[4])

class C3byC12(C12):
    __slots__ = (A[3])

class CEmpty(object):
    __slots__ = ()

MSG_FRM = '\n\tc1: {}\n\tc2: {}\n\t__slots__: {}'
NOT_DEF = 'not defined'

def test(c1, c2, slots):
    logging.debug('*'*20 + ' new class test ' + '*'*20)
    msg = MSG_FRM.format(c1, c2, slots)
    try:
        if slots == NOT_DEF:
            class TestClass(c1, c2): pass
        else:        
            class TestClass(c1, c2):
                __slots__ = slots
    except TypeError:
        logging.exception('BOOM!!! ' + msg)
    else:
        logging.debug('No Boom! ' + msg)
        instance = TestClass()
        if '__dict__' in dir(instance):
            logging.warning('Instance has __dict__!')
        else:
            logging.debug('Instance __slots__:{}'.format(
                          instance.__slots__))
        logging.debug('Attributes in instance dir: {}'.format(
            ' '.join(['X' if (a in dir(instance)) else '_'
                     for a in A[1:]])))

if __name__ == '__main__':
    logging.basicConfig(level=logging.DEBUG)
    test(C12, C34, (A[2], A[4]))
    test(C12, C3byC12, (A[2],))
    test(C3byC12, C12, (A[4],))
    test(C1234, C34, (A[2], A[4]))
    test(C1234, CEmpty, (A[2], A[4]))
    test(C12, CEmpty, (A[2], A[4]))
    test(C12, CEmpty, (A[1], A[2]))
    test(C12, CEmpty, ())
    test(CEmpty, C1234, (A[2], A[4]))
    test(CEmpty, C12, (A[3],))
    test(C12, C34, NOT_DEF)
    test(C12, CEmpty, NOT_DEF)

以下是结果:

DEBUG:root:******************** new class test ********************
ERROR:root:BOOM!!!
        c1: <class '__main__.C12'>
        c2: <class '__main__.C34'>
        __slots__: ('attr2', 'attr4')
Traceback (most recent call last):
  File "boom.py", line 30, in test
    class TestClass(c1, c2):
TypeError: multiple bases have instance lay-out conflict
DEBUG:root:******************** new class test ********************
ERROR:root:BOOM!!!
        c1: <class '__main__.C12'>
        c2: <class '__main__.C3byC12'>
        __slots__: ('attr2',)
Traceback (most recent call last):
  File "boom.py", line 30, in test
    class TestClass(c1, c2):
TypeError: Cannot create a consistent method resolution
order (MRO) for bases C3byC12, C12
DEBUG:root:******************** new class test ********************
DEBUG:root:No Boom!
        c1: <class '__main__.C3byC12'>
        c2: <class '__main__.C12'>
        __slots__: ('attr4',)
DEBUG:root:Instance __slots__:('attr4',)
DEBUG:root:Attributes in instance dir: X X X X
DEBUG:root:******************** new class test ********************
ERROR:root:BOOM!!!
        c1: <class '__main__.C1234'>
        c2: <class '__main__.C34'>
        __slots__: ('attr2', 'attr4')
Traceback (most recent call last):
  File "boom.py", line 30, in test
    class TestClass(c1, c2):
TypeError: multiple bases have instance lay-out conflict
DEBUG:root:******************** new class test ********************
DEBUG:root:No Boom!
        c1: <class '__main__.C1234'>
        c2: <class '__main__.CEmpty'>
        __slots__: ('attr2', 'attr4')
DEBUG:root:Instance __slots__:('attr2', 'attr4')
DEBUG:root:Attributes in instance dir: X X X X
DEBUG:root:******************** new class test ********************
DEBUG:root:No Boom!
        c1: <class '__main__.C12'>
        c2: <class '__main__.CEmpty'>
        __slots__: ('attr2', 'attr4')
DEBUG:root:Instance __slots__:('attr2', 'attr4')
DEBUG:root:Attributes in instance dir: X X _ X
DEBUG:root:******************** new class test ********************
DEBUG:root:No Boom!
        c1: <class '__main__.C12'>
        c2: <class '__main__.CEmpty'>
        __slots__: ('attr1', 'attr2')
DEBUG:root:Instance __slots__:('attr1', 'attr2')
DEBUG:root:Attributes in instance dir: X X _ _
DEBUG:root:******************** new class test ********************
DEBUG:root:No Boom!
        c1: <class '__main__.C12'>
        c2: <class '__main__.CEmpty'>
        __slots__: ()
DEBUG:root:Instance __slots__:()
DEBUG:root:Attributes in instance dir: X X _ _
DEBUG:root:******************** new class test ********************
DEBUG:root:No Boom!
        c1: <class '__main__.CEmpty'>
        c2: <class '__main__.C1234'>
        __slots__: ('attr2', 'attr4')
DEBUG:root:Instance __slots__:('attr2', 'attr4')
DEBUG:root:Attributes in instance dir: X X X X
DEBUG:root:******************** new class test ********************
DEBUG:root:No Boom!
        c1: <class '__main__.CEmpty'>
        c2: <class '__main__.C12'>
        __slots__: ('attr3',)
DEBUG:root:Instance __slots__:('attr3',)
DEBUG:root:Attributes in instance dir: X X X _
DEBUG:root:******************** new class test ********************
ERROR:root:BOOM!!!
        c1: <class '__main__.C12'>
        c2: <class '__main__.C34'>
        __slots__: not defined
Traceback (most recent call last):
  File "boom.py", line 28, in test
    class TestClass(c1, c2): pass
TypeError: multiple bases have instance lay-out conflict
DEBUG:root:******************** new class test ********************
DEBUG:root:No Boom!
        c1: <class '__main__.C12'>
        c2: <class '__main__.CEmpty'>
        __slots__: not defined
WARNING:root:Instance has __dict__!
DEBUG:root:Attributes in instance dir: X X _ _

如您所见,您有两个选项:

  1. 为除一个父类外的所有父类定义__slots__ = ()
  2. 或使其中一个父类成为另一个父类的子类。

请注意,在新类中也应定义__slots__,否则它将获得一个__dict__


-2
你看过这个替代方案吗? https://dev59.com/Jq_la4cB1Zd3GeqPtHXB#53063670 通过使用元类和一个假的_slots_属性,有一个“棘手”的解决方法。 这在Python 3.6中有效,并希望在Python 3.X中也是如此。

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