0

In python, the @property and @val.setter is very helpful. For example:

from types import FunctionType
class Test:

    def __init__(self):
        self.a = 1

    @property
    def A(self):
        print('get A')
        return self.a

    @A.setter
    def A(self, val):
        print('set A')
        self.a = val

t = Test()
print(t.A)
t.A = 3
print(t.A)

It works.

Now, I want to create setProperty and getProperty for many variable, so I want to dynamic create those functions.

My code is:

from types import FunctionType
class Test:

    def __init__(self):
        self.a = 1

        code = compile('@property\ndef A(self): print("get A")\nreturn self.a', '', 'exec')
        FunctionType(code.co_consts[0], globals(), "A")

        code = compile('@A.setter\ndef A(self, val): print("set A")\nself.a=val', '', 'exec')
        FunctionType(code.co_consts[0], globals(), "A")

t = Test()
print(t.A)
t.A = 3
print(t.A)

And it reports a bug:

Traceback (most recent call last):
  File "C:/Users/Administrator/Desktop/medpro/test.py", line 23, in <module>
    t = Test()
  File "C:/Users/Administrator/Desktop/medpro/test.py", line 7, in __init__
    code = compile('@property\ndef A(self): print("get A")\nreturn self.a', '', 'exec')
  File "", line 3
SyntaxError: 'return' outside function

Then, I remove print("get A"), and another bug is reported:

Traceback (most recent call last):
  File "C:/Users/Administrator/Desktop/medpro/test.py", line 24, in <module>
    print(t.A)
AttributeError: 'Test' object has no attribute 'A'
jonrsharpe
  • 115,751
  • 26
  • 228
  • 437
Qiang Zhang
  • 820
  • 8
  • 32
  • You don't need to dynamically compile code for this, you can create your own [_descriptor_](https://docs.python.org/3/howto/descriptor.html) that extends `property` with the logging behaviour. Examples: https://softwareengineering.stackexchange.com/a/291286/110531, https://stackoverflow.com/q/39539292/3001761, https://stackoverflow.com/a/27404221/3001761 – jonrsharpe Apr 04 '22 at 10:07
  • 2
    You could also use custom `__getattr__` and `__setattr__` methods. – Wombatz Apr 04 '22 at 10:09
  • example from fluent python book https://github.com/fluentpython/example-code-2e/blob/master/22-dyn-attr-prop/bulkfood/bulkfood_v2prop.py – deadshot Apr 04 '22 at 10:13
  • I wonder if [this answer](https://stackoverflow.com/a/1355444/2081835) might help. – theherk Apr 04 '22 at 10:15
  • smt similar [here](https://stackoverflow.com/questions/69125694/dynamically-add-overwrite-the-setter-and-getter-of-property-attributes) – cards Apr 04 '22 at 10:31
  • Thanks for all kindly reply. @jonrsharpe, @deadshot I want the `set` happen inside the scope of `Test` class. The method proved by the post define a new class, which can not access `Test` class when `set` happen. @Wombatz, the `__getattr__` and `__setattr__` would break the interface of my class. I want to keep `t.A` rather than `t['A']`. @theherk, Sorry, I still don't know how to solve this question after reading the post. – Qiang Zhang Apr 04 '22 at 14:16
  • _"define a new class, which can not access `Test` class when `set` happen"_ - what? It defines a new class that can be used inside `Test` class and which gets access to the object _via the descriptor protocol_. `class Test: A = LoggedProperty('a')`, for example. – jonrsharpe Apr 04 '22 at 14:18

2 Answers2

0

You can, but do not have to use property as decorator, consider following example from property docs

class C:
    def __init__(self):
        self._x = None

    def getx(self):
        return self._x

    def setx(self, value):
        self._x = value

    def delx(self):
        del self._x

    x = property(getx, setx, delx, "I'm the 'x' property.")
Daweo
  • 31,313
  • 3
  • 12
  • 25
  • 1
    But I need to create many `set/get` function for many variable. That's the reason why I want to dynamic create the functions. – Qiang Zhang Apr 04 '22 at 12:21
0

To dynamically add descriptors, in this case read & write, you can first create a decorator which take as arguments the attributes identifiers and add them to your target class.

class DynamicDescriptors:
    def __init__(self, id_attributes: tuple):
        self.ids = id_attributes

    def __call__(self, cls):
        for attr in self.ids:
            # getter
            setattr(cls, attr, property(lambda cls_self: getattr(cls, attr)))
            # setter
            setattr(cls, attr, getattr(cls, attr).setter(lambda cls_self, v: setattr(cls, attr, v)))
            
        return cls
   

dynamic_attrs = ('a', 'b', 'c')

@DynamicDescriptors(dynamic_attrs)
class Test:
    pass

# access from instance
t = Test()
t.a = 'a'
print(t.a)
t.b = 'b'
print(t.b)
t.c = 'c'
print(t.c)

# access from class
Test.a = 10
print(Test.a)

# property object
print(Test.b)

Output

a
b
c
10
<property object at 0x7f5ff9d12c70>

EDIT (reimplementation) without decorator + support for print (or custom implementation)

There are a plenty of different ways to achieve the goal without decorator. I like to separate the tasks, so I add a classmethod which add the descriptors and the attributes are given as class attribute.

Note by personal choice I assign to a descriptor a a private name __a. Since adding the descriptor dynamically and since the private name mangling happens at compilation time the attribute __a should be called as _ClassName__a, see docs 1, 2. For non private name attribute no need for that.

class Test:

    dynamic_attrs = ('a', 'b', 'c')

    @classmethod
    def dynamic_descriptors(cls, *id_attributes):

        def prop_factory(attr):

            def getter(attr):
                def __wrapper(self):
                    p_attr = f'_{type(self).__name__}__{attr}' # private name
                    v_attr = getattr(self, p_attr)
                    print(f'getter {attr}: {v_attr}')
                    return v_attr
                return __wrapper

            def setter(attr):
                def __wrapper(self, v):
                    p_attr = f'_{type(self).__name__}__{attr}' # private name
                    old_attr = getattr(self, p_attr) if hasattr(self, p_attr) else 'None'
                    setattr(self, p_attr, v)
                    print(f'setter {attr}: {old_attr} -> {v}')
                return __wrapper

            return property(getter(attr), setter(attr))

        for attr in id_attributes:
            setattr(cls, attr, prop_factory(attr))

    def __init__(self):
        self.dynamic_descriptors(*self.dynamic_attrs)


t = Test()

print(Test.a)
#<property object at 0x7f4962138cc0>
t.a = 'aaa'
#setter a: None -> aaa
t.a
#getter a: aaa 
t.b = 'b'*4
#setter b: None -> bbbb
t.b = 'b'*2
#setter b: bbbb -> bb
t.b
#getter b: bb 
t.c = 'c'
#setter c: None -> c
cards
  • 3,936
  • 1
  • 7
  • 25
  • Is it possible not use the `DynamicDescriptors`? Could you please help me check the posted code? I wonder what's wrong with my code. – Qiang Zhang Apr 04 '22 at 12:32
  • @Qiang Zhang instead using the types I created a custom class, DynamicDescriptors – cards Apr 04 '22 at 13:21
  • @Qiang Zhang the indentation is for sure a problem: `\n\t` is a must. But you are adding a method, so you need (not sure) to add to each line an extra indentation because are methods PS your string does not make much sense: `@property\n\tdef A(self):\n\tprint("get A")\n\treturn self.a'` <- better but may require a furher "global" indentation – cards Apr 04 '22 at 13:35
  • Unfortunately, it still report `unexpected indent` when I run `compile('@property\n\tdef A(self):\n\tprint("get A")\n\treturn self.a')` – Qiang Zhang Apr 04 '22 at 13:49
  • and with `\t'@property\n\t\tdef A(self):\n\t\tprint("get A")\n\t\treturn self.a'`? – cards Apr 04 '22 at 13:53
  • Yes, it still report `unexpected indent for `\t'@property\n\t\tdef A(self):\n\t\tprint("get A")\n\t\treturn self.a'`. In addition, if I `setattr(self, 'A', property(lambda self: self.a))` in the class, `t=Test()` and `t.A` would be ``. How can make `t.A` return `self.a`? – Qiang Zhang Apr 04 '22 at 14:23
  • see the edit I did – cards Apr 04 '22 at 14:31
  • `setattr(self, 'a', ...)` is not what I want. I have define `a` as a variable in the class by `self.a=1`. I want to do something, for example `print('a is changed')` if `self.a` is changed. That's why I use `property`. – Qiang Zhang Apr 04 '22 at 14:33
  • @Qiang Zhang fixed it! – cards Jun 03 '22 at 22:13