2

I have known the use of setter and getter for several properties, how could I trigger a same function when any property changes?

For example, the following codes add a setter to property a.

class AAA(object):
    def __init__(self):
        ...
    @property
    def a(self):
        ...
    @a.setter
    def a(self, value):
        ...

If the class has a lot of properties like a, b, ... , z, and I want to print something like property xxx is modified when any property changes.

It is stupid to add the similar getter and setter one by one.

I have read some related questions and answers, but I do not find the solution for many properties.

Hansimov
  • 597
  • 7
  • 10
  • Which Python version are you using? – timgeb Dec 07 '18 at 12:29
  • Do you anyways want to define getters, or do you really just want to know when something is *changed*? – L3viathan Dec 07 '18 at 12:32
  • @timgeb Python 3.6+, and I think this may have nothing to do with the version. – Hansimov Dec 07 '18 at 12:34
  • 3
    Any property or any attribute? If any attribute then it's probably easiest to override `__setattr__`? – Dunes Dec 07 '18 at 12:34
  • @L3viathan If one could know any property is changed, then he could do any things. The hard part is not **what to do when know any property is changed**, but **how to know if any property is changed**. – Hansimov Dec 07 '18 at 12:38
  • @Dunes That's it! The accepted answer gives an example. Thanks a lot! – Hansimov Dec 07 '18 at 12:42

3 Answers3

4

Metaprogramming, using __setattr__ to intercept modification:

class AAA(object):
    def __setattr__(self, attr, value):
        print("set %s to %s" % (attr, value))
        super().__setattr__(attr, value)

aaa = AAA()
aaa.x = 17
# => set x to 17
print(aaa.x)
# => 17

You can do similarly with __getattr__ for reading access.

Amadan
  • 191,408
  • 23
  • 240
  • 301
  • This is what I want! Thanks a lot! BTW, I wonder why no answer refers to the `__setattr__` in the related questions I mentioned ... – Hansimov Dec 07 '18 at 12:44
  • 2
    It's a bit of a chicken and an egg kind of problem. To find a relevant answer it really helps to know keywords to search for already (and if you know the keywords you might not need the answer in the first place). Reading the documentation for your programming language is a great way to expand your horisons. Specifically, for this, reading the [data model](https://docs.python.org/3/reference/datamodel.html) page is super useful. – Amadan Dec 07 '18 at 12:47
  • Your words really inspire me a lot. The key point of many questions and problems is just **how to describe it**, while in most cases the description contains the answer. Your advice for reading documentation is very helpful. Thanks again! – Hansimov Dec 07 '18 at 12:56
1

You can use descriptors. Descriptors are, in layman's terms, reusable properties. The advantage over the __getattr__ and __setattr__ hooks is that you have more fine-grained control over what attributes are managed by descriptors.

class MyDescriptor:
    def __init__(self, default='default'):
        self.default = default

    def __set_name__(self, owner, name): # new in Python3.6
        self.name = name

    def __get__(self, instance, owner):
        print('getting {} on {}'.format(self.name, instance))
        # your getter logic here
        # dummy implementation:
        if instance is not None:
            try:
                return vars(instance)[self.name]
            except KeyError:
                return self.default
        return self

    def __set__(self, instance, value):
        print('setting {} on {}'.format(self.name, instance))
        # your getter logic here
        # dummy implementation:
        vars(instance)[self.name] = value

class MyClass:
    a = MyDescriptor()
    b = MyDescriptor()

    _id = 1

    # some logic for demo __repr__
    def __init__(self):
        self.c = 'non-descriptor-handled'
        self.id = MyClass._id
        MyClass._id += 1

    def __repr__(self):
        return 'MyClass #{}'.format(self.id)

Demo:

>>> m1 = MyClass()
>>> m2 = MyClass()
>>> m1.c
'non-descriptor-handled'
>>> m1.a
getting a on MyClass #1
'default'
>>> m1.b
getting b on MyClass #1
'default'
>>> m1.b = 15 
setting b on MyClass #1
>>> m1.b
getting b on MyClass #1
15
>>> m2.b
getting b on MyClass #2
'default'
timgeb
  • 76,762
  • 20
  • 123
  • 145
  • Although a little bit complicated, your answer still gives a useful method. In your solution, one just needs to register/handle the specified properties as `MyDescriptor` objects as he wants. There is an advantage that if one does not want to trigger the function for some properties, he can just let it be. – Hansimov Dec 07 '18 at 13:06
  • 1
    @Hansimov In addition, you could write *other* descriptor classes and instantiate different attributes as different descriptors. If you want general, reusable, flexible properties it's going to be a little complicated, no way around that. :) Descriptors are very powerful, but you have to know when to use them. In simple use cases, `__getattr__`/`__setattr__` hooks or `property` decorated methods can do the job. – timgeb Dec 07 '18 at 14:20
0

One year after asking this question, I find a more elgant way to add getter and setter to multiple similar properties.

Just make a more 'abstract' function which returns decorated property. And pass each of these properties to this function with a for loop. Then the getter and setter of all these properties are added.

def propABC(arg):
    # arg: 'a', 'b', 'c'
    @property
    def prop(self):
        _arg = '_' + arg
        return getattr(self, _arg)
    @prop.setter
    def prop(self, val):
        _arg = '_' + arg
        setattr(self, _arg, val)
        print(f"Set prop {_arg}")
    return prop

for key in ['a', 'b', 'c']:
    exec(f"{key} = propABC('{key}')")
Hansimov
  • 597
  • 7
  • 10