¿Por qué no puedo cambiar el atributo __class__ de una instancia de objeto?

10
class A(object):
    pass

class B(A):
    pass

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

Si bien puedo cambiar a.__class__, no puedo hacer lo mismo con o.__class__(arroja un TypeErrorerror). ¿Por qué?

Por ejemplo:

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)

Sé que esto generalmente no es una buena idea, ya que puede conducir a un comportamiento muy extraño si se maneja incorrectamente. Es solo por curiosidad.

Riccardo Bucco
fuente
3
Los tipos incorporados sacrifican el dinamismo de un tipo definido por el usuario por razones de eficiencia. Tenga en cuenta que otra optimización opcional son las ranuras, que también evitarán esto.
juanpa.arrivillaga

Respuestas:

6

CPython tiene un comentario en Objects / typeobject.c sobre este tema:

En las versiones de CPython anteriores a 3.5, el código compatible_for_assignmentno estaba configurado para verificar correctamente la compatibilidad del diseño de memoria / ranura / etc. para las clases que no son HEAPTYPE, por lo que simplemente rechazamos la __class__asignación en cualquier caso que no fuera HEAPTYPE -> HEAPTYPE.

Durante el ciclo de desarrollo 3.5, arreglamos el código compatible_for_assignmentpara verificar correctamente la compatibilidad entre tipos arbitrarios, y comenzamos a permitir la __class__asignación en todos los casos en que los tipos antiguos y nuevos tenían ranuras y diseño de memoria compatibles (independientemente de si se implementaron como HEAPTYPE) o no).

Sin embargo, justo antes de que se lanzara 3.5, descubrimos que esto condujo a problemas con tipos inmutables como int, donde el intérprete asume que son inmutables e interna algunos valores. Anteriormente, esto no era un problema, porque realmente eran inmutables, en particular, todos los tipos en los que el intérprete aplicaba este truco interno también estaban asignados estáticamente, por lo que las viejas reglas HEAPTYPE les impedían "accidentalmente" __class__asignarlos. Pero con los cambios en la __class__asignación, comenzamos a permitir códigos como

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

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

En teoría, la solución adecuada sería identificar qué clases dependen de esta invariante y de alguna manera rechazar la __class__asignación solo para ellas, tal vez a través de algún mecanismo como un nuevo indicador Py_TPFLAGS_IMMUTABLE (un enfoque de "lista negra"). Pero en la práctica, dado que este problema no se notó al final del ciclo 3.5 RC, estamos adoptando el enfoque conservador y restableciendo la misma verificación HEAPTYPE-> HEAPTYPE que teníamos, más una "lista blanca". Por ahora, la lista blanca consta solo de subtipos de ModuleType, ya que esos son los casos que motivaron el parche en primer lugar, consulte https://bugs.python.org/issue22986 , y dado que los objetos del módulo son mutables, podemos estar seguros que definitivamente no están siendo internados. Entonces ahora permitimos HEAPTYPE-> HEAPTYPE o Subtipo ModuleType -> Subtipo ModuleType.

Hasta donde sabemos, todo el código más allá de la siguiente instrucción 'if' manejará correctamente las clases que no son HEAPTYPE, y la verificación HEAPTYPE es necesaria solo para proteger ese subconjunto de clases que no son HEAPTYPE para las cuales el intérprete ha asumido que Todas las instancias son verdaderamente inmutables.

Explicación:

CPython almacena objetos de dos maneras:

Los objetos son estructuras asignadas en el montón. Se aplican reglas especiales al uso de objetos para garantizar que se recojan correctamente. Los objetos nunca se asignan estáticamente o en la pila; se debe acceder a través de macros especiales y funciones solamente. (Los objetos de tipo son excepciones a la primera regla; los tipos estándar están representados por objetos de tipo inicializados estáticamente, aunque el trabajo en la unificación de tipo / clase para Python 2.2 también hizo posible tener objetos de tipo asignados en el montón).

Información del comentario en Incluir / objeto.h .

Cuando intenta establecer un nuevo valor some_obj.__class__, object_set_classse llama a la función. Se hereda de PyBaseObject_Type , ver /* tp_getset */campo. Esta función comprueba : ¿puede el nuevo tipo reemplazar al antiguo some_obj?

Toma tu ejemplo:

class A:
    pass

class B:
    pass

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

Primer caso:

a.__class__ = B 

El tipo de aobjeto es A, el tipo de montón, porque se asigna dinámicamente. Así como el B. El atipo de 's se cambia sin problema.

Segundo caso:

o.__class__ = B

El tipo de oes el tipo incorporado object( PyBaseObject_Type). No es de tipo montón, por lo que TypeErrorse genera:

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

Solo puede cambiar __class__a otro tipo que tenga el mismo diseño interno (C) . El tiempo de ejecución ni siquiera conoce ese diseño a menos que el tipo en sí se asigne dinámicamente (un "tipo de montón"), por lo que es una condición necesaria que excluye los tipos integrados como origen o destino. También debe tener el mismo conjunto de __slots__con los mismos nombres.

Arenque Davis
fuente