¿No se puede heredar de varias clases que definen __slots__?

Una cierta situación en Python me alarmó recientemente, y su razón aún no está completamente clara después de un poco de investigación. Las siguientes definiciones de clase parecen funcionar perfectamente y producirán lo que se pretende:

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

Estas son cuatro clases dispuestas en un patrón de herencia de diamante. Sin embargo, un patrón algo similar no está permitido. Las siguientes definiciones de clase parecen que deberían funcionar igual que la primera:

 class B: __slots__ = 'a', 'b' class C: __slots__ = 'a', 'b' class D(B, C): __slots__ = () Traceback (most recent call last): File "", line 1, in  class D(B, C): __slots__ = () TypeError: multiple bases have instance lay-out conflict 

Sin embargo, un TypeError se plantea en este ejemplo. Entonces surgen tres preguntas: (1) ¿Se trata de un error en Python, considerando los nombres de las ranuras? (2) ¿Qué justificaría tal respuesta? (3) ¿Cuál es la mejor solución?


Referencias:

  • __slots__ y herencia múltiple (con respuestas a continuación)
  • Herencia múltiple y __slots__
  • No uses __slots__
  • Herencia múltiple __slots__ problema

    ¿No se puede heredar de varias clases que definen __slots__ ?

    Cerrar.

    No puede heredar de varias clases que definen __slots__ no __slots__ cuando hay un conflicto de diseño.

    Las ranuras tienen un diseño ordenado, y los descriptores que se crean en la clase se basan en esas posiciones, por lo tanto, no deben tener un conflicto de diseño en herencia múltiple.

    Su enfoque más simple falla porque cada a y b se consideran ranuras diferentes, y el algoritmo de diseño no comprueba si son semánticamente iguales:

     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__ = () # Ba or Ca comes first? 

    Su primer ejemplo funciona porque la herencia múltiple solo obtiene las ranuras de A, por lo tanto, todos los casos utilizan los descriptores y las posiciones / diseño de A Por ejemplo, se permitiría lo siguiente:

     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' 

    Creando D, y usando esas ranuras:

     d = D() da = db = dc = dd = de = 'foo' 

    Y no podemos crear dinámicamente variables:

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

    El enfoque anterior es un método para resolver su código problemático, pero podría requerir un poco de reescritura. Si decide que B necesita otra ranura, debe refactorizar la funcionalidad de B en una abstracción para obtener la reutilización del código para D (lo cual está bien, pero potencialmente confuso).

    Es una buena práctica usar abstracciones, y otra solución sería hacer esto, donde las clases abstractas y / o los mixins contienen la funcionalidad para sus clases concretas:

     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' 

    Su primer ejemplo es bastante práctico porque evita un conflicto de diseño, esto simplemente vuelve a imaginar una solución utilizando abstracciones en lugar de concreciones.

    Preguntas finales:

    (1) ¿Es esto un error en Python, considerando los nombres de las ranuras?

    No, a pesar de mucha confusión al respecto, está algo documentado y los errores intentan aclarar este comportamiento.

    (2) ¿Qué justificaría tal respuesta?

    Las clases que definen ranuras obtienen descriptores que saben dónde se ubican sus datos en una posición. Si los diseños cambian, los descriptores serían incorrectos.

    ¿Podría cada subclase crear su propio diseño y sus propios descriptores? Supongo que podría, pero eso requeriría un poco de reescritura de cómo funcionan, y un poco de voluntad política para hacerlo, y posiblemente podría romper a otros usuarios que están hurgando en la API y confiando en el comportamiento actual.

    (3) ¿Cuál es la mejor solución?

    Definir “mejor”.

    ¿Es más rápido de escribir y posiblemente menos complejo? Simplemente evite los conflictos de diseño como en su primer ejemplo.

    ¿Mejores prácticas ?: use árboles de herencia abstractos y defina espacios en sus concreciones. Si bien puede haber más clases con este enfoque, podría decirse que es menos complejo para los demás y para el “futuro” usted.

    Al forzar una restricción que ninguna clase define __slots__, se podría construir una clase de objeto especial con las características deseadas para todas las clases secundarias. La clase se registra como un alias para objetos regulares.

     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 

    Este código se puede usar como un bloque de construcción para hacer que tkinter sea tkinter hilos clonando sus clases. La clase de Affinity asegura automáticamente que el código se ejecute en un solo hilo, evitando errores de GUI.

    Me enfrenté a este error y realmente quería usar ranuras para mis nodos de base de datos personalizados. Aquí está el conjunto de pruebas que he hecho (en 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) 

    Aquí están los resultados:

     DEBUG:root:******************** new class test ******************** ERROR:root:BOOM!!! c1:  c2:  __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:  c2:  __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:  c2:  __slots__: ('attr4',) DEBUG:root:Instance __slots__:('attr4',) DEBUG:root:Attributes in instance dir: XXXX DEBUG:root:******************** new class test ******************** ERROR:root:BOOM!!! c1:  c2:  __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:  c2:  __slots__: ('attr2', 'attr4') DEBUG:root:Instance __slots__:('attr2', 'attr4') DEBUG:root:Attributes in instance dir: XXXX DEBUG:root:******************** new class test ******************** DEBUG:root:No Boom! c1:  c2:  __slots__: ('attr2', 'attr4') DEBUG:root:Instance __slots__:('attr2', 'attr4') DEBUG:root:Attributes in instance dir: XX _ X DEBUG:root:******************** new class test ******************** DEBUG:root:No Boom! c1:  c2:  __slots__: ('attr1', 'attr2') DEBUG:root:Instance __slots__:('attr1', 'attr2') DEBUG:root:Attributes in instance dir: XX _ _ DEBUG:root:******************** new class test ******************** DEBUG:root:No Boom! c1:  c2:  __slots__: () DEBUG:root:Instance __slots__:() DEBUG:root:Attributes in instance dir: XX _ _ DEBUG:root:******************** new class test ******************** DEBUG:root:No Boom! c1:  c2:  __slots__: ('attr2', 'attr4') DEBUG:root:Instance __slots__:('attr2', 'attr4') DEBUG:root:Attributes in instance dir: XXXX DEBUG:root:******************** new class test ******************** DEBUG:root:No Boom! c1:  c2:  __slots__: ('attr3',) DEBUG:root:Instance __slots__:('attr3',) DEBUG:root:Attributes in instance dir: XXX _ DEBUG:root:******************** new class test ******************** ERROR:root:BOOM!!! c1:  c2:  __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:  c2:  __slots__: not defined WARNING:root:Instance has __dict__! DEBUG:root:Attributes in instance dir: XX _ _ 

    Como puedes ver tienes dos opciones:

    1. O bien defina __slots__ = () para todas las clases padre excepto una,
    2. o hacer uno de los padres a subclase del otro.

    Tenga en cuenta que también debe definir __slots__ en la nueva clase, de lo contrario obtiene un __dict__ .

    ¿Has visto esta alternativa? https://stackoverflow.com/a/53063670/1400467

    Hay una solución “complicada” mediante el uso de metaclases y un atributo falso _slots_ . Esto funciona en Python 3.6 y espero en Python 3.X así.