【问题标题】:Cannot inherit from multiple classes defining __slots__?不能从定义 __slots__ 的多个类继承?
【发布时间】:2012-06-07 02:55:34
【问题描述】:

最近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) 最好的解决方法是什么?


参考资料:

【问题讨论】:

    标签: python class slots


    【解决方案1】:

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

    关闭。

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

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

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

    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 中的错误吗?

    不,尽管在这件事上存在很多混淆,但它在一定程度上被记录在案,并且错误试图使这种行为变得清晰。

    (2) 什么可以证明这样的答案?

    定义槽的类获取知道其数据位置的描述符。如果布局发生变化,描述符就会出错。

    每个子类可以创建自己的布局和自己的描述符吗?我想它可以,但这需要对它们的工作方式进行一些重写,并且需要一些政治意愿来做到这一点,并且可以想象会破坏在 C api 中四处寻找并依赖当前行为的其他用户。

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

    定义“最佳”。

    编写速度最快且可能最不复杂?:只需避免像第一个示例中那样的布局冲突。

    最佳实践?:使用抽象继承树,并在具体实例中定义槽。虽然使用这种方法可能有更多类,但对于其他人和“未来的你”来说,它可能不那么复杂。

    【讨论】:

      【解决方案2】:

      你见过这种替代品吗? https://stackoverflow.com/a/53063670/1400467

      使用元类和伪造的_slots_ 属性有一个“棘手”的解决方法。 这适用于 Python 3.6,希望适用于 Python 3.X。

      【讨论】:

        【解决方案3】:

        我遇到了这个错误,我真的很想为我的自定义数据库节点使用插槽。这是我制作的测试套件(在 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__

        【讨论】:

          【解决方案4】:

          通过强制没有类定义 __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 错误。

          【讨论】:

            猜你喜欢
            • 2015-09-12
            • 2015-06-20
            • 1970-01-01
            • 2013-05-19
            • 1970-01-01
            • 2021-09-12
            • 2019-08-26
            • 1970-01-01
            相关资源
            最近更新 更多