7.3. Accessor Reflection

7.3.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.3.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.3.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.3.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.3.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.3.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.3.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.3.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.3.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.3.10. Assignments

Code 7.11. Solution
"""
* Assignment: Accessor 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
"""

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


Code 7.12. Solution
"""
* Assignment: Accessor 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 to modify 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)
"""

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


Code 7.13. Solution
"""
* Assignment: Accessor 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 to define `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
"""

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