7. Descriptor

7.1. Protocol

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

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

  • __delete__(self, parent) -> None

7.2. Rationale

  • Add managed attributes to objects

  • Outsource functionality into specialized classes

7.3. Builtin Descriptor Object Examples

  • classmethod

  • staticmethod

  • property

  • functions in general

7.4. Example

7.4.1. Outside class

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


class Temperature:
    def __init__(self):
        self._current_value = 0.0
        self.kelvin = Kelvin()


t = Temperature()

t.kelvin = 10        # Will trigger ``Kelvin.__set__()``
print(t.kelvin)      # Will trigger ``Kelvin.__get__()``
del t.kelvin         # Will trigger ``Kelvin.__delete__()``

7.4.2. Inside class

class Temperature:
    def __init__(self):
        self._current_value = 0.0
        self.kelvin = Temperature.Kelvin()

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



t = Temperature()

t.kelvin = 10        # Will trigger ``Kelvin.__set__()``
print(t.kelvin)      # Will trigger ``Kelvin.__get__()``
del t.kelvin         # Will trigger ``Kelvin.__delete__()``

7.5. Case Study

7.5.1. Temperature Conversion

class Temperature:
    def __init__(self):
        self._current_value = 0.0
        self.kelvin = Temperature.Kelvin()
        self.celsius = Temperature.Celsius()
        self.fahrenheit = Temperature.Fahrenheit()

    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)


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

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

7.6. Assignments

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

7.6.2. Geographic Coordinates

  • Complexity level: medium

  • Lines of code to write: 25 lines

  • Estimated time of completion: 15 min

  • Filename: 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 poniżej) 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