5.7. Protocol Reflection

5.7.1. Rationale

  • When accessing an attribute

  • Built-in Functions:

    • setattr(obj, 'attrname', 'new_value') -> None

    • delattr(obj, 'attrname') -> None

    • getattr(obj, 'attrname', 'default_value') -> Any

    • hasattr(obj, 'attrname') -> bool


>>> class Astronaut:
...     def __init__(self, name):
...         self.name = name
>>>
>>>
>>> astro = Astronaut('Mark Watney')
>>>
>>> if astro._salary is None:
...     astro._salary = 100
Traceback (most recent call last):
AttributeError: 'Astronaut' object has no attribute '_salary'
>>>
>>>
>>> if not hasattr(astro, '_salary'):
...     astro._salary = 100
>>>
>>> print(astro._salary)
100
>>> attrname = input('Type attribute name: ')   # _salary
>>> value = getattr(astro, attrname, 'no such attribute')
>>> print(value)  
100
>>> attrname = input('Type attribute name: ')  # notexisting
>>> value = getattr(astro, attrname, 'no such attribute')
>>> print(value)
no such attribute

5.7.2. Protocol

  • __setattr__(self, attrname, value) -> None

  • __delattr__(self, attrname) -> None

  • __getattribute__(self, attrname, default) -> Any

  • __getattr__(self, attrname, default) -> Any

>>> class Reflection:
...
...     def __setattr__(self, attrname, value):
...         ...
...
...     def __delattr__(self, attrname):
...         ...
...
...     def __getattribute__(self, attrname, default):
...         ...
...
...     def __getattr__(self, attrname, default):
...         ...

5.7.3. Example

>>> class Immutable:
...     def __setattr__(self, attrname, value):
...         raise PermissionError('Immutable')
>>> class Protected:
...     def __setattr__(self, attrname, value):
...         if attrname.startswith('_'):
...             raise PermissionError('Field is protected')
...         else:
...             return super().__setattr__(attrname, value)

5.7.4. Set Attribute

  • Called when trying to set attribute to a value

  • Call Stack:

    • astro.name = 'Mark Watney'

    • => setattr(astro, 'name', 'Mark Watney')

    • => astro.__setattr__('name', 'Mark Watney')

>>> class Astronaut:
...     def __setattr__(self, attrname, value):
...         if attrname.startswith('_'):
...             raise PermissionError('Field is protected')
...         else:
...             return super().__setattr__(attrname, value)
>>>
>>>
>>> astro = Astronaut()
>>>
>>> astro.name = 'Mark Watney'
>>> print(astro.name)
Mark Watney
>>>
>>> astro._salary = 100
Traceback (most recent call last):
PermissionError: Field is protected

5.7.5. Delete Attribute

  • Called when trying to delete attribute

  • Call stack:

    • del astro.name

    • => delattr(astro, 'name')

    • => astro.__delattr__(name)

>>> class Astronaut:
...     def __delattr__(self, attrname):
...         if attrname.startswith('_'):
...             raise PermissionError('Field is protected')
...         else:
...             return super().__delattr__(attrname)
>>>
>>>
>>> astro = Astronaut()
>>>
>>> astro.name = 'Mark Watney'
>>> astro._salary = 100
>>>
>>> del astro.name
>>> del astro._salary
Traceback (most recent call last):
PermissionError: Field is protected

5.7.6. Get Attribute

  • Called for every time, when you want to access any attribute

  • Before even checking if this attribute exists

  • If attribute is not found, then raises AttributeError and calls __getattr__()

  • Call stack:

    • astro.name

    • => getattr(astro, 'name')

    • => astro.__getattribute__('name')

    • if astro.__getattribute__('name') raises AttributeError

    • => astro.__getattr__('name')

>>> class Astronaut:
...     def __getattribute__(self, attrname):
...         if attrname.startswith('_'):
...             raise PermissionError('Field is protected')
...         else:
...             return super().__getattribute__(attrname)
>>>
>>>
>>> astro = Astronaut()
>>>
>>> astro.name = 'Mark Watney'
>>> print(astro.name)
Mark Watney
>>>
>>> print(astro._salary)
Traceback (most recent call last):
PermissionError: Field is protected

5.7.7. Get Attribute if Missing

  • Called whenever you request an attribute that hasn't already been defined

  • It will not execute, when attribute already exist

  • Implementing a fallback for missing attributes

Example __getattr__():

>>> class Astronaut:
...     def __init__(self):
...         self.name = None
...
...     def __getattr__(self, attrname):
...         return 'Sorry, field does not exist'
>>>
>>>
>>> astro = Astronaut()
>>> astro.name = 'Mark Watney'
>>>
>>> print(astro.name)
Mark Watney
>>>
>>> print(astro._salary)
Sorry, field does not exist
>>> class Astronaut:
...     def __init__(self):
...         self.name = None
...
...     def __getattribute__(self, attrname):
...         print('Getattribute called... ')
...         result = super().__getattribute__(attrname)
...         print(f'Result was: "{result}"')
...         return result
...
...     def __getattr__(self, attrname):
...         print('Not found. Getattr called...')
...         print(f'Creating attribute {attrname} with `None` value')
...         super().__setattr__(attrname, None)
>>>
>>>
>>>
>>> astro = Astronaut()
>>> astro.name = 'Mark Watney'
>>>
>>> astro.name
Getattribute called...
Result was: "Mark Watney"
'Mark Watney'
>>>
>>> astro._salary
Getattribute called...
Not found. Getattr called...
Creating attribute _salary with `None` value
>>>
>>> astro._salary
Getattribute called...
Result was: "None"

5.7.8. Has Attribute

  • Check if object has attribute

  • There is no __hasattr__() method

  • Calls __getattribute__() and checks if raises AttributeError

>>> class Astronaut:
...     def __init__(self, name):
...         self.name = name
>>>
>>>
>>> astro = Astronaut('Mark Watney')
>>>
>>> hasattr(astro, 'name')
True
>>>
>>> hasattr(astro, 'mission')
False
>>>
>>> astro.mission = 'Ares3'
>>> hasattr(astro, 'mission')
True

5.7.9. Use Cases

>>> class Astronaut:
...     def __getattribute__(self, attrname):
...         if attrname.startswith('_'):
...             raise PermissionError('Field is protected')
...         else:
...             return super().__getattribute__(attrname)
...
...     def __setattr__(self, attrname, value):
...         if attrname.startswith('_'):
...             raise PermissionError('Field is protected')
...         else:
...             return super().__setattr__(attrname, value)
>>>
>>>
>>> astro = Astronaut()
>>>
>>> astro.name = 'Mark Watney'
>>> print(astro.name)
Mark Watney
>>>
>>> astro._salary = 100
Traceback (most recent call last):
PermissionError: Field is protected
>>>
>>> print(astro._salary)
Traceback (most recent call last):
PermissionError: Field is protected
>>> class Temperature:
...     kelvin: float
...
...     def __init__(self, kelvin):
...         self.kelvin = kelvin
...
...     def __setattr__(self, attrname, value):
...         if attrname == 'kelvin' and value < 0.0:
...             raise ValueError('Kelvin temperature cannot be negative')
...         else:
...             return super().__setattr__(attrname, value)
>>>
>>>
>>> t = Temperature(100)
>>>
>>> t.kelvin = 20
>>> print(t.kelvin)
20
>>>
>>> t.kelvin = -10
Traceback (most recent call last):
ValueError: Kelvin temperature cannot be negative
>>> class Temperature:
...     kelvin: float
...     celsius: float
...     fahrenheit: float
...
...     def __getattr__(self, attrname):
...         if attrname == 'kelvin':
...             return super().__getattribute__('kelvin')
...         if attrname == 'celsius':
...             return self.kelvin - 273.15
...         if attrname == 'fahrenheit':
...             return (self.kelvin-273.15) * 1.8 + 32
>>>
>>>
>>> t = Temperature()
>>> t.kelvin = 373.15
>>>
>>> print(t.kelvin)
373.15
>>> print(t.celsius)
100.0
>>> print(t.fahrenheit)
212.0
>>> class Container:
...     def __init__(self, **kwargs: dict) -> None:
...         for key, value in kwargs.items():
...             setattr(self, key, value)
>>>
>>>
>>> a = Container(firstname='Jan', lastname='Twardowski')
>>> vars(a)
{'firstname': 'Jan', 'lastname': 'Twardowski'}
>>>
>>> b = Container(color='red')
>>> vars(b)
{'color': 'red'}
>>>
>>> c = Container(min=1, max=10)
>>> vars(c)
{'min': 1, 'max': 10}

5.7.10. Assignments

Code 5.10. Solution
"""
* Assignment: Protocol Reflection Delattr
* Complexity: easy
* Lines of code: 2 lines
* Time: 5 min

English:
    1. Create class `Point` with `x`, `y`, `z` attributes
    2. Prevent deleting attributes
    3. Run doctests - all must succeed

Polish:
    1. Stwórz klasę `Point` z atrybutami `x`, `y`, `z`
    2. Zablokuj usuwanie atrybutów
    3. Uruchom doctesty - wszystkie muszą się powieść

Tests:
    >>> import sys; sys.tracebacklimit = 0

    >>> pt = Point(1, 2, 3)
    >>> pt.x, pt.y, pt.z
    (1, 2, 3)

    >>> del pt.x
    Traceback (most recent call last):
    PermissionError: Cannot delete attributes

    >>> del pt.notexisting
    Traceback (most recent call last):
    PermissionError: Cannot delete attributes
"""

from dataclasses import dataclass


@dataclass
class Point:
    x: int
    y: int
    z: int


Code 5.11. Solution
"""
* Assignment: Protocol Reflection Setattr
* Complexity: easy
* Lines of code: 4 lines
* Time: 8 min

English:
    1. Create class `Point` with `x`, `y`, `z` attributes
    2. Prevent creation of new attributes
    3. Allow modifying values of `x`, `y`, `z`
    4. Run doctests - all must succeed

Polish:
    1. Stwórz klasę `Point` z atrybutami `x`, `y`, `z`
    2. Zablokuj tworzenie nowych atrybutów
    3. Zezwól na modyfikowanie wartości `x`, `y`, `z`
    4. Uruchom doctesty - wszystkie muszą się powieść

Tests:
    >>> import sys; sys.tracebacklimit = 0

    >>> pt = Point(1, 2, 3)
    >>> pt.x, pt.y, pt.z
    (1, 2, 3)
    >>> pt.notexisting = 10
    Traceback (most recent call last):
    PermissionError: Cannot set other attributes than x,y,z
    >>> pt.x = 10
    >>> pt.y = 20
    >>> pt.z = 30
    >>> pt.x, pt.y, pt.z
    (10, 20, 30)
"""

from dataclasses import dataclass


@dataclass
class Point:
    x: int
    y: int
    z: int


Code 5.12. Solution
"""
* Assignment: Protocol Reflection Frozen
* Complexity: easy
* Lines of code: 6 lines
* Time: 13 min

English:
    1. Create class `Point` with `x`, `y`, `z` attributes
    2. Prevent creation of new attributes
    3. Allow defining `x`, `y`, `z` but only at the initialization
    4. Prevent later modification of `x`, `y`, `z`
    5. Run doctests - all must succeed

Polish:
    1. Stwórz klasę `Point` z atrybutami `x`, `y`, `z`
    2. Zablokuj tworzenie nowych atrybutów
    3. Pozwól na zdefiniowanie `x`, `y`, `z` ale tylko przy inicjalizacji
    4. Zablokuj późniejsze modyfikacje `x`, `y`, `z`
    5. Uruchom doctesty - wszystkie muszą się powieść

Tests:
    >>> import sys; sys.tracebacklimit = 0

    >>> pt = Point(1, 2, 3)
    >>> pt.x, pt.y, pt.z
    (1, 2, 3)

    >>> pt.notexisting = 10
    Traceback (most recent call last):
    PermissionError: Cannot set other attributes than x,y,z

    >>> pt.x = 10
    Traceback (most recent call last):
    PermissionError: Cannot modify existing attributes

    >>> pt.y = 20
    Traceback (most recent call last):
    PermissionError: Cannot modify existing attributes

    >>> pt.z = 30
    Traceback (most recent call last):
    PermissionError: Cannot modify existing attributes
"""

from dataclasses import dataclass


@dataclass
class Point:
    x: int
    y: int
    z: int