6.6. Reflection¶
6.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
6.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):
...
6.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)
6.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
6.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
6.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')
raisesAttributeError
=>
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
6.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"
6.6.8. Has Attribute¶
Check if object has attribute
There is no
__hasattr__()
methodCalls
__getattribute__()
and checks if raisesAttributeError
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
6.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
6.6.10. Assignments¶
"""
* 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
"""
* 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
"""
* 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