5.7. Descriptor

5.7.1. Rationale

  • Add managed attributes to objects

  • Outsource functionality into specialized classes

5.7.2. Protocol

  • __get__(self, parent, parent_type=None) -> self

  • __set__(self, parent, new_value) -> None

  • __delete__(self, parent) -> None

5.7.3. Builtin Descriptors

  • @classmethod

  • @staticmethod

  • @property

  • functions in general

5.7.4. Syntax

Listing 547. Outside class
class Kelvin:
    def __get__(self, parent, parent_type):
        print('Calling Kelvin.__get__()')
        return round(parent._current_value, 2)

    def __set__(self, parent, new_value):
        print('Calling Kelvin.__set__()')
        parent._current_value = new_value

    def __delete__(self, parent):
        print('Calling Kelvin.__del__()')
        parent._current_value = 0.0


class Temperature:
    kelvin = Kelvin()

    def __init__(self):
        self._current_value = 0.0


t = Temperature()

t.kelvin = 10
# Calling Kelvin.__set__()

print(t.kelvin)
# Calling Kelvin.__get__()
# 10

del t.kelvin
# Calling Kelvin.__del__()
Listing 548. Inside class
class Temperature:

    class Kelvin:
        def __get__(self, parent, parent_type):
            print('Calling Kelvin.__get__()')
            return round(parent._current_value, 2)

        def __set__(self, parent, new_value):
            print('Calling Kelvin.__set__()')
            parent._current_value = new_value

        def __delete__(self, parent):
            print('Calling Kelvin.__del__()')
            parent._current_value = 0.0

    kelvin = Kelvin()

    def __init__(self):
        self._current_value = 0.0


t = Temperature()

t.kelvin = 10
# Calling Kelvin.__set__()

print(t.kelvin)
# Calling Kelvin.__get__()
# 10

del t.kelvin
# Calling Kelvin.__del__()

5.7.5. Examples

5.7.5.1. Temperature Conversion

class Kelvin:
    def __get__(self, parent, parent_type):
        return round(parent._current_value, 2)

    def __set__(self, parent, new_value):
        parent._current_value = new_value

    def __delete__(self, parent):
        parent._current_value = 0


class Celsius:
    def __get__(self, parent, parent_type):
        temp = parent._current_value - 273.15
        return round(temp, 2)

    def __set__(self, parent, new_value):
        temp = new_value + 273.15
        parent._current_value = temp

    def __delete__(self, parent):
        self.__set__(parent, 0)


class Fahrenheit:
    def __get__(self, parent, parent_type):
        temp = (parent._current_value - 273.15) * 9 / 5 + 32
        return round(temp, 2)

    def __set__(self, parent, fahrenheit):
        temp = (fahrenheit - 32) * 5 / 9 + 273.15
        parent._current_value = temp

    def __delete__(self, parent):
        self.__set__(parent, 0)


class Temperature:
    kelvin = Kelvin()
    celsius = Celsius()
    fahrenheit = Fahrenheit()

    def __init__(self):
        self._current_value = 0.0


t = Temperature()

t.kelvin = 273.15
print(f'K: {t.kelvin}')         # 273.15
print(f'C: {t.celsius}')        # 0.0
print(f'F: {t.fahrenheit}')     # 32.0

print()

t.fahrenheit = 100
print(f'K: {t.kelvin}')         # 310.93
print(f'C: {t.celsius}')        # 37.78
print(f'F: {t.fahrenheit}')     # 100.0

print()

t.celsius = 100
print(f'K: {t.kelvin}')         # 373.15
print(f'C: {t.celsius}')        # 100.0
print(f'F: {t.fahrenheit}')     # 212.0

print()

del t.celsius
print(f'K: {t.kelvin}')         # 273.15
print(f'C: {t.celsius}')        # 0.0
print(f'F: {t.fahrenheit}')     # 32.0

print()

del t.fahrenheit
print(f'K: {t.kelvin}')         # 255.37
print(f'C: {t.celsius}')        # -17.78
print(f'F: {t.fahrenheit}')     # 0

5.7.5.2. Timezone Conversion

from dataclasses import dataclass
from datetime import datetime
from pytz import timezone


class Timezone:
    def __init__(self, name):
        self.timezone = timezone(name)

    def __get__(self, parent, *args, **kwargs):
        """
        Converts absolute time to desired timezone.
        """
        return parent.utc.astimezone(self.timezone)

    def __set__(self, parent, new_datetime):
        """
        First localize timezone naive datetime,
        this will add information about timezone,
        next convert to UTC (shift time by UTC offset).
        """
        local_time = self.timezone.localize(new_datetime)
        parent.utc = local_time.astimezone(timezone('UTC'))

    def __delete__(self, parent):
        """
        Set to the not existent date
        """
        parent.utc = datetime(1, 1, 1)


@dataclass
class Time:
    utc = datetime.now(tz=timezone('UTC'))
    warsaw = Timezone('Europe/Warsaw')
    moscow = Timezone('Europe/Moscow')
    est = Timezone('America/New_York')
    pdt = Timezone('America/Los_Angeles')


t = Time()

t.warsaw = datetime(1969, 7, 21, 3, 56, 15)
print(t.utc)        # 1969-07-21 02:56:15+00:00
print(t.moscow)     # 1969-07-21 05:56:15+03:00
print(t.est)        # 1969-07-20 22:56:15-04:00
print(t.pdt)        # 1969-07-20 19:56:15-07:00

5.7.6. Assignments

5.7.6.1. Temperature

English
  1. Create class KelvinTemperature

  2. Temperature must always be positive

  3. Use descriptors to check boundaries at each value modification

Polish
  1. Stwórz klasę KelvinTemperature

  2. Temperatura musi być zawsze być dodatnia

  3. Użyj deskryptorów do sprawdzania wartości granicznych przy każdej modyfikacji

Output
t = KelvinTemperature()

t.value = 1
print(t.value)
# 1

t.value = -1
# ValueError: Negative temperature
The whys and wherefores
  • Using descriptors

  • Data validation

5.7.6.2. Geographic Coordinates

  • Complexity level: medium

  • Lines of code to write: 25 lines

  • Estimated time of completion: 15 min

  • Solution: solution/descriptor_gps.py

English
  1. From input data (see below) model the class GeographicCoordinate

  2. Use descriptors to check value boundaries

  3. Deleting field should set it to None

  4. Disable modification of elevation field

  5. Allow to set elevation field at the class initialization

Polish
  1. Na podstawie danych wejściowych (patrz sekcja input) zamodeluj klasę GeographicCoordinate

  2. Użyj deskryptory do sprawdzania wartości brzegowych

  3. Kasowanie pola powinno ustawiać jego wartość na None

  4. Zablokuj modyfikację pola elevation

  5. Zezwól na ustawianie pola elevation podczas inicjalizacji

Input Data
latitude - type: float, min: -90, max 90
longitude - type: float, min: -180, max: 180
elevation - type: float, min: -10994, max: 8848
The whys and wherefores
  • Using descriptors

  • Data validation