18. Descriptor

  • They provide the developer with the ability to add managed attributes to objects
  • The methods needed to create a descriptor are __get__, __set__ and __delete__
  • If you define any of these methods, then you have created a descriptor

18.1. The Descriptor Protocol

  • __get__(self, obj, type=None) -> Any
  • __set__(self, obj, value) -> None
  • __delete__(self, obj) -> None

18.2. Builtin Descriptor Object Examples

  • classmethod
  • staticmethod
  • property
  • functions in general

18.3. Example

Code Listing 18.2. 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>
# }
Code Listing 18.3. 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

18.4. Accessors

18.4.1. __setattr__()

Code Listing 18.4. 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

18.4.2. __getattribute__()

Code Listing 18.5. 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

18.4.3. __delattr__()

Code Listing 18.6. 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')

18.5. Assignments

18.5.1. Longtitude and Latitude

  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

About:
  • Filename: descriptor_geographic.py
  • Lines of code to write: 25 lines
  • Estimated time of completion: 15 min
The whys and wherefores:
 
  • Wykorzystanie deskryptorów
  • Walidacja danych

18.5.2. Temperatura

  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
About:
  • Filename: descriptor_temperature.py
  • Lines of code to write: 25 lines
  • Estimated time of completion: 15 min
The whys and wherefores:
 
  • Wykorzystanie deskryptorów
  • Walidacja danych