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

4.1. The Descriptor Protocol

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

4.2. Builtin Descriptor Object Examples

  • classmethod
  • staticmethod
  • property
  • functions in general

4.3. Example

Code Listing 4.18. 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 4.19. 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

4.4. Accessors

4.4.1. __setattr__()

Code Listing 4.20. 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__()

Code Listing 4.21. 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__()

Code Listing 4.22. 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. Assignments

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

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