Reclasificando una instancia en Python

Tengo una clase que me proporciona una biblioteca externa. He creado una subclase de esta clase. También tengo una instancia de la clase original.

Ahora quiero convertir esta instancia en una instancia de mi subclase sin cambiar ninguna de las propiedades que ya tiene la instancia (excepto las que mi subclase invalida de todos modos).

La siguiente solución parece funcionar.

# This class comes from an external library. I don't (want) to control # it, and I want to be open to changes that get made to the class # by the library provider. class Programmer(object): def __init__(self,name): self._name = name def greet(self): print "Hi, my name is %s." % self._name def hard_work(self): print "The garbage collector will take care of everything." # This is my subclass. class C_Programmer(Programmer): def __init__(self, *args, **kwargs): super(C_Programmer,self).__init__(*args, **kwargs) self.learn_C() def learn_C(self): self._knowledge = ["malloc","free","pointer arithmetic","curly braces"] def hard_work(self): print "I'll have to remember " + " and ".join(self._knowledge) + "." # The questionable thing: Reclassing a programmer. @classmethod def teach_C(cls, programmer): programmer.__class__ = cls # Hi, my name is Joel. #>I'll have to remember malloc and free and pointer arithmetic and curly braces. jeff = Programmer("Jeff") # We (or someone else) makes changes to the instance. The reclassing shouldn't # overwrite these. jeff._name = "Jeff A" jeff.greet() jeff.hard_work() #>Hi, my name is Jeff A. #>The garbage collector will take care of everything. # Let magic happen. C_Programmer.teach_C(jeff) jeff.greet() jeff.hard_work() #>Hi, my name is Jeff A. #>I'll have to remember malloc and free and pointer arithmetic and curly braces. 

Sin embargo, no estoy convencido de que esta solución no contenga ninguna advertencia que no haya pensado (perdón por la triple negación), especialmente porque la reasignación de la __class__ simplemente no se siente bien. Incluso si esto funciona, no puedo evitar la sensación de que debería haber una forma más pythonica de hacer esto.

¿Esta ahí?


Edit: Gracias a todos por sus respuestas. Esto es lo que obtengo de ellos:

  • Si bien la idea de reclasificar una instancia asignándola a __class__ no es un lenguaje muy utilizado, la mayoría de las respuestas (4 de 6 al momento de escribir este artículo) lo consideran un enfoque válido. Una respuesta (por ojrac) dice que es “bastante extraño a primera vista”, con lo que estoy de acuerdo (fue la razón para hacer la pregunta). Solo una respuesta (por Jason Baker; con dos comentarios y votos positivos) me desanimó activamente a hacer esto, sin embargo, basándome en el ejemplo de uso más que en la técnica en general.

  • Ninguna de las respuestas, ya sea positiva o no, encuentra un problema técnico real en este método. Una pequeña excepción es jls que menciona tener cuidado con las clases de estilo antiguo, lo que probablemente sea cierto, y las extensiones C. Supongo que las extensiones de C con reconocimiento de clase de estilo nuevo deberían ser tan buenas con este método como el propio Python (suponiendo que el último sea cierto), aunque si no está de acuerdo, siga respondiendo.

En cuanto a la cuestión de cómo es pythonic esto, hubo algunas respuestas positivas, pero no se dieron razones reales. Mirando el Zen ( import this ), supongo que la regla más importante en este caso es “Explícito es mejor que implícito”. No estoy seguro, sin embargo, si esa regla habla a favor o en contra de reclasificación de esta manera.

  • Usar {has,get,set}attr parece más explícito, ya que estamos haciendo explícitamente nuestros cambios en el objeto en lugar de usar magia.

  • Usar __class__ = newclass parece más explícito porque decimos explícitamente “Este es ahora un objeto de la clase ‘newclass’, espera un comportamiento diferente” en lugar de cambiar silenciosamente los atributos pero dejando a los usuarios del objeto creyendo que están tratando con un objeto regular del antiguo clase.

Resumiendo: desde un punto de vista técnico, el método parece estar bien; la pregunta de la pitonicidad permanece sin respuesta con un sesgo hacia el “sí”.

He aceptado la respuesta de Martin Geisler, porque el ejemplo del complemento Mercurial es bastante fuerte (y también porque respondió a una pregunta que aún no me había preguntado). Sin embargo, si hay algún argumento sobre la cuestión de la pitonicidad, todavía me gustaría escucharlos. Gracias a todos hasta ahora.

PS El caso de uso real es un objeto de control de datos de UI que necesita boost la funcionalidad adicional en tiempo de ejecución . Sin embargo, la pregunta pretende ser muy general.

La reclasificación de instancias como esta se realiza en Mercurial (un sistema de control de revisión distribuido) cuando las extensiones (complementos) desean cambiar el objeto que representa el repository local. El objeto se llama repo y es inicialmente una instancia localrepo . Se pasa a cada extensión y, cuando sea necesario, las extensiones definirán una nueva clase que es una subclase de repo.__class__ y ¡ cambiará la clase de repo a esta nueva subclase!

Se ve así en el código:

 def reposetup(ui, repo): # ... class bookmark_repo(repo.__class__): def rollback(self): if os.path.exists(self.join('undo.bookmarks')): util.rename(self.join('undo.bookmarks'), self.join('bookmarks')) return super(bookmark_repo, self).rollback() # ... repo.__class__ = bookmark_repo 

La extensión (tomé el código de la extensión de marcadores) define una función de nivel de módulo llamada reposetup . Mercurial lo llamará al inicializar la extensión y aprobará un argumento ui (interfaz de usuario) y repo (repository).

La función entonces define una subclase de cualquier clase de repo que sea. No sería suficiente simplemente subclasificar localrepo ya que las extensiones deben poder extenderse entre sí. Entonces, si la primera extensión cambia repo.__class__ a foo_repo , la siguiente extensión debería cambiar repo.__class__ a una subclase de foo_repo y no solo a una subclase de localrepo . Finalmente, la función cambia la clase de la instancia, tal como lo hiciste en tu código.

Espero que este código pueda mostrar un uso legítimo de esta función de idioma. Creo que es el único lugar donde lo he visto en la naturaleza.

No estoy seguro de que el uso de la herencia sea mejor en este caso (al menos en lo que respecta a la “reclasificación”). Parece que estás en el camino correcto, pero parece que la composición o la agregación serían las mejores para esto. Aquí hay un ejemplo de lo que estoy pensando (en código pseudo-esque no probado):

 from copy import copy # As long as none of these attributes are defined in the base class, # this should be safe class SkilledProgrammer(Programmer): def __init__(self, *skillsets): super(SkilledProgrammer, self).__init__() self.skillsets = set(skillsets) def teach(programmer, other_programmer): """If other_programmer has skillsets, append this programmer's skillsets. Otherwise, create a new skillset that is a copy of this programmer's""" if hasattr(other_programmer, skillsets) and other_programmer.skillsets: other_programmer.skillsets.union(programmer.skillsets) else: other_programmer.skillsets = copy(programmer.skillsets) def has_skill(programmer, skill): for skillset in programmer.skillsets: if skill in skillset.skills return True return False def has_skillset(programmer, skillset): return skillset in programmer.skillsets class SkillSet(object): def __init__(self, *skills): self.skills = set(skills) C = SkillSet("malloc","free","pointer arithmetic","curly braces") SQL = SkillSet("SELECT", "INSERT", "DELETE", "UPDATE") Bob = SkilledProgrammer(C) Jill = Programmer() teach(Bob, Jill) #teaches Jill C has_skill(Jill, "malloc") #should return True has_skillset(Jill, SQL) #should return False 

Es posible que tenga que leer más sobre conjuntos y listas de argumentos arbitrarios si no está familiarizado con ellos para obtener este ejemplo.

Esto esta bien. He usado este idioma muchas veces. Sin embargo, una cosa que se debe tener en cuenta es que esta idea no funciona bien con clases de estilo antiguo y varias extensiones en C. Normalmente, esto no sería un problema, pero como está utilizando una biblioteca externa, tendrá que asegurarse de que no está tratando con clases de estilo antiguo o extensiones en C.

“El patrón de estado permite que un objeto altere su comportamiento cuando cambia su estado interno. El objeto parecerá que cambia su clase”. – Head First Design Pattern. Algo muy similar escribe Gamma et.al. en su libro de patrones de diseño. (Lo tengo en mi otro lugar, así que no hay cita). Creo que ese es el punto central de este patrón de diseño. Pero si puedo cambiar la clase de un objeto en tiempo de ejecución, la mayoría de las veces no necesito el patrón (hay casos en que State Pattern hace más que simular un cambio de clase).

Además, cambiar de clase en tiempo de ejecución no siempre funciona:

 class A(object): def __init__(self, val): self.val = val def get_val(self): return self.val class B(A): def __init__(self, val1, val2): A.__init__(self, val1) self.val2 = val2 def get_val(self): return self.val + self.val2 a = A(3) b = B(4, 6) print a.get_val() print b.get_val() a.__class__ = B print a.get_val() # oops! 

Aparte de eso, considero cambiar de clase en tiempo de ejecución Pythonic y lo uso de vez en cuando.

Jeje, ejemplo divertido.

“Reclasificación” es bastante extraño, a primera vista. ¿Qué pasa con el enfoque ‘copia constructor’? Puede hacer esto con los hasattr reflexión hasattr , getattr y setattr . Este código copiará todo de un objeto a otro, a menos que ya exista. Si no desea copiar métodos, puede excluirlos; ver el comentado if .

 class Foo(object): def __init__(self): self.cow = 2 self.moose = 6 class Bar(object): def __init__(self): self.cat = 2 self.cow = 11 def from_foo(foo): bar = Bar() attributes = dir(foo) for attr in attributes: if (hasattr(bar, attr)): break value = getattr(foo, attr) # if hasattr(value, '__call__'): # break # skip callables (ie functions) setattr(bar, attr, value) return bar 

Toda esta reflexión no es bonita, pero a veces necesitas una máquina de reflexión fea para hacer que sucedan cosas geniales. 😉

Esta técnica me parece razonablemente pythonica. La composición también sería una buena opción, pero asignar a __class__ es perfectamente válido (consulte aquí una receta que la usa de una manera ligeramente diferente).

En la respuesta de Ojrac, los saltos se break fuera del for loop y no prueban más atributos. Creo que tiene más sentido usar la sentencia if para decidir qué hacer con cada atributo de uno en uno y continuar a través de for loop en todos los atributos. De lo contrario, me gusta la respuesta de Ojrac, ya que también veo que asignar a __class__ es raro. (Soy un principiante con Python y por lo que recuerdo, esta es mi primera publicación en StackOverFlow. ¡Gracias por toda la gran información!)

Así que traté de implementar eso. Noté que dir () no lista todos los atributos. http://jedidjah.ch/code/2013/9/8/wrong_dir_function/ Así que agregué ‘ clase ‘, ‘ doc ‘, ‘ módulo ‘ e ‘ init ‘ a la lista de cosas para agregar si ya no están , (aunque es probable que ya estén todos allí), y se preguntó si había más cosas que el dir falla. También noté que estaba (potencialmente) asignándome a ” clase ” después de haber dicho que era extraño.

Diré que esto está perfectamente bien, si funciona para ti.