Pourquoi ne puis-je pas modifier l'attribut __class__ d'une instance d'objet?

10
class A(object):
    pass

class B(A):
    pass

o = object()
a = A()
b = B()

Bien que je puisse changer a.__class__, je ne peux pas faire de même avec o.__class__(cela génère une TypeErrorerreur). Pourquoi?

Par exemple:

isinstance(a, A) # True
isinstance(a, B) # False
a.__class__ = B
isinstance(a, A) # True
isinstance(a, B) # True

isinstance(o, object) # True
isinstance(o, A) # False
o.__class__ = A # This fails and throws a TypeError
# isinstance(o, object)
# isinstance(o, A)

Je sais que ce n'est généralement pas une bonne idée, car cela peut conduire à un comportement très étrange s'il est mal géré. C'est juste pour la curiosité.

Riccardo Bucco
la source
3
Les types intégrés sacrifient le dynamisme d'un type défini par l'utilisateur pour des raisons d'efficacité. Remarque, une autre optimisation facultative est les emplacements, ce qui empêchera également cela.
juanpa.arrivillaga

Réponses:

6

CPython a un commentaire dans Objects / typeobject.c sur ce sujet:

Dans les versions de CPython antérieures à 3.5, le code dans compatible_for_assignmentn'était pas configuré pour vérifier correctement la compatibilité de la disposition de la mémoire / de l'emplacement / etc. pour les classes non HEAPTYPE, nous avons donc simplement interdit l' __class__affectation dans tous les cas qui n'étaient pas HEAPTYPE -> HEAPTYPE.

Au cours du cycle de développement 3.5, nous avons corrigé le code compatible_for_assignmentpour vérifier correctement la compatibilité entre les types arbitraires et commencé à autoriser l' __class__attribution dans tous les cas où les anciens et les nouveaux types avaient en fait des emplacements et une disposition de mémoire compatibles (qu'ils soient ou non implémentés comme HEAPTYPE) ou pas).

Cependant, juste avant la sortie de la version 3.5, nous avons découvert que cela entraînait des problèmes avec des types immuables comme int, où l'interpréteur suppose qu'ils sont immuables et interne certaines valeurs. Auparavant, ce n'était pas un problème, car ils étaient vraiment immuables - en particulier, tous les types où l'interprète appliquait cette astuce d'internement se trouvaient également être alloués statiquement, de sorte que les anciennes règles HEAPTYPE les empêchaient "accidentellement" d'autoriser l' __class__affectation. Mais avec les changements d' __class__affectation, nous avons commencé à autoriser du code comme

class MyInt(int):
#   ...
# Modifies the type of *all* instances of 1 in the whole program,
# including future instances (!), because the 1 object is interned.
 (1).__class__ = MyInt

(voir https://bugs.python.org/issue24912 ).

En théorie, la solution appropriée consisterait à identifier les classes qui s'appuient sur cet invariant et à interdire en quelque sorte l' __class__affectation uniquement pour elles, peut-être via un mécanisme comme un nouveau drapeau Py_TPFLAGS_IMMUTABLE (une approche de "liste noire"). Mais en pratique, puisque ce problème n'a pas été remarqué à la fin du cycle 3.5 RC, nous adoptons l'approche conservatrice et rétablissons le même contrôle HEAPTYPE-> HEAPTYPE que nous avions, plus une "liste blanche". Pour l'instant, la liste blanche se compose uniquement de sous-types ModuleType, car ce sont les cas qui ont motivé le correctif en premier lieu - voir https://bugs.python.org/issue22986 - et puisque les objets de module sont mutables, nous pouvons être sûrs qu'ils ne sont définitivement pas internés. Alors maintenant, nous autorisons HEAPTYPE-> HEAPTYPE ou Sous-type ModuleType -> Sous-type ModuleType.

Pour autant que nous le sachions, tout le code au-delà de l'instruction «if» suivante gérera correctement les classes non HEAPTYPE, et la vérification HEAPTYPE est nécessaire uniquement pour protéger ce sous-ensemble de classes non HEAPTYPE pour lequel l'interpréteur a cuit en supposant que toutes les instances sont vraiment immuables.

Explication:

CPython stocke les objets de deux manières:

Les objets sont des structures allouées sur le tas. Des règles spéciales s'appliquent à l'utilisation des objets pour garantir qu'ils sont correctement récupérés. Les objets ne sont jamais alloués statiquement ou sur la pile; ils doivent être accessibles uniquement via des macros et fonctions spéciales. (Les objets type sont des exceptions à la première règle; les types standard sont représentés par des objets type initialisés statiquement, bien que les travaux sur l'unification de type / classe pour Python 2.2 aient également permis d'avoir des objets type alloués par segment).

Informations du commentaire dans Include / object.h .

Lorsque vous essayez de définir une nouvelle valeur sur some_obj.__class__, la object_set_classfonction est appelée. Il est hérité de PyBaseObject_Type , voir /* tp_getset */champ. Cette fonction vérifie : le nouveau type peut-il remplacer l'ancien type dans some_obj?

Prenez votre exemple:

class A:
    pass

class B:
    pass

o = object()
a = A() 
b = B() 

Premier cas:

a.__class__ = B 

Le type d' aobjet est Ale type de segment, car il est alloué dynamiquement. Ainsi que le B. Le atype de est modifié sans problème.

Deuxième cas:

o.__class__ = B

Le type de oest le type intégré object( PyBaseObject_Type). Ce n'est pas du type tas, donc le TypeErrorest levé:

TypeError: __class__ assignment only supported for heap types or ModuleType subclasses.
MiniMax
la source
4

Vous ne pouvez changer __class__à un autre type qui a la même interne (C) mise en page . Le runtime ne connaît même pas cette disposition à moins que le type lui-même ne soit alloué dynamiquement (un «type de segment»), c'est donc une condition nécessaire qui exclut les types intégrés en tant que source ou destination. Vous devez également avoir le même ensemble de __slots__avec les mêmes noms.

Davis Herring
la source