7.5. Protocol Reflection¶
When accessing an attribute
setattr(obj, 'attrname', 'new_value') -> None
delattr(obj, 'attrname') -> None
getattr(obj, 'attrname', 'default_value') -> Any
hasattr(obj, 'attrname') -> bool
>>> class Point:
... def __init__(self, x, y, z):
... self.x = x
... self.y = y
... self.z = z
...
... def __setattr__(self, attrname, attrvalue):
... if attrname not in {'x', 'y', 'z'}:
... raise NameError('You can set only x, y, z attributes')
... if type(attrvalue) not in (int,float):
... raise TypeError('Attribute value must be int or float')
... if attrvalue < 0:
... raise ValueError('Attribute cannot be less than 0')
... return super().__setattr__(attrname, attrvalue)
>>> pt = Point(1,2,3)
>>>
>>> pt.x = 1
>>>
>>> pt.x = -1
Traceback (most recent call last):
ValueError: Attribute cannot be less than 0
>>>
>>> pt.x = 'one'
Traceback (most recent call last):
TypeError: Attribute value must be int or float
>>>
>>> pt.notexisting = 1
Traceback (most recent call last):
NameError: You can set only x, y, z attributes
>>> 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
>>> def input(prompt):
... return '_salary'
>>>
>>>
>>> attrname = input('Type attribute name: ') # _salary
>>> value = getattr(astro, attrname, 'no such attribute')
>>> print(value)
100
>>> def input(prompt):
... return 'notexisting'
>>>
>>>
>>> attrname = input('Type attribute name: ') # notexisting
>>> value = getattr(astro, attrname, 'no such attribute')
>>> print(value)
no such attribute
7.5.1. 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.5.2. 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.5.3. 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.5.4. 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.5.5. 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
7.5.6. 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.5.7. 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
7.5.8. Use Case - 0x01¶
>>> 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
7.5.9. Use Case - 0x02¶
>>> 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
7.5.10. Use Case - 0x03¶
>>> 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.5.11. Use Case - 0x04¶
>>> class Container:
... def __init__(self, **kwargs: dict) -> None:
... for key, value in kwargs.items():
... setattr(self, key, value)
>>>
>>>
>>> a = Container(firstname='Pan', lastname='Twardowski')
>>> vars(a)
{'firstname': 'Pan', 'lastname': 'Twardowski'}
>>>
>>> b = Container(color='red')
>>> vars(b)
{'color': 'red'}
>>>
>>> c = Container(min=1, max=10)
>>> vars(c)
{'min': 1, 'max': 10}
7.5.12. Assignments¶
"""
* Assignment: Protocol Reflection Delattr
* Complexity: easy
* Lines of code: 2 lines
* Time: 3 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
"""
* Assignment: Protocol Reflection Setattr
* Complexity: easy
* Lines of code: 4 lines
* Time: 5 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)
"""
from dataclasses import dataclass
@dataclass
class Point:
x: int
y: int
z: int
"""
* Assignment: Protocol Reflection Frozen
* Complexity: easy
* Lines of code: 6 lines
* Time: 5 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
"""
from dataclasses import dataclass
@dataclass
class Point:
x: int
y: int
z: int