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.