pythonoopsetter

What is the python alternative for C#'s custom property setters?


I'm coming from C# world into python. In C#, I can use get and set to make updates to other object properties upon setting a value to a property, something like this:

private int _age;
private string _description;

public int Age
{
    set
    {
        _age = value;
        _description = (_age < 1500) ? "new": "old";
    }
}

In python, I have the following class definition:

class Record(object):
    age = 0
    description = ""
    <...a bunch of other properties...>

How can I set the description property of a Record object when I set the age property, in the same way as I can do it in C#, without having to write custom functions like get_age(self): and set_age(self):?


Solution

  • You should use the property decorator for this:

    class Record:
        def __init__(self):
            self._age = 0
            self._description = ""
            
        @property
        def age(self):
            return self._age
    
        @age.setter
        def age(self, value):
            self._age = value
            self._description = "new" if value < 1500 else "old"
    
        @property
        def description(self):
            return self._description
    
    rec = Record()
    print(rec.age, rec.description)
    rec.age = 1000
    print(rec.age, rec.description)
    rec.age = 2000
    print(rec.age, rec.description)
    

    output:

    0 
    1000 new
    2000 old
    

    When you use @property, every time you use rec.description, you are actually calling rec.description() and the return value of that call is used as the attributes value. This value gets recalculated every time you use the attribute.

    If there was only the @property decorator, it would be impossible to set the attribute, as there is only a function that calculates the value. Luckily, we can use @age.setter. Every time the attribute is set (rec.age = value), this function is called.

    We can use of this to make the description attribute also being set when the age attribute is set.

    If you want to learn more about decorators, this page is a good start.

    Another, less recommended, way to do this is using __setattr__:

    class Record:
        def __init__(self):
            self.age = 0
            self.description = ""
            
        def __setattr__(self, key, value):
            super().__setattr__(key, value)
            if key == "age":
                self.description = "new" if value < 1500 else "old"
    
    rec = Record()
    print(rec.age, rec.description)
    rec.age = 1000
    print(rec.age, rec.description)
    rec.age = 2000
    print(rec.age, rec.description)
    

    The output is exactly the same.

    Every time we set an attribute on an instance by using instance.attrname = attrvalue, python calls instance.__setattr__("attrname", attrvalue). Usually, this is the __setattr__ function which is inherited from object, the class from which every class inherits.

    But when we override this function, we can choose our own behavior. In this case, it means that we also set the description attribute when the attribute 's name is "age".

    But, of course, we should still call the object's __setattr__ function to make that the attribute is actually set. We could do this directly using object.__setattr__(self, key, value), but a better way to do this is using super. super() allows us to call functions of the class from which there is inherited. In this case it's object, but if you later on decide to let Record inherit from another class that also defines __setattr__, that class's __setattr__ will be called.