Подтвердить что ты не робот

Не удается наследовать от нескольких классов, определяющих __slots__?

Определенная ситуация в 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) Какой лучший обходной путь?


Рекомендации:

4b9b3361

Ответ 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 автоматически гарантирует, что код выполняется в одном потоке, предотвращая ошибки графического интерфейса.

Ответ 2

Не может наследовать от нескольких классов, определяющих __slots__?

Близко.

Вы не можете наследовать от нескольких классов, определяющих непустые __slots__ когда есть конфликт макета.

Слоты имеют упорядоченный макет, и дескрипторы, создаваемые в классе, полагаются на эти позиции, поэтому они не должны иметь конфликт макетов при множественном наследовании.

Ваш самый простой подход терпит неудачу, потому что каждый a и b рассматриваются как разные слоты, и алгоритм компоновки не проверяет, являются ли они семантически одинаковыми:

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 (что нормально, но потенциально сбивает с толку).

Рекомендуется использовать абстракции, и другим решением будет сделать это, когда абстрактные классы и/или миксины содержат функциональность для ваших конкретных классов:

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) Какой лучший обходной путь?

Определите "лучшее".

Быстрее всего писать и, возможно, наименее сложно? Просто избегайте конфликтов макета, как в первом примере.

Лучшие практики?: Используйте абстрактные деревья наследования и определяйте слоты в своих конкрециях. Хотя с таким подходом может быть больше классов, он, возможно, будет менее сложным для других и для будущего.

Ответ 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 _ _

Как вы видите, у вас есть два варианта:

  • Определите __slots__ = () для всех, кроме одного из родительских классов,
  • или сделать одного из родителей подклассом другого.

Обратите внимание, что вы также должны определить __slots__ в новом классе, иначе он получит __dict__.

Ответ 4

Вы видели эту альтернативу? fooobar.com/questions/16648745/...

Существует хитрый обходной путь с использованием метаклассов и поддельного атрибута _slots_. Это работает в Python 3.6 и, надеюсь, в Python 3.X так.

Ответ 5

class superSlots:
      @property
      def __slots__(self):return self.MY_SLOTS
class A(superSlots):
      MY_SLOTS = "A","B"
class B(superSlots):
      MY_SLOTS = "A","B"
class C(A,B):
      MY_SLOTS = "X","Y"

может быть?? не положительный, что было бы лучшим методом, но я думаю, что он будет работать нормально