python python-3.x monkeypatching

python - Mono parcheando una propiedad @



python-3.x monkeypatching (6)

¿Es posible parchear el valor de una @property de una instancia de una clase que no controlo?

class Foo: @property def bar(self): return here().be[''dragons''] f = Foo() print(f.bar) # baz f.bar = 42 # MAGIC! print(f.bar) # 42

Obviamente, lo anterior produciría un error al intentar asignar a f.bar . Es # MAGIC! posible de alguna manera? Los detalles de implementación de la @property son una caja negra y no indirectamente @property el mono. Es necesario reemplazar toda la llamada al método. Debe afectar solo a una sola instancia (la aplicación de parches a nivel de clase está bien si es inevitable, pero el comportamiento modificado solo debe afectar selectivamente a una instancia determinada, no a todas las instancias de esa clase).


Acabo de encontrar esta Q&A porque PyCharm se estaba quejando de que "la propiedad ''barra'' no se puede configurar", pero esto realmente funcionó para mí:

>>> class Foo: ... @property ... def bar(self): ... return ''Foo.bar'' ... >>> f = Foo() >>> f.bar ''Foo.bar'' >>> @property ... def foo_bar(self): ... return 42 ... >>> Foo.bar = foo_bar >>> f.bar 42 >>>

Sé que es posible que esto no cumpla con la condición de instancia única , pero si mi respuesta ya estuviera aquí, me ahorraría algo de tiempo;)


Idea: reemplazar descriptor de propiedad para permitir la configuración en ciertos objetos. A menos que un valor se establezca explícitamente de esta manera, se llama al captador de propiedad original.

El problema es cómo almacenar los valores establecidos explícitamente. No podemos usar un dict codificado por objetos parcheados, ya que 1) no son necesariamente comparables por identidad; 2) esto evita que los objetos parcheados sean recolectados en la basura. Para 1) podríamos escribir un Handle que envuelva los objetos y anule la comparación de la semántica por identidad y para 2) podríamos usar weakref.WeakKeyDictionary . Sin embargo, no pude hacer que estos dos trabajen juntos.

Por lo tanto, utilizamos un enfoque diferente para almacenar los valores establecidos explícitamente en el propio objeto, usando un "nombre de atributo muy improbable". Por supuesto, todavía es posible que este nombre coincida con algo, pero eso es bastante inherente a lenguajes como Python.

Esto no funcionará en objetos que carecen de una ranura __dict__ . Sin embargo, un problema similar surgiría para los débiles.

class Foo: @property def bar (self): return ''original'' class Handle: def __init__(self, obj): self._obj = obj def __eq__(self, other): return self._obj is other._obj def __hash__(self): return id (self._obj) _monkey_patch_index = 0 _not_set = object () def monkey_patch (prop): global _monkey_patch_index, _not_set special_attr = ''$_prop_monkey_patch_{}''.format (_monkey_patch_index) _monkey_patch_index += 1 def getter (self): value = getattr (self, special_attr, _not_set) return prop.fget (self) if value is _not_set else value def setter (self, value): setattr (self, special_attr, value) return property (getter, setter) Foo.bar = monkey_patch (Foo.bar) f = Foo() print (Foo.bar.fset) print(f.bar) # baz f.bar = 42 # MAGIC! print(f.bar) # 42


Para parchear una propiedad, hay una forma aún más simple:

from module import ClassToPatch def get_foo(self): return ''foo'' ClassToPatch.foo = property(get_foo)


Parece que necesita pasar de las propiedades a los dominios de descriptores de datos y no descriptores de datos. Las propiedades son solo una versión especializada de descriptores de datos. Las funciones son un ejemplo de no descriptores de datos: cuando los recuperas de una instancia, devuelven un método en lugar de la función en sí.

Un descriptor que no es de datos es solo una instancia de una clase que tiene un método __get__ . La única diferencia con un descriptor de datos es que también tiene un método __set__ . Las propiedades inicialmente tienen un método __set__ que __set__ un error a menos que proporcione una función de establecimiento.

Puede lograr lo que quiere realmente fácilmente con solo escribir su propio descriptor trivial sin datos.

class nondatadescriptor: """generic nondata descriptor decorator to replace @property with""" def __init__(self, func): self.func = func def __get__(self, obj, objclass): if obj is not None: # instance based access return self.func(obj) else: # class based access return self class Foo: @nondatadescriptor def bar(self): return "baz" foo = Foo() another_foo = Foo() assert foo.bar == "baz" foo.bar = 42 assert foo.bar == 42 assert another_foo.bar == "baz" del foo.bar assert foo.bar == "baz" print(Foo.bar)

Lo que hace que todo este trabajo funcione es la lógica bajo el capó __getattribute__ . No puedo encontrar la documentación adecuada en este momento, pero el orden de recuperación es:

  1. Los descriptores de datos definidos en la clase reciben la prioridad más alta (objetos con __get__ y __set__ ), y se invoca su método __get__ .
  2. Cualquier atributo del objeto en sí.
  3. Descriptores que no son datos definidos en la clase (objetos con solo un método __get__ ).
  4. Todos los demás atributos definidos en la clase.
  5. Finalmente, el método __getattr__ del objeto se invoca como último recurso (si está definido).

Subclase la clase base ( Foo ) y cambie la clase de instancia única para que coincida con la nueva subclase usando el atributo __class__ :

>>> class Foo: ... @property ... def bar(self): ... return ''Foo.bar'' ... >>> f = Foo() >>> f.bar ''Foo.bar'' >>> class _SubFoo(Foo): ... bar = 0 ... >>> f.__class__ = _SubFoo >>> f.bar 0 >>> f.bar = 42 >>> f.bar 42


from module import ClassToPatch def get_foo(self): return ''foo'' setattr(ClassToPatch, ''foo'', property(get_foo))