Click here for the first post, which contains the context of this series.
Item #46: Use descriptors for reusable @property methods.
Consider
class GradeBook:
def __init__(self, grade_1=0, grade_2=0):
self._grade_1 = grade_1
self._grade_2 = grade_2
@staticmethod
def is_valid(value):
if not 0 <= value <= 100:
raise ValueError
@property
def grade_1(self):
return self._grade_1
@grade_1.setter
def grade_1(self, value):
self.is_valid(value)
self._grade_1 = value
@property
def grade_2(self):
return self._grade_2
@grade_2.setter
def grade_2(self, value):
self.is_valid(value)
self._grade_2 = value
Adding grade_3, grade_4, ... requires duplicating code, and creating a new class with similar functionality requires also duplicating is_valid.
This can be addressed using a descriptor:
from weakref import WeakKeyDictionary
class Grade:
def __init__(self):
self._values = WeakKeyDictionary()
def __get__(self, instance, instance_type):
return self._values.get(instance, 0)
def __set__(self, instance, value):
if not 0 <= value <= 100:
raise ValueError
self._values[instance] = value
class GradeBook:
grade_1 = Grade()
grade_2 = Grade()
weakref prevents memory leaks.
Item #47: Use __getattr__, __getattribute__, and __setattr__ for lazy attributes.
Note that
class I47:
def __getattr__(self, name):
self.__setattr__(name, None)
return None
i47 = I47()
i47.test # Calls __getattr__.
i47.test # Doesn't call __getattr__.
Also note that
class I47:
def __getattribute__(self, name):
self.__setattr__(name, None)
return None
i47 = I47()
i47.test # Calls __getattribute__.
i47.test # Calls __getattribute__.
There are interesting use cases for these overloads, like logging. As before, be mindful of the use of super() to avoid infinite recursion.
Item #48: Validate subclasses with __init_subclass__.
Consider
class Polygon:
sides = None
def __init_subclass__(cls):
super().__init_subclass__()
if cls.sides is None or cls.sides < 3:
raise ValueError('Polygons must have more than 2 sides.')
class Triangle(Polygon):
sides = 3
print(1)
class Line(Polygon):
print(2)
sides = 2
print(3)
print(4)
This throws an exception after printing 3 but before printing 4.
Although the use of super().__init_subclass__() is unnecessary here, it is recommended in order to handle multiple inheritance with classes that implement __init_subclass__.
This is only one use case of __init_subclass__.
Item #49: Register class existence with __init_subclass__.
Here is another interesting use case of __init_subclass__:
import json
class_registry = {}
def deserialize(data):
params = json.loads(data)
return class_registry[params['class']](*params['args'])
class Serializable:
def __init__(self, *args):
self.args = args
def serialize(self):
return json.dumps({
'class': self.__class__.__name__,
'args': self.args
})
def __init_subclass__(cls):
class_registry[cls.__name__] = cls
class Point3D(Serializable):
def __init__(self, x, y, z):
super().__init__(x, y, z)
self.x = x
self.y = y
self.z = z
For such reasons, keeping a class registry is often useful.
Item #50: Annotate class attributes with __set_name__.
Consider
class Grade:
def __set_name__(self, _, name):
self.name = name
self.protected_name = '_' + name
def __get__(self, instance, _):
return getattr(instance, self.protected_name, 0)
def __set__(self, instance, value):
if not 0 <= value <= 100:
raise ValueError
setattr(instance, self.protected_name, value)
class GradeBook:
grade_1 = Grade()
grade_2 = Grade()
gb = GradeBook()
print(f'{gb.grade_1}, {gb.grade_2}, {gb.__dict__}')
gb.grade_1 = 91
gb.grade_2 = 98
print(f'{gb.grade_1}, {gb.grade_2}, {gb.__dict__}')
Compare this to Item 46.