7.6. Reflection

7.6.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: ')
value = getattr(astro, attrname, 'no such attribute')
print(value)

# Type attribute name: >? name
# Mark Watney

# Type attribute name: >? _salary
# 100

# Type attribute name: >? notexisting
# no such attribute

7.6.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):
        ...

7.6.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)

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

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

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

7.6.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"

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

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

7.6.10. Assignments

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

English:
    1. Use data from "Given" section (see below)
    2. Create class `Point` with `x`, `y`, `z` attributes
    3. Prevent deleting attributes
    4. Compare result with "Tests" section (see below)

Polish:
    1. Użyj danych z sekcji "Given" (patrz poniżej)
    2. Stwórz klasę `Point` z atrybutami `x`, `y`, `z`
    3. Zablokuj usuwanie atrybutów
    4. Porównaj wyniki z sekcją "Tests" (patrz poniżej)

Tests:
    >>> 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
"""


# Given
class Point:
    def __init__(self, x, y, z):
        self.x = x
        self.y = y
        self.z = z


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

English:
    1. Use data from "Given" section (see below)
    2. Create class `Point` with `x`, `y`, `z` attributes
    3. Prevent creation of new attributes
    4. Allow to modify values of `x`, `y`, `z`
    5. Compare result with "Tests" section (see below)

Polish:
    1. Użyj danych z sekcji "Given" (patrz poniżej)
    2. Stwórz klasę `Point` z atrybutami `x`, `y`, `z`
    3. Zablokuj tworzenie nowych atrybutów
    4. Zezwól na modyfikowanie wartości `x`, `y`, `z`
    5. Porównaj wyniki z sekcją "Tests" (patrz poniżej)

Tests:
    >>> 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)
"""


# Given
class Point:
    def __init__(self, x, y, z):
        self.x = x
        self.y = y
        self.z = z


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

English:
    1. Use data from "Given" section (see below)
    2. Create class `Point` with `x`, `y`, `z` attributes
    3. Prevent creation of new attributes
    4. Allow to define `x`, `y`, `z` but only at the initialization
    5. Prevent later modification of `x`, `y`, `z`
    6. Compare result with "Tests" section (see below)

Polish:
    1. Użyj danych z sekcji "Given" (patrz poniżej)
    2. Stwórz klasę `Point` z atrybutami `x`, `y`, `z`
    3. Zablokuj tworzenie nowych atrybutów
    4. Pozwól na zdefiniowanie `x`, `y`, `z` ale tylko przy inicjalizacji
    5. Zablokuj późniejsze modyfikacje `x`, `y`, `z`
    6. Porównaj wyniki z sekcją "Tests" (patrz poniżej)

Tests:
    >>> 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
"""


# Given
class Point:
    def __init__(self, x, y, z):
        self.x = x
        self.y = y
        self.z = z