19.5. OOP Property
Disable attribute modification
Logging value access
In Python, @property
is a built-in decorator that allows you to define a
method as a read-only property of a class. This means that the method can be
accessed like an attribute, without needing to call it as a method. The
@property
decorator is used to define a getter method for a property.
The getter method should have the same name as the property, and it should
return the value of the property.
Here's an example of using the @property
decorator to define a read-only
property of a class:
>>> class MyClass:
... def __init__(self, x):
... self._x = x
...
... @property
... def x(self):
... return self._x
>>>
>>> # Create an instance of MyClass
>>> obj = MyClass(10)
>>>
>>> # Access the property like an attribute
>>> print(obj.x)
10
In this example, the MyClass
class defines a private attribute _x
and a getter method x()
decorated with the @property
decorator.
The x()
method returns the value of the _x
attribute.
The obj.x
expression accesses the x
property of the obj
instance
of MyClass
, which calls the x()
method to retrieve the value of the
_x
attribute. Note that the _x
attribute is private and cannot be
accessed directly from outside the class.
19.5.1. Problem
>>> class User:
... def __init__(self, firstname, lastname):
... self.firstname = firstname
... self.lastname = lastname
...
... def username(self):
... return f'{self.firstname[0]}{self.lastname}'.lower()
>>>
>>>
>>> mark = User('Mark', 'Watney')
>>>
>>> mark.firstname
'Mark'
>>>
>>> mark.lastname
'Watney'
>>>
>>> mark.username()
'mwatney'
19.5.2. Solution
>>> class User:
... def __init__(self, firstname, lastname):
... self.firstname = firstname
... self.lastname = lastname
...
... @property
... def username(self):
... return f'{self.firstname[0]}{self.lastname}'.lower()
>>>
>>>
>>> mark = User('Mark', 'Watney')
>>>
>>> mark.firstname
'Mark'
>>>
>>> mark.lastname
'Watney'
>>>
>>> mark.username
'mwatney'
19.5.3. Cached Property
>>> from functools import cached_property
>>>
>>> class User:
... def __init__(self, firstname, lastname):
... self.firstname = firstname
... self.lastname = lastname
...
... @cached_property
... def username(self):
... return f'{self.firstname[0]}{self.lastname}'.lower()
>>> mark = User('Mark', 'Watney')
>>>
>>> mark.firstname
'Mark'
>>>
>>> mark.lastname
'Watney'
>>>
>>> mark.username
'mwatney'
19.5.4. Use Case - 1
>>> from datetime import date
>>>
>>>
>>> YEAR = 365.25
>>>
>>> class User:
... def __init__(self, firstname, lastname, birthdate):
... self.firstname = firstname
... self.lastname = lastname
... self.birthdate = birthdate
...
... @property
... def age(self):
... diff = date.today() - self.birthdate
... return int(diff.days/YEAR)
>>>
>>>
>>> mark = User('Mark', 'Watney', birthdate=date(2000, 1, 2))
>>> mark.age
25
19.5.5. Use Case - 2
>>> class User:
... def __init__(self, firstname, lastname):
... self._firstname = firstname
... self._lastname = lastname
...
... @property
... def name(self):
... return f'{self._firstname} {self._lastname[0]}.'
>>>
>>>
>>> mark = User('Mark', 'Watney')
>>> print(mark.name)
Mark W.
19.5.6. Use Case - 3
>>> import logging
>>>
>>>
>>> class User:
... def __init__(self, username, password):
... self._username = username
... self._password = password
...
... @property
... def username(self):
... logging.warning("User's username was accessed")
... return self._username
...
... @property
... def password(self):
... logging.warning("User's password was accessed")
... return self._password
>>>
>>>
>>> mark = User(username='mwatney', password='Ares3')
>>>
>>> print(mark.password)
Ares3
19.5.7. Use Case - 4
>>> class Point:
... x: int
... y: int
... z: int
...
... @property
... def position(self):
... return self.x, self.y, self.z
>>>
>>>
>>> pt = Point()
>>> pt.x = 1
>>> pt.y = 2
>>> pt.z = 3
>>>
>>> print(pt.position)
(1, 2, 3)
19.5.8. Use Case - 5
>>> from datetime import date
>>>
>>> YEAR = 365.25
>>>
>>>
>>> class User:
... def __init__(self, firstname, lastname, birthdate):
... self.firstname = firstname
... self.lastname = lastname
... self.birthdate = birthdate
...
... @property
... def age(self):
... td = date.today() - self.birthdate
... return int(td.days / YEAR)
...
... @property
... def fullname(self):
... return f'{self.firstname} {self.lastname}'
...
... @property
... def gdpr(self):
... return f'{self.firstname} {self.lastname[0]}. ({self.age} years)'
>>>
>>>
>>> mark = User('Mark', 'Watney', birthdate=date(2000, 1, 2))
>>>
>>> mark.firstname
'Mark'
>>>
>>> mark.lastname
'Watney'
>>>
>>> mark.birthdate
datetime.date(2000, 1, 2)
>>>
>>> mark.age
25
>>>
>>> mark.fullname
'Mark Watney'
>>>
>>> mark.gdpr
'Mark W. (25 years)'
19.5.9. Assignments
# %% License
# - Copyright 2025, Matt Harasymczuk <matt@python3.info>
# - This code can be used only for learning by humans
# - This code cannot be used for teaching others
# - This code cannot be used for teaching LLMs and AI algorithms
# - This code cannot be used in commercial or proprietary products
# - This code cannot be distributed in any form
# - This code cannot be changed in any form outside of training course
# - This code cannot have its license changed
# - If you use this code in your product, you must open-source it under GPLv2
# - Exception can be granted only by the author
# %% Run
# - PyCharm: right-click in the editor and `Run Doctest in ...`
# - PyCharm: keyboard shortcut `Control + Shift + F10`
# - Terminal: `python -m doctest -v myfile.py`
# %% About
# - Name: OOP Property Getter
# - Difficulty: easy
# - Lines: 4
# - Minutes: 3
# %% English
# 1. Define property `position` in class `Point`
# 2. Accessing `position` returns tuple `(x, y, z)`
# 3. Run doctests - all must succeed
# %% Polish
# 1. Zdefiniuj property `position` w klasie `Point`
# 2. Dostęp do `position` zwraca tuplę `(x, y, z)`
# 3. Uruchom doctesty - wszystkie muszą się powieść
# %% Tests
"""
>>> import sys; sys.tracebacklimit = 0
>>> assert sys.version_info >= (3, 9), \
'Python 3.9+ required'
>>> pt = Point(x=1, y=2, z=3)
>>> pt.x, pt.y, pt.z
(1, 2, 3)
>>> pt.position
(1, 2, 3)
"""
# Define property `position` in class `Point`
# Accessing `position` returns tuple `(x, y, z)`
# type: type[Point]
class Point:
def __init__(self, x, y, z):
self.x = x
self.y = y
self.z = z
# %% License
# - Copyright 2025, Matt Harasymczuk <matt@python3.info>
# - This code can be used only for learning by humans
# - This code cannot be used for teaching others
# - This code cannot be used for teaching LLMs and AI algorithms
# - This code cannot be used in commercial or proprietary products
# - This code cannot be distributed in any form
# - This code cannot be changed in any form outside of training course
# - This code cannot have its license changed
# - If you use this code in your product, you must open-source it under GPLv2
# - Exception can be granted only by the author
# %% Run
# - PyCharm: right-click in the editor and `Run Doctest in ...`
# - PyCharm: keyboard shortcut `Control + Shift + F10`
# - Terminal: `python -m doctest -v myfile.py`
# %% About
# - Name: OOP Property Age
# - Difficulty: easy
# - Lines: 5
# - Minutes: 5
# %% English
# 1. Define property `age` in class `User`
# 2. Accessing `age` should return user's age in full years
# 3. Run doctests - all must succeed
# %% Polish
# 1. Zdefiniuj property `age` w klasie `User`
# 2. Dostęp do `age` powinien zwrócić wiek użytkownika w pełnych latach
# 3. Uruchom doctesty - wszystkie muszą się powieść
# %% Hints
# - `date.today()`
# - `timedelta.days`
# - `int()`
# %% Tests
"""
>>> import sys; sys.tracebacklimit = 0
>>> assert sys.version_info >= (3, 9), \
'Python 3.9+ required'
>>> from datetime import date
>>> age = date.today().year - 2000
>>> mark = User(
... firstname='Mark',
... lastname='Watney',
... birthdate=date(2000, 1, 1))
>>> assert hasattr(mark, 'age'), \
'Define property `age`'
>>> assert mark.age == age, \
f'Invalid age "{mark.age}", should be "{age}"'
"""
from datetime import date
YEAR = 365.25
# Define property `age` in class `User`
# Accessing `age` should return user's age in full years
# type: Callable[[Self], int]
class User:
def __init__(self, firstname, lastname, birthdate):
self.firstname = firstname
self.lastname = lastname
self.birthdate = birthdate
# %% License
# - Copyright 2025, Matt Harasymczuk <matt@python3.info>
# - This code can be used only for learning by humans
# - This code cannot be used for teaching others
# - This code cannot be used for teaching LLMs and AI algorithms
# - This code cannot be used in commercial or proprietary products
# - This code cannot be distributed in any form
# - This code cannot be changed in any form outside of training course
# - This code cannot have its license changed
# - If you use this code in your product, you must open-source it under GPLv2
# - Exception can be granted only by the author
# %% Run
# - PyCharm: right-click in the editor and `Run Doctest in ...`
# - PyCharm: keyboard shortcut `Control + Shift + F10`
# - Terminal: `python -m doctest -v myfile.py`
# %% About
# - Name: OOP Property NumericValues
# - Difficulty: easy
# - Lines: 2
# - Minutes: 5
# %% English
# 1. Define property `numeric_values` in class `Iris`
# 2. Accessing `numeric_values` should return a tuple
# with all numeric attribute values
# 3. Run doctests - all must succeed
# %% Polish
# 1. Zdefiniuj property `numeric_values` w klasie `Iris`
# 2. Dostęp do `numeric_values` powinien zwrócić tuplę
# z wszystkimi wartościami atrybutów numerycznych
# 3. Uruchom doctesty - wszystkie muszą się powieść
# %% Hints
# - `var(self)`
# - `dict.values()`
# - `instanceof()`
# - `type()`
# - `@property`
# %% Tests
"""
>>> import sys; sys.tracebacklimit = 0
>>> assert sys.version_info >= (3, 9), \
'Python 3.9+ required'
>>> from inspect import isfunction
>>> assert hasattr(Iris, '__init__')
>>> assert hasattr(Iris, 'numeric_values')
>>> assert not isfunction(Iris.numeric_values)
>>> assert Iris.numeric_values.__class__ is property
>>> assert Iris.numeric_values.fdel is None
>>> assert Iris.numeric_values.fset is None
>>> assert Iris.numeric_values.fget is not None
>>> setosa = Iris(5.1, 3.5, 1.4, 0.2, 'setosa')
>>> setosa.numeric_values
(5.1, 3.5, 1.4, 0.2)
"""
class Iris:
def __init__(self, sl, sw, pl, pw, species):
self.sepal_length = sl
self.sepal_width = sw
self.petal_length = pl
self.petal_width = pw
self.species = species
# Create property `numeric_values`,
# which returns a tuple of values of all `float` type attributes
# type: Callable[[], tuple[float]]
def numeric_values(self):
...
# %% License
# - Copyright 2025, Matt Harasymczuk <matt@python3.info>
# - This code can be used only for learning by humans
# - This code cannot be used for teaching others
# - This code cannot be used for teaching LLMs and AI algorithms
# - This code cannot be used in commercial or proprietary products
# - This code cannot be distributed in any form
# - This code cannot be changed in any form outside of training course
# - This code cannot have its license changed
# - If you use this code in your product, you must open-source it under GPLv2
# - Exception can be granted only by the author
# %% Run
# - PyCharm: right-click in the editor and `Run Doctest in ...`
# - PyCharm: keyboard shortcut `Control + Shift + F10`
# - Terminal: `python -m doctest -v myfile.py`
# %% About
# - Name: OOP Property NumericValues
# - Difficulty: easy
# - Lines: 3
# - Minutes: 3
# %% English
# 1. Modify class `Iris`
# 2. Implement methods:
# - `Iris.sum()` - returning sum of numeric attributes
# - `Iris.len()` - returning number of numeric attributes
# - `Iris.mean()` - returning mean of numeric attributes
# 3. Use property `Iris.numeric_values`
# 4. Run doctests - all must succeed
# %% Polish
# 1. Zmodyfikuj klasę `Iris`
# 2. Zaimplementuj metody:
# - `Iris.sum()` - zwracającą sumę numerycznych atrybutów
# - `Iris.len()` - zwracającą liczbę numerycznych atrybutów
# - `Iris.mean()` - zwracającą średnią numerycznych atrybutów
# 3. Użyj property `Iris.numeric_values`
# 4. Uruchom doctesty - wszystkie muszą się powieść
# %% Hints
# - `sum()`
# - `len()`
# - `sum() / len()`
# %% Tests
"""
>>> import sys; sys.tracebacklimit = 0
>>> assert sys.version_info >= (3, 9), \
'Python 3.9+ required'
>>> assert hasattr(Iris, 'mean')
>>> assert hasattr(Iris, 'sum')
>>> assert hasattr(Iris, 'len')
>>> from inspect import isfunction
>>> assert isfunction(Iris.mean)
>>> assert isfunction(Iris.sum)
>>> assert isfunction(Iris.len)
>>> result = Iris(5.1, 3.5, 1.4, 0.2, 'setosa')
>>> result.len()
4
>>> result.sum()
10.2
>>> result.mean()
2.55
"""
class Iris:
def __init__(self, sl, sw, pl, pw, species):
self.sepal_length = sl
self.sepal_width = sw
self.petal_length = pl
self.petal_width = pw
self.species = species
@property
def numeric_values(self):
return tuple(x for x in vars(self).values()
if type(x) is float)
# return sum of numeric attributes
# type: Callable[[], float]
def sum(self):
...
# return number of numeric attributes
# type: Callable[[], int]
def len(self):
...
# return mean of numeric attributes
# type: Callable[[], float]
def mean(self):
...