4.1. Encapsulation Property

  • Disable attribute modification

  • Logging value access

  • Check boundary

  • Raise exceptions such as ValueError or TypeError

  • Check argument type

In Python you can also define a setter method for a property using the @propertyname.setter decorator. The setter method should have the same name as the property, followed by .setter, and it should take a single parameter that represents the new value of the property.

Here's an example of using the @propertyname.setter decorator to define a read-write property of a class:

>>> class MyClass:
...     x = property()
...
...     @x.getter
...     def x(self):
...         return self._x
...
...     @x.setter
...     def x(self, value):
...         self._x = value
>>>
>>> # Create an instance of MyClass
>>> obj = MyClass()
>>>
>>> # Change the value of the property
>>> obj.x = 1
>>>
>>> # Access the property like an attribute
>>> print(obj.x)
1

In this example, the MyClass class defines a x = property(), private attribute _x.

The @x.getter decorator defines a getter method for the x property. The x() method returns the value of the _x attribute.

The @x.setter decorator defines a setter method for the x property. The x() method takes a single parameter value that represents the new value of the _x attribute.

The obj.x = 1 expression calls the x() setter method to set the value of the _x attribute to 1. The obj.x expression calls the x() getter method to retrieve the new value of the _x attribute.

4.1.1. SetUp

>>> from dataclasses import dataclass
>>> from datetime import date

4.1.2. Setter and Getter Methods

  • Not only Java, but C++ and many others too

>>> class Point:
...     _x: int
...     _y: int
...     _z: int
...
...     def set_x(self, value):
...         self._x = value
...
...     def get_x(self):
...         return self._x
...
...     def set_y(self, value):
...         self._y = value
...
...     def get_y(self):
...         return self._y
...
...     def set_z(self, value):
...         self._z = value
...
...     def get_z(self):
...         return self._z

End-users code:

>>> pt = Point()
>>>
>>> pt.set_x(1)
>>> pt.set_y(2)
>>> pt.set_z(3)
>>>
>>> print(pt.get_x(), pt.get_y(), pt.get_z())
1 2 3

Let's introduce feature, that z-axis cannot be negative (value below 0). Although we cannot change previously defined API (methods). If we change API it will break our end-users code and we don't want that. However we can change our class code without any problem.

Not a big of a deal. We already have placeholders to inject such validation. What was previously considered as an overhead, eventually gave us future proof. Very good!

But this is the Java way...

>>> class Point:
...     _x: int
...     _y: int
...     _z: int
...
...     def set_x(self, value):
...         self._x = value
...
...     def get_x(self):
...         return self._x
...
...     def set_y(self, value):
...         self._y = value
...
...     def get_y(self):
...         return self._y
...
...     def set_z(self, value):
...         if value < 0:
...             raise ValueError('Value cannot be negative')
...         self._z = value
...
...     def get_z(self):
...         return self._z

End-users code is left unchanged:

>>> pt = Point()
>>>
>>> pt.set_x(1)
>>> pt.set_y(2)
>>> pt.set_z(3)
>>>
>>> print(pt.get_x(), pt.get_y(), pt.get_z())
1 2 3

And new feature is working:

>>> pt.set_z(-1)
Traceback (most recent call last):
ValueError: Value cannot be negative

4.1.3. Pythonic Way

>>> class Point:
...     x: int
...     y: int
...     z: int

End-user's code:

>>> pt = Point()
>>> pt.x = 1
>>> pt.y = 2
>>> pt.z = 3
>>> print(pt.x, pt.y, pt.z)
1 2 3

Let's introduce the same feature, that z-axis cannot be negative. And still we cannot change previously defined API (methods).

Woops, we don't have placeholders to inject such validation. We previously considered this as an overhead and we removed it. We are not future proof.

However in Python, you have properties, which is exactly for that reason.

>>> class Point:
...     x: int
...     y: int
...     z = property()
...
...     @z.getter
...     def z(self):
...         return self._z
...
...     @z.setter
...     def z(self, value):
...         if value < 0:
...             raise ValueError('Value cannot be negative')
...         self._z = value

End-users code is left unchanged:

>>> pt = Point()
>>> pt.x = 1
>>> pt.y = 2
>>> pt.z = 3
>>> print(pt.x, pt.y, pt.z)
1 2 3

And new feature is working:

>>> pt.z = -1
Traceback (most recent call last):
ValueError: Value cannot be negative

4.1.4. Encapsulation

Defining setter, getter and deleter methods for each property is not a valid way to do an encapsulation. In Java there is Project Lombok which can generate setters and getters for your fields automatically and you can overwrite only those which need to be overwritten. But, this not a sustainable solution and it requires a 3rd-party library installation as dependency.

Setter and getter way:

>>> class Point:
...     _x: int
...     _y: int
...     _z: int
...
...     def set_x(self, value):
...         self._x = value
...
...     def get_x(self):
...         return self._x
...
...     def set_y(self, value):
...         self._y = value
...
...     def get_y(self):
...         return self._y
...
...     def set_z(self, value):
...         self._z = value
...
...     def get_z(self):
...         return self._z
>>>
>>>
>>> pt = Point()
>>> pt.set_x(1)
>>> pt.set_y(2)
>>> pt.set_z(3)
>>> print(pt.get_x(), pt.get_y(), pt.get_z())
1 2 3

Python properties way:

>>> class Point:
...     x = property()
...     y = property()
...     z = property()
...
...     @x.setter
...     def x(self, value):
...         if value < 0:
...             raise ValueError
...         self._x = value
...
...     @x.getter
...     def x(self):
...         return self._x
...
...     @y.setter
...     def y(self, value):
...         if value < 0:
...             raise ValueError
...         self._y = value
...
...     @y.getter
...     def y(self):
...         return self._y
...
...     @z.setter
...     def z(self, value):
...         if value < 0:
...             raise ValueError
...         self._z = value
...
...     @z.getter
...     def z(self):
...         return self._z
>>>
>>>
>>> pt = Point()
>>> pt.x = 1
>>> pt.y = 2
>>> pt.z = 3
>>> print(pt.x, pt.y, pt.z)
1 2 3

Real encapsulation is about something else. It is about creating an abstraction over your implementation in order to be allowed to change it in future.

>>> class Point:
...     _x: int
...     _y: int
...     _z: int
...
...     def set_position(self, x, y, z):
...         self._x = x
...         self._y = y
...         self._z = z
...
...     def get_position(self):
...         return self._x, self._y, self._z
>>>
>>>
>>> pt = Point()
>>> pt.set_position(1, 2, 3)
>>> print(pt.get_position())
(1, 2, 3)

Now, I can change from 2D to 3D without a big of a hassle.

4.1.5. Protocol

  • myattribute = property() - creates property

  • @myattribute.getter - getter for attribute

  • @myattribute.setter - setter for attribute

  • @myattribute.deleter - deleter for attribute

  • Method name must be the same as attribute name

  • myattribute has to be property

  • @property - creates property and a getter

>>> class MyClass:
...     myattribute = property()
...
...     @myattribute.getter
...     def myattribute(self):
...         return ...
...
...     @myattribute.setter
...     def myattribute(self):
...         ...
...
...     @myattribute.deleter
...     def myattribute(self):
...         ...

4.1.6. Property Descriptor

  • name = property()

>>> class Point:
...     x: int
...     y: int
...     z: int = property()
...
...     @z.getter
...     def z(self):
...         return self._z
...
...     @z.setter
...     def name(self, value):
...         self._z
...
...     @z.deleter
...     def name(self):
...         self._z = None

4.1.7. Property Decorator

  • Typically used when, there is only getter and no setter and deleter methods

>>> class Point:
...     x: int
...     y: int
...     z: int
...
...     @property
...     def z(self):
...         return self._z
...
...     @z.setter
...     def z(self, value):
...         self._z = value
...
...     @z.deleter
...     def z(self):
...         self._z = None

4.1.8. Property Class

  • property() has 4 arguments

  • fset: Callable - setter method

  • fget: Callable - getter method

  • fdel: Callable - deleter method

  • doc: str - docstring

>>> class Point:
...     x: int
...     y: int
...     z: int
...
...     def get_z(self):
...         return self._z
...
...     def set_z(self, value):
...         self._z = value
...
...     def del_z(self):
...         self._z = None
...
...     z = property(del_z, set_z, get_z)

4.1.9. Property Lambda

  • property() has 4 arguments

  • fset: Callable - setter method

  • fget: Callable - getter method

  • fdel: Callable - deleter method

  • doc: str - docstring

>>> class Point:
...     x: int
...     y: int
...     z: int = property(
...         fget=lambda self: getattr(self, '_z'),
...         fset=lambda self, value: setattr(self, '_z', value),
...         fdel=lambda self: setattr(self, '_z', None),
...     )

4.1.10. Use Case - 0x01

>>> class User:
...     def __init__(self):
...         self._name = None
...
...     def set_name(self, name):
...         self._name = name.title()
...
...     def get_name(self):
...         if self._name:
...             firstname, lastname = self._name.split()
...             return f'{firstname} {lastname[0]}.'
...
...     def del_name(self):
...         self._name = None
>>>
>>>
>>> mark = User()
>>>
>>> mark.set_name('MARK WaTNeY')
>>> print(mark.get_name())
Mark W.
>>>
>>> mark.del_name()
>>> print(mark.get_name())
None
>>> class User:
...     name = property()
...
...     def __init__(self):
...         self._name = None
...
...     @name.getter
...     def name(self):
...         if self._name:
...             firstname, lastname = self._name.split()
...             return f'{firstname} {lastname[0]}.'
...
...     @name.setter
...     def name(self, name):
...         self._name = name.title()
...
...     @name.deleter
...     def name(self):
...         self._name = None
>>>
>>>
>>> mark = User()
>>>
>>> mark.name = 'MARK WaTNeY'
>>> print(mark.name)
Mark W.
>>>
>>> del mark.name
>>> print(mark.name)
None

4.1.11. Use Case - 0x02

>>> class User:
...     name = property()
...     _name: str
...
...     def __init__(self, name):
...         self.name = name
...
...     @name.getter
...     def name(self):
...         return self._name
...
...     @name.setter
...     def name(self, new_name):
...         if any(letter in '0123456789' for letter in new_name):
...             raise ValueError('Name cannot have digits')
...         self._name = new_name
...
...     @name.deleter
...     def name(self):
...         self._name = None
>>> mark = User('Mark Watney')
>>> mark.name = 'Melissa Lewis'
>>> mark.name = 'Rick Martinez 1'
Traceback (most recent call last):
ValueError: Name cannot have digits
>>> mark = User('Mark Watney')
>>> mark = User('Rick Martinez 1')
Traceback (most recent call last):
ValueError: Name cannot have digits
>>> mark = User('Mark Watney')
>>> print(f'Name is: {mark.name}')
Name is: Mark Watney
>>>
>>> del mark.name
>>> print(f'Name is: {mark.name}')
Name is: None

4.1.12. Use Case - 0x06

  • Kelvin is an absolute scale (no values below zero)

>>> class KelvinTemperature:
...     value: float
>>>
>>> t = KelvinTemperature()
>>> t.value = -1  # Should raise ValueError('Kelvin cannot be negative')
>>> class KelvinTemperature:
...     value = property()
...
...     @value.setter
...     def value(self, newvalue):
...         if newvalue < 0:
...             raise ValueError('Negative Kelvin Temperature')
...         self._value = newvalue
>>>
>>>
>>> t = KelvinTemperature()

This will pass:

>>> t.value = 1

This will raise an exception:

>>> t.value = -1
Traceback (most recent call last):
ValueError: Negative Kelvin Temperature

4.1.13. Assignments

Code 4.48. Solution
"""
* Assignment: Accessor Property Deleter
* Complexity: easy
* Lines of code: 6 lines
* Time: 3 min

English:
    1. Define class `Point` with `x`, `y`, `z` attributes
    2. Define property `position` in class `Point`
    3. Deleting `position` sets all attributes to 0 (`x=0`, `y=0`, `z=0`)
    4. Run doctests - all must succeed

Polish:
    1. Zdefiniuj klasę `Point` z atrybutami `x`, `y`, `z`
    2. Zdefiniuj property `position` w klasie `Point`
    3. Usunięcie `position` ustawia wszystkie atrybuty na 0 (`x=0`, `y=0`, `z=0`)
    4. Uruchom doctesty - wszystkie muszą się powieść

Tests:
    >>> import sys; sys.tracebacklimit = 0

    >>> pt = Point()
    >>> pt.x = 1
    >>> pt.y = 2
    >>> pt.z = 3

    >>> del pt.position
    >>> assert pt.x == 0
    >>> assert pt.y == 0
    >>> assert pt.z == 0
"""

class Point:
    x: int
    y: int
    z: int

    # Define property `position` in class `Point`
    # Deleting `position` sets all attributes to 0 (`x=0`, `y=0`, `z=0`)
    # type: Callable[[Self], None]
    def position():
        ...


Code 4.49. Solution
"""
* Assignment: Accessor Property Setter
* Complexity: easy
* Lines of code: 9 lines
* Time: 5 min

English:
    1. Define class `Point` with:
        a. Attribute `x: int`
        b. Attribute `y: int`
        c. Attribute `z: int`
        d. Property `position`
    2. Setting `position`:
        a. If argument is not list, tuple, set raise Type Error
        b. If argument has length other than 3, raise Value
        b. Else sets `x`, `y`, `z` attributes from sequence
    3. Run doctests - all must succeed

Polish:
    1. Zdefiniuj klasę `Point` z:
        a. Atrybut `x: int`
        b. Atrybut `y: int`
        c. Atrybut `z: int`
        d. Property `position`
    2. Ustawianie `position`:
        a. Jeżeli argument nie jest list, tuple, set podnieś TypeError
        b. Jeżeli argument nie ma długości 3, podnieś ValueError
        b. W przeciwnym wypadku ustaw kolejne atrybuty `x`, `y`, `z` z sekwencji
    3. Uruchom doctesty - wszystkie muszą się powieść

Tests:
    >>> import sys; sys.tracebacklimit = 0

    >>> pt = Point()

    >>> pt.position = 1, 2, 3
    >>> assert pt.x == 1
    >>> assert pt.y == 2
    >>> assert pt.z == 3

    >>> pt.position = (4, 5, 6)
    >>> assert pt.x == 4
    >>> assert pt.y == 5
    >>> assert pt.z == 6

    >>> pt.position = [7, 8, 9]
    >>> assert pt.x == 7
    >>> assert pt.y == 8
    >>> assert pt.z == 9

    >>> pt.position = {'a':1, 'b':2, 'c':3}
    Traceback (most recent call last):
    TypeError

    >>> pt.position = 1, 2
    Traceback (most recent call last):
    ValueError
"""

from dataclasses import dataclass


# Define class `Point` with `x`, `y`, `z` attributes
# Define property `position` in class `Point`
# Setting `position`:
# - If argument is not list, tuple, set raise Type Error
# - If argument has length other than 3, raise Value
# - Else sets `x`, `y`, `z` attributes from sequence
# type: type[Point]
class Point:
    x: int
    y: int
    z: int


Code 4.50. Solution
"""
* Assignment: Accessor Property NonNegative
* Complexity: easy
* Lines of code: 6 lines
* Time: 5 min

English:
    1. Define class `Point` with:
        a. Attribute `x: int`
        b. Attribute `y: int`
        c. Attribute `z: int`
        d. Property `position`
    3. Setting `position` raises ValueError
       if any value is less than 0
    4. Run doctests - all must succeed

Polish:
    1. Zdefiniuj klasę `Point` z:
        a. Atrybut `x: int`
        b. Atrybut `y: int`
        c. Atrybut `z: int`
        d. Property `position`
    3. Ustawianie `position` podnosi wyjątek,
       jeżeli którakolwiek wartość jest mniejsza od 0
    4. Uruchom doctesty - wszystkie muszą się powieść

Tests:
    >>> import sys; sys.tracebacklimit = 0

    >>> pt = Point()

    >>> pt.position = 1, 2, 3
    >>> assert pt.x == 1
    >>> assert pt.y == 2
    >>> assert pt.z == 3

    >>> pt.position = -1, 2, 3
    Traceback (most recent call last):
    ValueError

    >>> pt.position = 1, -2, 3
    Traceback (most recent call last):
    ValueError

    >>> pt.position = 1, 2, -3
    Traceback (most recent call last):
    ValueError
"""


# Define class `Point` with:
# - Attribute `x: int`
# - Attribute `y: int`
# - Attribute `z: int`
# - Property `position`
# Setting `position` raises ValueError
# if any value is less than 0
# type: type[Point]
class Point:
    x: int
    y: int
    z: int