29. Properties ‐ OOP - MantsSk/CA_PTUA14 GitHub Wiki

Introduction

In object-oriented programming, each attribute of a class may have three basic methods:

  • A getter method to get its value.
  • A setter method to set its value.
  • A deleter method to delete it.

In Python, properties are a way to control access to class attributes. The @property decorator can be used to define a getter method, which retrieves the value of an attribute, and the @propertyname.setter decorator can be used to define a setter method, which sets the value of an attribute. This allows for more control over the behavior of attributes, while still maintaining a simple, easy-to-use interface for the class user.

Here's an example of how to use the @property decorator to define a getter method:

class Rectangle:
    def __init__(self, width, height):
        self.width = width
        self.height = height

    @property
    def area(self):
        return self.width * self.height

r = Rectangle(3.0, 4.0)
print(r.area)  # output: 12.0

In this example, we define a class Rectangle with two attributes, width and height. We then use the @property decorator to define a getter method called area, which calculates the area of the rectangle by multiplying the width and height attributes. We can then use r.area to retrieve the area of the rectangle, without having to call a separate method.

How to Use the @property and @property.setter decorators?

Let’s implement the Student class like this (notice that this time, I set attribute as protected, unlike last time):

class Student:
    def __init__(self):
        self._score = 0

    @property
    def score(self):
        return self._score

    @score.setter
    def score(self, s):
        if 0 <= s <= 100:
            self._score = s
        else:
            raise ValueError('The score must be between 0 ~ 100!')

    @score.deleter
    def score(self):
        del self._score

Tom = Student()
Tom.score = 999
# ValueError: The score must be between 0 ~ 100!

Using @property.deleter is a way to encapsulate the logic around deleting an attribute, ensuring that all necessary actions are taken when an attribute is removed from an object.

class MyClass:
    def __init__(self, value):
        self._value = value

    @property
    def value(self):
        return self._value

    @value.deleter
    def value(self):
        # Perform some checks or logging
        print("Value is being deleted")
        del self._value


# Create an instance of MyClass
obj = MyClass(10)

# Access the value (calls the getter)
print(obj.value)  # Output: 10

# Delete the value (calls the deleter)
del obj.value
# Output: "Value is being deleted"

As the above code shown, after using the @property decorator, we can both set the value directly and ensure the validity of it. The property decorator is a very useful and widely used mechanism in Python classes. It helps us call the getter, setter and deleter methods directly by the attribute.

A common template for using the property decorator is:

class C(object):

    @property
    def x(self):
        return self._x

    @x.setter
    def x(self, value):
        self._x = value

    @x.deleter
    def x(self):
        del self._x

Note: The names of getter, setter and deleter methods should be the same. The best choice is to use the attribute’s name.

Define a Read-only Attribute

As we known, there are three methods (getter, setter and deleter) we can define by the property decorator. If one of the three methods is not defined, we cannot use this method. Therefore, we can skillfully use the property decorator to define a read-only attribute. For example:

class Student:
    def __init__(self):
        self._score = 0

    @property
    def score(self):
        return self._score

    @score.deleter
    def score(self):
        del self._score



Tom= Student()
Tom.score = 99
# AttributeError: can't set attribute

As the above example shown, we don’t define the setter method of score , so score can’t be set. In other words, it’s a read-only attribute. This is an important usage scenario of the property decorator.

You can also use properties in initializer. This can be useful for validation or transformation of initial values passed to the object.

Use setter in initializer

class Product:
    def __init__(self, price):
        self.price = price  # Using the price setter property in the initializer

    @property
    def price(self):
        return self._price

    @price.setter
    def price(self, value):
        if value < 0:
            raise ValueError("Price cannot be negative")
        self._price = value

# Usage
try:
    product = Product(-50)
except ValueError as e:
    print(e)  # This will raise "Price cannot be negative"

product = Product(50)
print(product.price)  # This will work fine, output: 50

Classic GET/SET methods and why its not that Pythonic

class Product:
    def __init__(self, price):
        self.set_price(price)

    def get_price(self):
        return self._price

    def set_price(self, value):
        if value < 0:
            raise ValueError("Price cannot be negative")
        self._price = value

# Usage
try:
    product = Product(-50)
except ValueError as e:
    print(e)  # Outputs: Price cannot be negative

product = Product(50)
print(product.get_price())  # Outputs: 50

**Why I would argue that properties are better: **

  • Pythonic Syntax: The @property decorator is more in line with Python's philosophy of simple, readable, and elegant code. It allows attributes to be accessed and modified with straightforward syntax (product.price), similar to accessing regular attributes.
  • Encapsulation: While both methods provide encapsulation, the @property approach does so with a cleaner interface. The internal representation of the attribute (_price) is hidden, and interaction is done through the property interface.
  • Ease of Maintenance: With @property, it's easier to maintain and modify the code. For example, if later you decide to add additional logic to the getter or setter, you can do so without changing how the attribute is accessed in other parts of your code.
  • Consistent Interface: Using properties keeps the interface consistent. Users of your class can always interact with price as if it were a regular attribute, even when complex logic is performed in the background.

Exercises:

1st task:

Create a "ReadOnlyProperty" class with a single read-only property "value". Once the property is set during initialization, it cannot be altered. Example:

# Example Usage
read_only = ReadOnlyProperty("This is read-only")

read_only.value = "Whats going on?" # property 'value' of 'ReadOnlyProperty' object has no setter

2nd task (together):

class Temperature:
    def __init__(self, celsius):
        self._celsius = celsius

    @property
    def celsius(self):
        return self._celsius

    @celsius.setter
    def celsius(self, value):
        self._celsius = value

    @property
    def fahrenheit(self):
        return (self.celsius * 1.8) + 32

t = Temperature(20)
t.celsius = 20
print(t.celsius)
print(t.fahrenheit)

3rd task (Optional Homework):

  • Write a class "Student" with attributes "name" and "credits". The name can be any name, and credits can range from 0 to 30. Write a method for the class that prints the student's name and credits. The method should only perform information printing (no other logic).

  • The name and credits should be assignable during initialization:

  • s = Student("Mantas", -5)

  • If credits are assigned as 0 or less (e.g., -66), set them to 0.

  • If credits are assigned as 30 or more (e.g., 66), set them to 30.

  • Use @property and @property.setter decorators.

🌐 Extra reading (or watching 📺 ):