Cómo falsificar el tipo con Python

Recientemente desarrollé una clase llamada DocumentWrapper alrededor de un objeto de documento ORM en Python para agregarle algunas características de forma transparente sin cambiar su interfaz de ninguna manera.

Solo tengo un problema con esto. Digamos que tengo un objeto de User envuelto en él. Llamar a isinstance(some_var, User) devolverá False porque some_var es una instancia de DocumentWrapper .

¿Hay alguna manera de falsificar el tipo de un objeto en Python para que la misma llamada devuelva True ?

La prueba del tipo de un objeto suele ser un antipattern en python. En algunos casos, tiene sentido probar el “tipo de pato” del objeto, algo como:

 hasattr(some_var, "username") 

Pero incluso eso no es deseable, por ejemplo, hay razones por las que esa expresión puede devolver falsa, incluso si un contenedor usa algo de magia con __getattribute__ para representar correctamente el atributo.

Generalmente se prefiere permitir que las variables solo tomen un solo tipo abstracto, y posiblemente None . Se deben lograr diferentes comportamientos basados ​​en diferentes entradas al pasar los datos escritos opcionalmente en diferentes variables. Quieres hacer algo como esto:

 def dosomething(some_user=None, some_otherthing=None): if some_user is not None: #do the "User" type action elif some_otherthing is not None: #etc... else: raise ValueError("not enough arguments") 

Por supuesto, todo esto supone que tiene algún nivel de control del código que está realizando la comprobación de tipo. Supongamos que no lo es. para que “isinstance ()” devuelva true, la clase debe aparecer en las bases de la instancia, o la clase debe tener un __instancecheck__ . Ya que no controlas ninguna de esas cosas para la clase, debes recurrir a algunos chanchullos en la instancia. Haz algo como esto:

 def wrap_user(instance): class wrapped_user(type(instance)): __metaclass__ = type def __init__(self): pass def __getattribute__(self, attr): self_dict = object.__getattribute__(type(self), '__dict__') if attr in self_dict: return self_dict[attr] return getattr(instance, attr) def extra_feature(self, foo): return instance.username + foo # or whatever return wrapped_user() 

Lo que estamos haciendo es crear una nueva clase dinámicamente en el momento en que necesitamos envolver la instancia y, en realidad, heredar de la __class__ del objeto envuelto. También nos tomamos la molestia de anular el __metaclass__ , en caso de que el original tuviera algunos comportamientos adicionales que realmente no queremos encontrar (como buscar una tabla de base de datos con un determinado nombre de clase). Una buena conveniencia de este estilo es que nunca tenemos que crear ningún atributo de instancia en la clase de envoltura, no hay un self.wrapped_object , ya que ese valor está presente en el momento de la creación de la clase .

Edición: Como se señaló en los comentarios, lo anterior solo funciona para algunos tipos simples, si necesita representar atributos más elaborados en el objeto de destino (por ejemplo, métodos), luego vea la siguiente respuesta: Python – Tipo de falsificación Continúa

Puede usar el método mágico __instancecheck__ para anular el comportamiento predeterminado de la isinstance :

 @classmethod def __instancecheck__(cls, instance): return isinstance(instance, User) 

Esto es solo si desea que su objeto sea un envoltorio transparente ; es decir, si desea que un DocumentWrapper comporte como un User . De lo contrario, simplemente exponga la clase envuelta como un atributo.

Esta es una adición de Python 3; Vino con clases de base abstractas. No puedes hacer lo mismo en Python 2.

Sobrescriba __class__ en su contenedor de DocumentWrapper :

 class DocumentWrapper(object): @property def __class__(self): return User >>> isinstance(DocumentWrapper(), User) True 

De esta manera no se necesitan modificaciones a la clase envuelta User .

Python Mock hace lo mismo (ver mock.py:612 en mock-2.0.0, no se pudieron encontrar fonts en línea para enlazar, lo siento).

Aquí hay una solución usando metaclase, pero necesita modificar las clases envueltas:

 >>> class DocumentWrapper: def __init__(self, wrapped_obj): self.wrapped_obj = wrapped_obj >>> class MetaWrapper(abc.ABCMeta): def __instancecheck__(self, instance): try: return isinstance(instance.wrapped_obj, self) except: return isinstance(instance, self) >>> class User(metaclass=MetaWrapper): pass >>> user=DocumentWrapper(User()) >>> isinstance(user,User) True >>> class User2: pass >>> user2=DocumentWrapper(User2()) >>> isinstance(user2,User2) False 

Parece que desea probar el tipo de objeto que su DocumentWrapper envuelve, no el tipo del mismo DocumentWrapper . Si eso es correcto, entonces la interfaz de DocumentWrapper necesita exponer ese tipo. Puede agregar un método a su clase DocumentWrapper que devuelva el tipo del objeto envuelto, por ejemplo. Pero no creo que hacer la llamada a una isinstance ambigua, al hacerla realidad cuando no lo es, es la forma correcta de resolver esto.

La mejor forma es heredar DocumentWrapper del propio Usuario, o un patrón de mezcla y hacer herencia múltiple de muchas clases.

  class DocumentWrapper(User, object) 

También puede falsificar los resultados de la instancia () manipulando obj.__class__ pero esto es magia de nivel profundo y no debe hacerse.