ZhangZhihui's Blog  

Creating Managed Attributes using properties

Properties can be used to create data attributes with special functionality. If you want some extra functionality (like type checking, data validation or transformation) while getting or setting a data attribute, you can define a property which creates a managed attribute. The user can access and modify this managed attribute with regular syntax (e.g. print(MyClass.x) or MyClass.x = 3), but behind the scene some method will be automatically executed while setting or getting the attribute. Property allows us to access data like a variable, but the accessing is handled internally by methods. This way, we can control attribute access by attaching custom behavior.

Suppose we have developed this class Person, with two instance variables name and age, and the method display.

class Person:
    def __init__(self, name, age):
        self.name = name
        self.age = age

    def display(self):
        print(self.name, self.age)


if __name__ == '__main__':
    p = Person('Raj', 30)
    p.display()

Let us assume that this is a big class that is being used by many clients. After some time, we as the implementors of the class want to restrain the value of age. We want to ensure that whenever age is assigned a value, that value should be within the range 20 - 80.

A solution to this could be to make age a private variable and use getter and setter methods to access and update this private variable. Setters (also know as mutators) and getters (also know as accessors) are generally used in object-oriented languages to restrict access to private variables and they allow you to control how these variables are accessed and updated.

We modify the class and make age a private variable by prefixing it with an underscore, so now client is not supposed to access it directly. We define a method set_age that will be used to assign a value to the private variable _age, and we define another method get_age that will be used to access the value of variable _age. In the set_age method we can put the validation code.

class Person:
    def __init__(self, name, age):
        self.name = name
        self._age = age

    def display(self):
        print(self.name, self._age)

    def set_age(self, new_age):
        if 20 <= new_age <= 80:
            self._age = new_age
        else:
            raise ValueError('Age must be between 20 and 80')

    def get_age(self):
        return self._age


if __name__ == '__main__':
    p = Person('Raj', 30)
    p.display()

Now, whenever the user wants to change the age, he will do it through the set_age method, and the data validation will be done.

>>> p.set_age(100)

ValueError: Age must be between 20 and 80

>>> p.set_age(12)

ValueError: Age must be between 20 and 80

>>> p.set_age(25)

>>> p.display()

Raj 25

So, by defining the setter and getter methods, we could successfully implement the new restriction on age.

Earlier when there was no restriction, and age was a public variable, if the user had to increase the current age by 1, he would simply write:

p.age +=1

Now in the modified class, we have setter and getter methods so to increase the value of age, user has to write this:

p.set_age(p.get_age() + 1)

These types of expressions are confusing and decrease readability. There is still a problem in our modified class. When the user creates a new object, he can send any value for the age because there is no data validation done in the initializer.

p1 = Person('Dev', 2000)

So, we need to perform the data validation in the initializer also by calling the set_age method.

    def __init__(self, name, age):
         self.name = name
         self.set_age(age)

Now the data validation will be done at the time of creation of a new object also. It seems that we have solved the problem of restricting the value of age. Now users of our class will not be able to enter any value of age outside the range 20-80. But remember our Person class is being used by several clients, and there is lot of existing code that accesses age directly, for example p.age = 30 or print(p.age). The new changes in your class will break this client code and it will have to be rewritten with statements like p.set_age(30) and print(p.get_age(). You have changed the user interface and so your new update is not backward compatible. This refactoring can cause problems in your client code.

To avoid this problem, in other object-oriented languages, programmers would start their class design with private attributes along with getters and setters that do nothing except getting and setting the value of the private variable. These setters and getters do not perform any extra processing and they are not needed at the outset but they have to be added because they might be needed later, when you need some processing to be done while setting and getting an attribute. This design makes sure that if in future you have to add any data validation, then the existing client code will not break. The clients will already be accessing data through setters and getters, so you can change the implementation without changing the interface and breaking your client’s code.

The getter and setter methods can also be used to make an attribute read only or write only. If you define only the getter method for a private variable and don’t define the setter method for it then the variable becomes read only, users will be able to read that variable but cannot update it. As we have seen, setters and getters also allow data validation, i.e., the setter method can control what value can be assigned to the variable and getter method can change the way the variable is represented when it is accessed. In most other languages, getter and setter methods are common and they are used to protect and validate your private data.

This setter and getter methods approach is not preferred in Python, the Pythonic way of going about this whole thing would be to create a property. Properties allow us to write our class in a way that does not require the user of the class to call setter and getter methods.

class Person:
    def __init__(self, name, age):
        self.name = name
        self._age = age

    @property
    def age(self):
        return self._age

    @age.setter
    def age(self, new_age):
        if 20 <= new_age <= 80:
            self._age = new_age
        else:
            raise ValueError('Age must be between 20 and 80')

    def display(self):
        print(self.name, self._age)

We have added two special methods, and both are named age. Before the header line of these methods, we have added a line starting with ‘@’ symbol. The line @property makes the first method a getter method, and the line @age.setter makes the second method a setter method.

Now after this modification, the name age has become a property, we can access it like we access an instance variable. There is no need to call it like a method by using parentheses. The actual value of age is stored in the private variable named _age. The age attribute is a property which provides an interface to this private variable. The name of the property should be different from the attribute where we store our data.

Whenever we reference the attribute named age, the method with the line @property will be executed and whenever we assign something to it, the method with the line @age.setter will be executed. The method with @property is the getter method and the method with @age.setter is the setter method for the property.

 

The user of the class can now access age as if it were an instance variable.

>>> p = Person('Raj', 30)

>>> p.age + 1

31

>>> p.age = 40

>>> p.age = 200

ValueError: Age must be between 20 and 80

>>> p.age()

TypeError: 'int' object is not callable

 

Creating read only attributes using properties

Another use of property is that you can make an attribute read-only or write-only. If you provide only the getter method, not the setter method, the property becomes a read only property. This way we can protect our private attribute from any sort of modification by the client, while still giving the access to read the value of the attribute.

 

Creating Computed attributes using properties

A common use of property is to create dynamically computed attributes, the values of these attributes are not actually stored, they are computed on request.

class Rectangle():
    def __init__(self, length, breadth):
       self.length = length
       self.breadth = breadth
       self.diagonal = (self.length * self.length + self.breadth * self.breadth) ** 0.5 

    def area(self):
        return self.length * self.breadth

    def perimeter(self):
        return 2 * (self.length + self.breadth)

In this Rectangle class we have three instance variables, length, breadth and diagonal, and two methods area and perimeter. The value of instance variable diagonal is computed from the values of instance variables length and breadth.

>>> r = Rectangle(2, 5)

>>> r.diagonal

5.385164807134504

>>> r.area()

10

>>> r.perimeter()

14

Now let us change length:

>>> r.length = 10

>>> r.diagonal

5.385164807134504

We changed length, but value of diagonal has not changed.

>>> r.area()

50

>>> r.perimeter()

30

Area and perimeter have changed because they are implemented as methods.

So, if you change the value of an instance variable, any other instance variable that is computed from it will not automatically update. Here in this class if we change length or breadth, then diagonal will not change accordingly. One solution could be to implement diagonal as a method. But then we will not be able to access it as an instance variable; whenever we want to access it, we have to put parentheses. This will also break any client code that has used diagonal as an instance variable. The solution is to turn it into a property.

    @property
    def diagonal(self):
        return (self.length * self.length + self.breadth * self.breadth) ** 0.5 

Now we can continue to access diagonal as an instance variable; whenever we will access diagonal, its value will be calculated and we will get the updated value. So, changes in length and breadth will be reflected in the diagonal.

>>> r = Rectangle(2, 5)

>>> r.diagonal

5.385164807134504

>>> r.length = 10

>>> r.diagonal

11.180339887498949

There is no need to define the setter method, because we do not expect the user to change the diagonal.

 

Deleter method of property

We can also define a deleter method for the property, this deleter method defines what happens when a property is deleted. To create the deleter method, you have to define a method with the same name as the property and add the decorator with the word deleter in it.

class Person:
    def __init__(self, name, age):
        self.name = name
        self.age = age

    @property
    def age(self):
        return self._age

    @age.setter
    def age(self, new_age):
        if 20 <= new_age <= 80:
            self._age = new_age
        else:
            raise ValueError('Age must be between 20 and 80')

    @age.deleter
    def age(self):
        del self._age
        print('age deleted')

    def display(self):
        print(self.name, self._age)

The deleter method will be executed, when the attribute is deleted.

>>> p = Person('Jill', 25)

>>> print(p.age)

25

>>> del p.age

age deleted

 

posted on 2024-07-30 18:55  ZhangZhihuiAAA  阅读(3)  评论(0编辑  收藏  举报