2011-06-08 7 views
12
class Foo(object): 
    pass 

foo = Foo() 
def bar(self): 
    print 'bar' 

Foo.bar = bar 
foo.bar() #bar 

Procedente de JavaScript, si un prototipo de "clase" se ha aumentado con un determinado atributo. Se sabe que todas las instancias de esa "clase" tendrían ese atributo en su cadena de prototipos, por lo tanto, no se deben hacer modificaciones en ninguna de sus instancias o "subclases".¿Cómo funciona la extensión de clases (Monkey Patching) en Python?

En ese sentido, ¿cómo puede un lenguaje basado en clases como Python lograr el parche de mono?

Respuesta

11

La verdadera pregunta es, ¿cómo no? En Python, las clases son objetos de primera clase por derecho propio. El acceso de atributos en instancias de una clase se resuelve buscando atributos en la instancia, y luego la clase, y luego las clases principales (en el orden de resolución del método). Todas estas búsquedas se realizan en tiempo de ejecución (como todo en Python). Si agrega un atributo a una clase después de crear una instancia, la instancia aún "verá" el nuevo atributo, simplemente porque nada lo impide.

En otras palabras, funciona porque Python no almacena en caché los atributos (a menos que su código lo haga), porque no usa caching negativo o shadowchlasses o cualquiera de las técnicas de optimización que lo inhiben (o, cuando implementa Python hacer, tienen en cuenta que la clase podría cambiar) y porque todo es tiempo de ejecución.

+1

Pensé que esto no se podía hacer y fue un diferenciador con Ruby. – OscarRyz

+0

¿El envío de métodos en idiomas dinámicos basados ​​en clases funciona de manera idéntica a los lenguajes basados ​​en prototipos? –

+0

Lo intenté yo mismo, y funcionó. Con buena pinta. – JAB

10

acabo de leer a través de un montón de documentación, y por lo que puedo decir, la toda la historia de cómo foo.bar se resuelve, es el siguiente:

  • podemos encontrar foo.__getattribute__ por el siguiente proceso ? Si es así, use el resultado de foo.__getattribute__('bar').
    • (Mirando hacia arriba __getattribute__ no causará una recursión infinita, pero la aplicación de la misma fuerza.)
    • (En realidad, siempre vamos a encontrar __getattribute__ en los objetos de nuevo cuño, como se proporciona una implementación por defecto en object - pero que la aplicación es del siguiente proceso;.))
    • (Si definimos un método __getattribute__ en Foo, y el acceso foo.__getattribute__, foo.__getattribute__('__getattribute__')se ser llamados pero esto no implica recursi infinita! en - si se tiene cuidado;))
  • Es bar un nombre "especial" para un atributo proporcionada por la ejecución de Python (por ejemplo, __dict__, __class__, __bases__, __mro__)? Si es así, usa eso. (Por lo que puedo decir, __getattribute__ entra en esta categoría, lo que evita la recursividad infinita.)
  • ¿bar en el foo.__dict__ dict? Si es así, use foo.__dict__['bar'].
  • ¿Existe foo.__mro__ (es decir, es foo en realidad una clase)? Si es así,
    • Para cada clase base base en foo.__mro__ [1:]:
      • (Tenga en cuenta que el primero de ellos será foo sí, que ya se realizaron búsquedas.)
      • ¿bar en base.__dict__? Si es así:
        • Deje x ser base.__dict__['bar'].
        • ¿Podemos encontrar (de nuevo, de forma recursiva, pero no causará ningún problema) x.__get__?
          • Si es así, utilice x.__get__(foo, foo.__class__).
          • (Tenga en cuenta que la función de bar es, en sí, un objeto, y el compilador Python da automáticamente funciones un atributo __get__ que está diseñado para ser utilizado de esta manera.)
          • lo contrario, utilice x.
  • Para cada base de clase base de foo.__class__.__mro__:
    • (Tenga en cuenta que esta recursividad no es un problema: siempre deben existir esos atributos, y caen en el " proporcionado por el caso del tiempo de ejecución de Python. foo.__class__.__mro__[0] siempre será foo.__class__, es decir, Foo en nuestro ejemplo.)
    • (Tenga en cuenta que hacemos esto incluso si existe foo.__mro__. Esto se debe a que las clases tienen una clase, también: su nombre es type, y proporciona, entre otras cosas, el método utilizado para calcular __mro__ atributos en el primer lugar)
    • Es bar en base.__dict__.? Si es así:
      • Deje x ser base.__dict__['bar'].
      • ¿Podemos encontrar (de nuevo, de forma recursiva, pero no causará ningún problema) x.__get__?
        • Si es así, utilice x.__get__(foo, foo.__class__).
        • (Tenga en cuenta que la función de bar es, en sí, un objeto, y el compilador Python da automáticamente funciones un atributo __get__ que está diseñado para ser utilizado de esta manera.)
        • lo contrario, utilice x.
  • Si todavía no hemos encontrado algo para usar: podemos encontrar foo.__getattr__ por el proceso anterior? Si es así, use el resultado de foo.__getattr__('bar').
  • Si falló todo, raise AttributeError.

bar.__get__ no es realmente una función - es un "método de envoltura" - pero se puede imaginar que sea implementado vagamente como esto:

# Somewhere in the Python internals 
class __method_wrapper(object): 
    def __init__(self, func): 
     self.func = func 
    def __call__(self, obj, cls): 
     return lambda *args, **kwargs: func(obj, *args, **kwargs) 
     # Except it actually returns a "bound method" object 
     # that uses cls for its __repr__ 
    # and there is a __repr__ for the method_wrapper that I *think* 
    # uses the hashcode of the underlying function, rather than of itself, 
    # but I'm not sure. 

# Automatically done after compiling bar 
bar.__get__ = __method_wrapper(bar) 

La "unión" que ocurre dentro de la __get__ adjunta automáticamente a bar (llamado descriptor), dicho sea de paso, es más o menos la razón por la que tiene que especificar los parámetros self explícitamente para los métodos de Python. En Javascript, this en sí mismo es mágico; en Python, es simplemente el proceso de atar cosas al self que es mágico. ;)

Y sí, puede establecer explícitamente un método __get__ en sus propios objetos y hacer que haga cosas especiales cuando se establece un atributo de clase de una instancia del objeto y luego acceder a ella desde una instancia de ese otro clase. Python es extremadamente reflectante. :) Pero si quieres aprender a hacer eso, y obtener una comprensión realmente completa de la situación, have a lot de lectura to do. ;)

+1

Gracias @karl eso es muy útil, y algo confuso :) –