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 5.81. 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 5.82. 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. Protocol Descriptor Simple

English
  1. Create class KelvinTemperature

  2. Temperature must always be positive

  3. Use descriptors to check boundaries at each value modification

  4. Compare result with "Output" section (see below)

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

  4. Porównaj wyniki z sekcją "Output" (patrz poniżej)

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. Protocol Descriptor Inheritance

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

  2. Model the class GeographicCoordinate

  3. Use descriptors to check value boundaries

  4. Deleting field should set it to None

  5. Disable modification of elevation field

  6. Allow to set elevation field at the class initialization

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

  2. Zamodeluj klasę GeographicCoordinate

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

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

  5. Zablokuj modyfikację pola elevation

  6. 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