3 votes

Comment puis-je obliger les sous-classes à avoir des __lots__ ?

J'ai une classe avec __slots__ :

class A:
    __slots__ = ('foo',)

Si je crée une sous-classe sans spécifier __slots__ la sous-classe aura un __dict__ :

class B(A):
    pass

print('__dict__' in dir(B))  # True

Y a-t-il un moyen d'empêcher B d'avoir un __dict__ sans avoir à définir __slots__ = () ?

5voto

MSeifert Points 6307

La réponse de @AKX est presque correct. Je pense __prepare__ et une métaclasse est en effet le moyen de résoudre ce problème assez facilement.

Juste pour récapituler :

  • Si l'espace de nom de la classe contient un __slots__ après l'exécution du corps de la classe, alors la classe utilisera la touche __slots__ au lieu de __dict__ .
  • On peut injecter des noms dans l'espace de nom de la classe avant le corps de la classe est exécuté en utilisant __prepare__ .

Ainsi, si nous retournons simplement un dictionnaire contenant la clé '__slots__' de __prepare__ alors la classe sera (si le '__slots__' n'est pas supprimée lors de l'évaluation du corps de la classe). __slots__ au lieu de __dict__ . Parce que __prepare__ ne fournit que l'espace de nom initial, on peut facilement remplacer l'option __slots__ ou les supprimer à nouveau dans le corps de la classe.

Ainsi, une métaclasse qui fournit __slots__ par défaut ressemblerait à ceci :

class ForceSlots(type):
    @classmethod
    def __prepare__(metaclass, name, bases, **kwds):
        # calling super is not strictly necessary because
        #  type.__prepare() simply returns an empty dict.
        # But if you plan to use metaclass-mixins then this is essential!
        super_prepared = super().__prepare__(metaclass, name, bases, **kwds)
        super_prepared['__slots__'] = ()
        return super_prepared

Ainsi, chaque classe et sous-classe avec cette métaclasse aura (par défaut) une classe vide __slots__ dans leur espace de nom et ainsi créer une "classe avec des créneaux" (à l'exception de l'élément __slots__ sont volontairement supprimés).

Juste pour illustrer comment cela fonctionnerait :

class A(metaclass=ForceSlots):
    __slots__ = "a",

class B(A):  # no __dict__ even if slots are not defined explicitly
    pass

class C(A):  # no __dict__, but provides additional __slots__
    __slots__ = "c",

class D(A):  # creates normal __dict__-based class because __slots__ was removed
    del __slots__

class E(A):  # has a __dict__ because we added it to __slots__
    __slots__ = "__dict__",

Qui passe les tests mentionnés dans la réponse d'AKZ :

assert "__dict__" not in dir(A)
assert "__dict__" not in dir(B)
assert "__dict__" not in dir(C)
assert "__dict__" in dir(D)
assert "__dict__" in dir(E)

Et pour vérifier qu'il fonctionne comme prévu :

# A has slots from A: a
a = A()
a.a = 1
a.b = 1  # AttributeError: 'A' object has no attribute 'b'

# B has slots from A: a
b = B()  
b.a = 1
b.b = 1  # AttributeError: 'B' object has no attribute 'b'

# C has the slots from A and C: a and c
c = C()
c.a = 1
c.b = 1  # AttributeError: 'C' object has no attribute 'b'
c.c = 1

# D has a dict and allows any attribute name
d = D()  
d.a = 1
d.b = 1
d.c = 1

# E has a dict and allows any attribute name
e = E()  
e.a = 1
e.b = 1
e.c = 1

Comme indiqué dans un commentaire (par Aran-Fey ) il y a une différence entre del __slots__ et en ajoutant __dict__ au __slots__ :

Il y a une petite différence entre les deux options : del __slots__ donnera à votre classe non seulement une __dict__ mais aussi un __weakref__ fente.

1voto

AKX Points 14236

Que diriez-vous d'une métaclasse comme celle-ci et le site __prepare__() crochet ?

import sys

class InheritSlots(type):
    def __prepare__(name, bases, **kwds):
        # this could combine slots from bases, I guess, and walk the base hierarchy, etc
        for base in bases:
            if base.__slots__:
                kwds["__slots__"] = base.__slots__
                break
        return kwds

class A(metaclass=InheritSlots):
    __slots__ = ("foo", "bar", "quux")

class B(A):
    pass

assert A.__slots__
assert B.__slots__ == A.__slots__
assert "__dict__" not in dir(A)
assert "__dict__" not in dir(B)

print(sys.getsizeof(A()))
print(sys.getsizeof(B()))

Pour une raison quelconque, cela s'imprime toujours 64, 88 - peut-être que les instances d'une classe héritée sont toujours un peu plus lourdes que la classe de base elle-même ?

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X