jasp116
jasp116

Reputation: 1

Dynamically add @property to python class from user defined string

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

Upvotes: 0

Views: 223

Answers (1)

salparadise
salparadise

Reputation: 5805

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]

Upvotes: 1

Related Questions