0

I have a class instance with several properties:

class MyClass(object):
    @property
    def func(self):
        return [1,4,5] 

    @property
    def func2(self):
        return 6

I would like to dynamically change a property from a user supplied new method, for example:

obj = MyClass()

def patch_property(name, new_return):
    source = '@property\ndef %s(self):\n   return %s' % (parameter_name, new_return) 
    code = compile(source, file_name, 'exec')`

    class SubMyClass(MyClass):
        eval(code)

    obj.__class__ = SubMyClass

patch_property('func', [6,7,8])

This works, however it changes type(obj), which messes up some other stuff. Doing the following does not:

cls = type(obj)
new_cls = type(cls.__name__, (cls,), {})
obj.__class__ = new_cls

However, I can't figure out how to properly get the eval(code) from above in the new_cls. Any ideas on how to solve this?

I also tried monkeypatching the property:

    def patch_fun(code):
        def patched_fun(self):
            eval(code)

        return patched_fun

patched_fun = patch_fun(code)
setattr(cls, name, property(patched_fun))

or the bound method:

patched_fun = patch_fun(code).__get__(obj, type(obj))
setattr(cls, name, property(patched_fun))

(I couldn't figure it out from these: Dynamically adding a property to a class , Dynamically adding @property in python, Monkey Patching an attribute within a class, Monkey patching a @property , Python: changing methods and attributes at runtime

jasp116
  • 1
  • 1
  • 1
    `compile()` inside your code seems like a massive securiry breach. In previous posts there are attempts to use setatrr and a lambda function, which feel more reasonable. – Evgeny May 22 '18 at 05:46

1 Answers1

1

I would avoid using eval due to the potential security implications.

This do what you are after without eval:

def patch_property(name, new_return):
    def _new_property(self):
        return new_return

    setattr(obj.__class__, name, property(_new_property))

Demo:

In [39]: class MyClass:
    ...:     pass
    ...:

In [40]: obj = MyClass()

In [41]: type(obj)
Out[41]: __main__.MyClass

In [42]: patch_property('func', [6,7,8])

In [43]: type(obj)
Out[43]: __main__.MyClass

In [44]: obj.func
Out[44]: [6, 7, 8]

This will of course change all objects from this class.

Look into metaclasses for doing this sort of stuff.

Edit, this version of patch_property takes a custom callable:

In [105]: class MyClass(object):
     ...:     def __init__(self):
     ...:         self.a = 10  #accounts for attribute referenced
     ...:
     ...:     @property
     ...:     def func(self):
     ...:         return [1,4,5]
     ...:
     ...:     @property
     ...:     def func2(self):
     ...:         return 6


In [106]: def patch_property(name, new_return):
     ...:     def _new_property(self):
     ...:         return new_return
     ...:
     ...:     setattr(obj.__class__, name, property(new_return if callable(new_return) else _new_property))


In [107]: def custom_func(self):
     ...:     x = self.a * 4
     ...:     z = x * 9
     ...:     return z

In [108]: obj = MyClass()

In [109]: patch_property('func', custom_func)

In [110]: obj.func
Out[110]: 360

In [111]: patch_property('func', [4, 5, 6])

In [112]: obj.func
Out[112]: [4, 5, 6]
salparadise
  • 5,699
  • 1
  • 26
  • 32
  • so how about instead of new_return I would like a multiline function. For example `'x = self.a * 4\nz = x *9\nreturn z'` – jasp116 May 22 '18 at 06:12
  • 1
    @jasp116 Then pass the whole `_new_property` as argument, rather than just `new_return`. – zvone May 22 '18 at 06:14
  • @jasp116 what zvone says, have the function take a callable. I edited a version that takes either a value or a callable. – salparadise May 22 '18 at 06:22
  • @salparadise unfortunately the setattr way of patching doesn't work as the method has to be created from inside MyClass for some other stuff to work. So like the MySubClass method but then inside MyClass somehow.. – jasp116 May 22 '18 at 06:33
  • @zvone and how would that work if it was a string as in my comment above? – jasp116 May 22 '18 at 11:05