5.5. Properties

5.5.1. Setter and Getter Methods

  • Java way

  • don't do that in Python

Listing 5.108. 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
Listing 5.109. Problem with setters and getters
class MyClass:
    def __init__(self):
        self._x = None
        self._y = None

    def get_x(self):
        return self._x

    def set_x(self, value):
        self._x = value

    def del_x(self):
        del self._x

    def get_y(self):
        return self._y

    def set_y(self, value):
        self._x = value

    def del_y(self):
        del self._y
Listing 5.110. Rationale for Setters and Getters
class Astronaut:
    def __init__(self):
        self._name = None

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

    def get_name(self):
        firstname, lastname = self._name.split()
        return f'{firstname} {lastname[0]}.'


astro = Astronaut()

astro.set_name('JaN TwARdoWskI')
print(astro.get_name())
# Jan T.
Listing 5.111. Rationale for Setters and Getters HabitatOS Z-Wave sensor admin
from django.contrib import admin
from habitat._common.admin import HabitatAdmin
from habitat.sensors.models import ZWaveSensor


@admin.register(ZWaveSensor)
class ZWaveSensorAdmin(HabitatAdmin):
    change_list_template = 'sensors/change_list_charts.html'
    list_display = ['mission_date', 'mission_time', 'type', 'device', 'value', 'unit']
    list_filter = ['created', 'type', 'unit', 'device']
    search_fields = ['^date', 'device']
    ordering = ['-datetime']

    def get_list_display(self, request):
        list_display = self.list_display

        if request.user.is_superuser:
            list_display = ['earth_datetime'] + list_display

        return list_display

5.5.2. Direct Attribute Access

  • Pythonic way

Listing 5.112. 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

5.5.3. Properties

  • @property - for defining getters

  • @value.getter - defining getter for field (require field to be @property)

  • @value.setter - defining setter for field (require field to be @property)

  • @value.deleter - defining deleter for field (require field to be @property)

5.5.3.1. Rationale

  • Disable attribute modification

  • Logging value access

  • Check boundary

  • Raise exceptions (TypeError)

  • Check argument type

Listing 5.113. Property class
property()
# <property object at 0x10ff07940>

property().getter
# <built-in method getter of property object at 0x10ff07998>

property().setter
# <built-in method setter of property object at 0x10ff07940>

property().deleter
# <built-in method deleter of property object at 0x10ff07998>

5.5.3.2. Property decorator

  • @decorator syntax is just syntactic sugar; the syntax:

    class MyClass:
    
        @property
        def attribute(self):
            return self._attribute
    
  • really means the same thing as

    class MyClass:
    
        def attribute(self):
            return self._attribute
    
        attribute = property(attribute)
    

5.5.3.3. Creating properties with property class

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

Listing 5.114. Properties
class Astronaut:
    def __init__(self):
        self._protected = None

    def get_name(self):
        return self._protected

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

    def del_name(self):
        del self._protected

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

5.5.3.4. Creating properties with @property decorator

class Astronaut:
    name = property()

    def __init__(self):
        self._protected = None

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

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

    @name.deleter
    def name(self):
        del self._protected
Listing 5.115. Properties as a decorators
class Astronaut:
    def __init__(self):
        self._protected = None

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

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

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

5.5.4. Use Cases

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

5.5.5. Examples

Listing 5.116. Using @property as a getter
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
Listing 5.117. @x.setter
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
# ValueError: Kelvin Temperature cannot be negative

5.5.5.1. Deleter

  • @value.deleter - for defining deleter for field value

  • Require value to be @property

Listing 5.118. @x.deleter
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

5.5.6. Assignments

5.5.6.1. Protocol Property

English
  1. Use data from "Input" section (see below)

  2. Create class Point with x, y, z attributes

  3. Add position property which returns tuple (x, y, z)

  4. Deleting position sets all attributes to 0 (x=0, y=0, z=0)

  5. Prevent setting position

  6. All tests must pass

Polish
  1. Użyj danych z sekcji "Input" (patrz poniżej)

  2. Stwórz klasę Point z atrybutami x, y, z

  3. Dodaj property position, który zwraca tuple (x, y, z)

  4. Usunięcie position ustawia wszystkie atrybuty na 0 (x=0, y=0, z=0)

  5. Zablokuj edycję atrybutów

  6. Wszystkie testy muszą przejść

Input
class Point:
    """
    >>> pt = Point(x=1, y=2, z=3)
    >>> pt.position
    (1, 2, 3)
    >>> del pt.position
    >>> pt.position
    (0, 0, 0)
    >>> pt.position = (4, 5, 6)
    Traceback (most recent call last):
        ...
    PermissionError: Cannot modify values
    """