4. Descriptor

4.1. The Descriptor Protocol

  • They provide the developer with the ability to add managed attributes to objects

  • __get__(self, obj, type=None) -> self

  • __set__(self, obj, value) -> None

  • __delete__(self, obj) -> None

4.2. Builtin Descriptor Object Examples

  • classmethod

  • staticmethod

  • property

  • functions in general

4.3. Example

Listing 4.24. Example
class Kelvin:
    def __init__(self, value=None):
        self.value = value

    def __get__(self, parent, owner):
        print(locals())
        return self.value

    def __set__(self, parent, value):
        print(locals())
        self.value = value

    def __delete__(self, parent):
        print(locals())
        self.value = None


class Temperature:
    kelvin = Kelvin()


temp = Temperature()


temp.kelvin = 10
# will trigger __set__(), which prints:
# {
#   'self': <__main__.Kelvin object at 0x11b9f3470>,
#   'parent': <__main__.Temperature object at 0x11b9f34a8>,
#   'value': 10
# }

print(temp.kelvin)
# will trigger __get__(), which prints:
# {
#   'self': <__main__.Kelvin object at 0x11b9f3470>,
#   'parent': <__main__.Temperature object at 0x11b9f34a8>,
#   'owner': <class '__main__.Temperature'>
# }

del temp.kelvin
# will trigger __delete__()
# {
#   'self': <__main__.Kelvin object at 0x11b9f3470>,
#   'parent': <__main__.Temperature object at 0x11b9f34a8>
# }
Listing 4.25. Example
class Celsius:
    def __get__(self, parent, owner):
        celsius = parent.kelvin - 273.15
        return round(celsius, 2)

    def __set__(self, parent, celsius):
        kelvin = celsius + 273.15
        parent.kelvin = round(kelvin, 2)

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


class Fahrenheit:
    def __get__(self, parent, owner):
        celsius = parent.kelvin - 273.15
        fahrenheit = celsius * 9/5 + 32
        return round(fahrenheit, 2)

    def __set__(self, parent, fahrenheit):
        celsius = (fahrenheit - 32) * 5/9
        kelvin = celsius + 273.15
        parent.kelvin = round(kelvin, 2)

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


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


temp = Temperature()

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

print()

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

print()

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

print()

del temp.celsius
print(f'K: {temp.kelvin}')      # 0
print(f'C: {temp.celsius}')     # -273.15
print(f'F: {temp.fahrenheit}')  # -459.67
Listing 4.26. Example Timezones
from datetime import datetime
from pytz import timezone, utc


class TimeConverter:
    def __get__(self, parent, type):
        return parent.utc.astimezone(self.tz)

    def __set__(self, parent, value):
        parent.utc = self.tz.localize(value).astimezone(utc)

    def __delete__(self, parent):
        parent.utc = datetime(1, 1, 1)


class EuropeWarsaw(TimeConverter):
    tz = timezone('Europe/Warsaw')


class EuropeMoscow(TimeConverter):
    tz = timezone('Europe/Moscow')


class Time:
    warsaw = EuropeWarsaw()
    moscow = EuropeMoscow()

    def __init__(self, dt=datetime.now(tz=utc)):
        self.utc = dt


now = Time()

print(now.warsaw)
# 2019-03-28 13:07:14.486365+01:00

now.warsaw = datetime(2019, 3, 28, 13, 00, 00)

print(now.utc)
# 2019-03-28 12:00:00+00:00

print(now.moscow)
# 2019-03-28 15:00:00+03:00

4.4. Accessors

4.4.1. __setattr__()

Listing 4.27. Example __setattr__()
class Kelvin:
    def __init__(self, initial_temperature):
        self.temperature = initial_temperature

    def __setattr__(self, name, new_value):
        if name == 'value' and new_value < 0.0:
            raise ValueError('Temperature cannot be negative')
        else:
            super().__setattr__(name, new_value)


temp = Kelvin(273)

temp.value = 20
print(temp.value)  # 20

temp.value = -10
print(temp.value)  # ValueError: Temperature cannot be negative

4.4.2. __getattribute__()

Listing 4.28. Example __getattribute__()
class Kelvin:
    def __init__(self, temperature):
        self.temperature = temperature

    def __getattribute__(self, name):
        if name == 'value':
            raise ValueError('Field is private, cannot display')
        else:
            super().__getattribute__(name)


temp = Kelvin(273)

temp.value = 20
print(temp.value)  # ValueError: Field is private, cannot display

4.4.3. __delattr__()

Listing 4.29. Example __delattr__()
class Point:
    x = 10
    y = -5
    z = 0

    def __delattr__(self, name):
        if name == 'z':
            raise ValueError('Cannot delete field')
        else:
            super().__delattr__(name)

p = Point()

del p.y
delattr(p, 'z')

4.5. setter, getter, deleter

4.5.1. @property

  • @property - for defining getters

  • Przykład użycia:

    • Blokowanie możliwości edycji pola klasy

    • Dodawanie logowania przy ustawianiu wartości

Listing 4.30. Using @property as a getter
class Temperature:
    def __init__(self, kelvin: float = 0.0):
        self.kelvin = kelvin

    @property
    def celsius(self):
        temp = self.kelvin - 273.15
        return round(temp, 2)


temp = Temperature(kelvin=309.75)

print(temp.celsius)
# 36.6

4.5.2. @x.setter

  • @x.setter - for defining setter for field x

  • Require field to be @property

Listing 4.31. @x.setter
class Temperature:
    def __init__(self, kelvin: float = 0.0):
        self.kelvin = kelvin

    @property
    def celsius(self):
        temp = self.kelvin - 273.15
        return round(temp, 2)

    @celsius.setter
    def celsius(self, value):
        if value < -273.15:
            raise ValueError('Temperature below -273.15 is not possible')
        else:
            self.kelvin = value + 273.15

temp = Temperature()

print(temp.kelvin)
# 0.0

temp.celsius = 36.60
print(temp.kelvin)
# 309.75

temp.celsius = -1000
# ValueError: Temperature below -273.15 is not possible

4.5.3. @x.deleter

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

  • Require field to be @property

Listing 4.32. @x.deleter
class Temperature:
    def __init__(self, kelvin: float = 0.0):
        self.kelvin = kelvin

    @property
    def celsius(self):
        temp = self.kelvin - 273.15
        return round(temp, 2)

    @celsius.deleter
    def celsius(self):
        self.kelvin = 0.0

temp = Temperature(kelvin=100)

print(temp.celsius)
# -173.15

del temp.celsius

print(temp.celsius)
# -273.15

4.6. Assignments

4.6.1. Longitude and Latitude

  • Filename: descriptor_geographic.py

  • Lines of code to write: 25 lines

  • Estimated time of completion: 15 min

  1. Stwórz klasę GeographicCoordinate

  2. Klasa ma mieć pola:

    • latitude - min: -180.0; max: 180.0

    • longitude - min: -90.0; max 90.0

    • elevation - min: -10,994; max: 8,848 m

  3. Wykorzystując deskryptory dodaj mechanizm sprawdzania wartości

  4. Przy kasowaniu (del) wartości, nie usuwaj jej, a ustaw na None

  5. Zablokuj całkowicie modyfikację pola elevation

The whys and wherefores
  • Wykorzystanie deskryptorów

  • Walidacja danych

4.6.2. Temperatura

  • Filename: descriptor_temperature.py

  • Lines of code to write: 25 lines

  • Estimated time of completion: 15 min

  1. Stwórz klasę KelvinTemperature

  2. Temperatura musi być dodatnia, sprawdzaj to przy zapisie do pola value

  3. Usunięcie temperatury nie usunie wartości, ale ustawi ją na None

The whys and wherefores
  • Wykorzystanie deskryptorów

  • Walidacja danych