5.6. Reflection

5.6.1. Rationale

  • Act on accessing an attribute

5.6.2. Syntax

Built-in Functions:

  • setattr(obj, 'attribute_name', 'new value') -> None

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

  • getattr(obj, 'attribute_name', 'default value') -> Any

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

Protocol:

  • __setattr__(self, attribute_name, value)

  • __delattr__(self, attribute_name)

  • __getattr__(self, attribute_name, default)

  • __getattribute__(self, attribute_name, default)

5.6.3. Set Attribute

  • Called when trying to set attribute to a value

  • setattr(astro, 'name', 'value') is equivalent to astro.name = 'value'

  • Call Stack:

    • astro.name = 'Mark Watney'

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

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

class Astronaut:

    def __setattr__(self, name, value):
        if name.startswith('_'):
            raise PermissionError(f'Field "{name}" is protected, cannot "set" value.')
        else:
            return super().__setattr__(name, value)


astro = Astronaut()

astro.name = 'Mark Watney'
print(astro.name)
# Mark Watney

astro._salary = 100
# PermissionError: Field "_salary" is protected, cannot "set" value.

5.6.4. Delete Attribute

  • Called when trying to delete attribute

  • delattr(astro, 'name') is equivalent to del astro.name

  • Call stack:

    • del astro.name

    • => delattr(astro, 'name')

    • => astro.__delattr__(name)

class Astronaut:

    def __delattr__(self, name):
        if name.startswith('_'):
            raise PermissionError(f'Field "{name}" is protected, cannot "delete" value.')
        else:
            return super().__delattr__(name)


astro = Astronaut()

astro.name = 'Mark Watney'
astro._salary = 100

del astro.name
del astro._salary
# PermissionError: Field "_salary" is protected, cannot "delete" value.

5.6.5. Get Attribute

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

  • Before even checking if this attribute exists

  • getattr(astro, 'name') is equivalent to astro.name

  • if __getattribute__() raises AttributeError it calls __getattr__()

  • Call stack:

    • astro.name

    • => getattr(astro, 'name')

    • => astro.__getattribute__('name')

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

    • => astro.__getattr__('name')

Listing 5.119. Example __getattribute__()
class Astronaut:

    def __getattribute__(self, name):
        if name.startswith('_'):
            raise PermissionError(f'Field "{name}" is protected, cannot "get" value.')
        else:
            return super().__getattribute__(name)


astro = Astronaut()

astro.name = 'Mark Watney'
print(astro.name)
# Mark Watney

print(astro._salary)
# PermissionError: Field "_salary" is protected, cannot "get" value.

5.6.6. Get Attribute if Does Not Exist

  • 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

  • If __getattribute__() raises AttributeError it calls __getattr__()

Listing 5.120. Example __getattr__()
class Astronaut:
    def __init__(self):
        self.fullname = None

    def __getattr__(self, name):
        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.fullname = None

    def __getattribute__(self, name):
        print('Getattribute called... ')
        result = super().__getattribute__(name)
        print(f'Result was: "{result}"')
        return result

    def __getattr__(self, name):
        print('Not found. Getattr called...')
        print(f'Creating attibute {name} with `None` value')
        super().__setattr__(name, None)


astro = Astronaut()
astro.name = 'Mark Watney'

astro.name
# Getattribute called...
# Result was: "Mark Watney"

astro._salary
# Getattribute called...
# Not found. Getattr called...
# Creating attibute _salary with `None` value

astro._salary
# Getattribute called...
# Result was: "None"

5.6.7. Has Attribute

  • Check if object has attribute

  • There is no __hasattr__() method

  • Calls __getattribute__() and checks if raises AttributeError

class Astronaut:
    def __init__(self, firstname, lastname):
        self.firstname = firstname
        self.lastname = lastname


astro = Astronaut('Mark', 'Watney')

print(hasattr(astro, 'firstname'))     # True
print(hasattr(astro, 'lastname'))      # True
print(hasattr(astro, 'fullname'))      # False

astro.fullname = 'Mark Watney'

print(hasattr(astro, 'fullname'))
# True

5.6.8. Examples

class Astronaut:

    def __getattribute__(self, name):
        if name.startswith('_'):
            raise PermissionError(f'Field "{name}" is protected, cannot "get" value.')
        else:
            return super().__getattribute__(name)

    def __setattr__(self, name, value):
        if name.startswith('_'):
            raise PermissionError(f'Field "{name}" is protected, cannot "set" value.')
        else:
            return super().__setattr__(name, value)


astro = Astronaut()

astro.name = 'Mark Watney'
print(astro.name)
# Mark Watney

astro._salary = 100
# PermissionError: Field "_salary" is protected, cannot "set" value.

print(astro._salary)
# PermissionError: Field "_salary" is protected, cannot "get" value.
class Temperature:
    def __init__(self, kelvin):
        self.kelvin = kelvin

    def __setattr__(self, name, value):
        if value < 0.0:
            raise ValueError('Kelvin temperature cannot be negative')
        else:
            return super().__setattr__(name, value)


t = Temperature(100)

t.kelvin = 20
print(t.kelvin)
# 20

t.kelvin = -10
# ValueError: Kelvin temperature cannot be negative
class Temperature:
    def __init__(self, kelvin):
        self.kelvin = kelvin

    def __setattr__(self, name, value):
        super().__setattr__(name, value)

        if name == 'kelvin':
            self.celsius = 273.15 + self.kelvin
            self.fahrenheit = (self.kelvin-273.15) * 1.8 + 32


t = Temperature(100)

print(t.kelvin)
# 100

print(t.celsius)
# 373.15

print(t.fahrenheit)
# -279.66999999999996

5.6.9. Assignments

5.6.9.1. Protocol Reflection

English
  1. Create class Point with x, y, z attributes

  2. Prevent adding new attributes

  3. Prevent deleting attributes

  4. Prevent changing attributes

  5. Allow to set attributes only at the initialization

  6. All tests must pass

Polish
  1. Stwórz klasę Point z atrybutami x, y, z

  2. Zablokuj możliwość dodawania nowych atrybutów

  3. Zablokuj możliwość usuwania atrybutów

  4. Zablokuj edycję atrybutów

  5. Pozwól na ustawianie atrybutów tylko przy inicjalizacji klasy

  6. Wszystkie testy muszą przejść

Input
class Point:
    """
    >>> pt = Point(1, 2, 3)
    >>> pt.x
    1
    >>> pt.y
    2
    >>> pt.z
    3

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

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

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

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