7.5. Property

7.5.1. Rationale

  • Disable attribute modification

  • Logging value access

  • Check boundary

  • Raise exceptions (TypeError)

  • Check argument type

7.5.2. Problem

class Point:
    x: int

    def get_x(self): pass
    def set_x(self, newvalue): pass
    def del_x(self): pass


pt = Point()
pt.set_x(1)
class Point:
    x: int
    y: int

    def get_x(self): pass
    def set_x(self, newvalue): pass
    def del_x(self): pass
    def get_y(self): pass
    def set_y(self, newvalue): pass
    def del_y(self): pass


pt = Point()
pt.set_x(1)
pt.set_y(1)
class Point:
    x: int
    y: int
    z: int

    def get_x(self) -> int: pass
    def get_x(self): pass
    def set_x(self, newvalue): pass
    def del_x(self): pass
    def get_y(self): pass
    def set_y(self, newvalue): pass
    def del_y(self): pass
    def get_z(self): pass
    def set_z(self, newvalue): pass
    def del_z(self): pass


pt = Point()
pt.set_x(1)
pt.set_y(1)
pt.set_z(1)

7.5.3. What if...

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

    def set_position(self, x, y, z):
        self.x = x
        self.y = y
        self.z = z

pt = Point()
pt.set_position(1, 2, 3)

Works for point. How about astronauts

class Astronaut:
    firstname: str
    middlename: str
    lastname: str

    def set_name(self, name):
        first, mid, last = name.split()
        self.firstname = first
        self.middlename = mid
        self.lastname = last

Do everyone have a middle name? Do everyone have first or lastname?

7.5.4. Solution

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

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

But what if we want to make validation:

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

    def set_x(self, newvalue):
        if newvalue > 0:
            self.x = newvalue
        else:
            raise ValueError

    def set_y(self, newvalue):
        if newvalue > 0:
            self.y = newvalue
        else:
            raise ValueError

    def set_z(self, newvalue):
        if newvalue > 0:
            self.z = newvalue
        else:
            raise ValueError

We can refactor this code:

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

    def _is_valid(self, value):
        if newvalue > 0:
            return value
        else:
            raise ValueError

    def set_x(self, newvalue):
        self.x = self._valid(newvalue)

    def set_y(self, newvalue):
        self.y = self._valid(newvalue)

    def set_z(self, newvalue):
        self.z = self._valid(newvalue)

But problem persist.

What if all parameters can have different ranges:

  • age between 0 and 130

  • height between 150 and 210

  • name first capital letter, then lowercased letters

7.5.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):
        ...

7.5.6. Example

class KelvinTemperature:
    value: float

t = KelvinTemperature()
t.value = -2               # Should raise ValueError('Kelvin cannot be negative')
class KelvinTemperature:
    value: float

    def __init__(self, initialvalue):
        self.value = initialvalue

t = KelvinTemperature(-1)   # Should raise ValueError('Kelvin cannot be negative')
t.value = -2                # Should raise ValueError('Kelvin cannot be negative')
class KelvinTemperature:
    value: float

    def __init__(self, initialvalue):
        if initialvalue < 0:
            raise ValueError('Negative Kelvin Temperature')
        self.value = initialvalue


t = KelvinTemperature()
t.value = -1
class KelvinTemperature:
    _value: float

    def __init__(self, initialvalue):
        self.set_value(initialvalue)

    def set_value(self, newvalue):
        if newvalue < 0:
            raise ValueError('Negative Kelvin Temperature')
        self._value = newvalue
class KelvinTemperature:
    _value: float
    value = property()

    def __init__(self, initialvalue):
        self.value = initialvalue

    @value.setter
    def value(self, newvalue):
        if newvalue < 0:
            raise ValueError('Negative Kelvin Temperature')
        self._value = newvalue

7.5.7. Use Cases

class Astronaut:
    def __init__(self, firstname, lastname):
        self._firstname = firstname
        self._lastname = lastname

    @property
    def name(self):
        return f'{self._firstname} {self._lastname[0]}.'


astro = Astronaut('Mark', 'Watney')
print(astro.name)
# Mark W.
class Astronaut:
    name = property()

    def __init__(self, firstname, lastname):
        self._firstname = firstname
        self._lastname = lastname

    @name.getter
    def name(self):
        return f'{self._firstname} {self._lastname[0]}.'


astro = Astronaut('Mark', 'Watney')
print(astro.name)
# Mark W.
class Temperature:
    kelvin = property()
    __value: float

    def __init__(self, kelvin=None):
        self.__value = kelvin

    @kelvin.setter
    def kelvin(self, newvalue):
        if newvalue < 0:
            raise ValueError('Negative Kelvin Temperature')
        else:
            self.__value = newvalue


t = Temperature()
t.kelvin = 10
t.kelvin = -1
# Traceback (most recent call last):
# ValueError: Negative Kelvin Temperature

7.5.8. Attribute Access

  • Java way: Setter and Getter

  • Pythonic way: Properties, Reflection, Descriptors

Accessing class fields using setter and getter:

class Astronaut:
    def __init__(self, name=None):
        self._name = name

    def set_name(self, name):
        self._name = name

    def get_name(self):
        return self._name


astro = Astronaut()
astro.set_name('Mark Watney')
print(astro.get_name())
# Mark Watney

Accessing class fields. Either put name as an argument for __init__() or create dynamic field in runtime:

class Astronaut:
    def __init__(self, name=None):
        self.name = name


astro = Astronaut()
astro.name = 'Jan Twardowski'
print(astro.name)
# Jan Twardowski

7.5.9. Property class

  • Property's arguments are method pointers get_name, set_name, del_name and a docstring

  • Don't do that

class Astronaut:
    def __init__(self, name=None):
        self._name = name

    def get_name(self):
        return self._name

    def set_name(self, value):
        self._name = value

    def del_name(self):
        del self._name

    name = property(get_name, set_name, del_name, "I am the 'name' property.")

7.5.10. @property Decorator

  • Prefer name = property()

class Astronaut:
    name = property()

    def __init__(self, name=None):
        self._name = name

    @name.getter
    def name(self):
        return self._name

    @name.setter
    def name(self, value):
        self._name = value

    @name.deleter
    def name(self):
        del self._name
class Astronaut:
    def __init__(self, name=None):
        self._name = name

    @property
    def name(self):
        return self._name

    @name.setter
    def name(self, value):
        self._name = value

    @name.deleter
    def name(self):
        del self._name

7.5.11. Use Cases

Astronaut:

class Astronaut:
    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


astro = Astronaut()

astro.set_name('JaN TwARdoWskI')
print(astro.get_name())
# Jan T.

astro.del_name()
print(astro.get_name())
# None
class Astronaut:
    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


astro = Astronaut()

astro.name = 'JAN TwARdoWski'
print(astro.name)
# Jan T.

del astro.name
print(astro.name)
# None

Temperature:

class Temperature:
    def __init__(self, initial_temperature):
        self._protected = initial_temperature

    @property
    def value(self):
        print('You are trying to access a value')
        return self._protected


t = Temperature(100)

print(t.value)
# You are trying to access a value
# 100
class Temperature:
    def __init__(self, initial_temperature):
        self._protected = initial_temperature

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

    @value.setter
    def value(self, new_value):
        if new_value < 0.0:
            raise ValueError('Kelvin Temperature cannot be negative')
        else:
            self._protected = new_value


t = Temperature(100)
t.value = -10
# Traceback (most recent call last):
# ValueError: Kelvin Temperature cannot be negative
class Temperature:
    def __init__(self, initial_temperature):
        self._protected = initial_temperature

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

    @value.deleter
    def value(self):
        print('Resetting temperature')
        self._protected = 0.0


t = Temperature(100)

del t.value
# Resetting temperature

print(t.value)
# 0.0

7.5.12. Assignments

Code 7.20. Solution
"""
* Assignment: Protocol Property Getter
* Complexity: easy
* Lines of code: 4 lines
* Time: 5 min

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

Polish:
    1. Zdefiniuj klasę `Point` z atrybutami `x`, `y`, `z`
    2. Zdefiniuj property `position` w klasie `Point`
    3. Dostęp do `position` zwraca `(x, y, z)`
    4. Uruchom doctesty - wszystkie muszą się powieść

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

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

class Point:
    def __init__(self, x, y, z):
        self.x = x
        self.y = y
        self.z = z


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

English:
    1. Define class `Point` with `x`, `y`, `z` attributes
    2. Define property `position` in class `Point`
    3. 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
    4. Run doctests - all must succeed

Polish:
    1. Zdefiniuj klasę `Point` z atrybutami `x`, `y`, `z`
    2. Zdefiniuj property `position` w klasie `Point`
    3. 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
    4. Uruchom doctesty - wszystkie muszą się powieść

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

    >>> pt = Point(x=1, y=2, z=3)
    >>> pt.x, pt.y, pt.z
    (1, 2, 3)
    >>> pt.position = 4, 5, 6
    >>> pt.x, pt.y, pt.z
    (4, 5, 6)
    >>> pt.position = [7, 8, 9]
    >>> pt.x, pt.y, pt.z
    (7, 8, 9)
    >>> pt.position = 1, 2
    Traceback (most recent call last):
    ValueError
    >>> pt.position = {'a':1, 'b':2}
    Traceback (most recent call last):
    TypeError
"""

class Point:
    def __init__(self, x, y, z):
        self.x = x
        self.y = y
        self.z = z


Code 7.22. Solution
"""
* Assignment: Protocol Property Deleter
* Complexity: easy
* Lines of code: 6 lines
* Time: 5 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(x=1, y=2, z=3)
    >>> pt.x, pt.y, pt.z
    (1, 2, 3)
    >>> del pt.position
    >>> pt.x, pt.y, pt.z
    (0, 0, 0)
"""

class Point:
    def __init__(self, x, y, z):
        self.x = x
        self.y = y
        self.z = z