6 Metaclasses and Attributes
Last edited
Definition
Metaclasses: metaclasses let you intercept Python’s
classstatement and provide special behavior each time a class is defined.
44 Use Plain Attributes Instead of Setter and Getter Methods
In Python you never need to implement explicit setter or getter methods. Instead, you should always start your implementations with simple public attributes.
If you want special behavior on an attribute, migrate to @property decorator and use it’s corresponding .setter() attribute.
This is the pythonic way to use a setter
class VoltageResistance(Resistor):
def __init__(self, ohms):
super().__init__(ohms)
self._voltage = 0
@property
def voltage(self):
return self._voltage
@voltage.setter
def voltage(self, voltage):
self._voltage = voltage
self.current = self._voltage / self.ohmsThis now requires two attributes:
voltage= behavior_voltage= storage
The public attribute is voltage, which the user accesses while inside the class we can muteable _voltage.
This is to force the user to hit the setter, while not endlessly looping on the setter.
The setter method is a great way to perform type checking and validation:
class BoundedResistance(Resistor):
def __init__(self, ohms):
super().__init__(ohms)
@property
def ohms(self):
return self._ohms
@ohms.setter
def ohms(self, ohms):
if ohms <= 0:
raise ValueError(f'ohms must be > 0; got {ohms}')
self._ohms = ohmsHow to use this safely
- Modify only related object state in
@property.settermethods. - Avoid any other side effects that the caller may not expect beyond the object, such as importing modules dynamically, running slow helper functions, doing I/O or making expensive database queries.
- Users of a class will expect its attributes to be like any other Python object: quick and easy. Use normal methods to do anything more complex or slow.
46 Use Descriptors for Reusable @property Methods
This is a kinda niche problem I find that we’re solving.
The problem
Using @property locks your code to the attribute, it can’t be pulled and reused. In the example below I can’t take this logic and reuse it.
@grade.setter
def grade(self, value):
if not (0 <= value <= 100):
raise ValueError(
'Grade must be between 0 and 100')
self._grade = valueSolution
In Python you never need to implement explicit setter or getter methods.
Use a descriptor. A descriptor is a class that defines: __get__ and __set__. Write the valdiation logic once and use instance of the class wherever you need it.
A descriptor is kind off the next level from using @property - kinda..
This example is convoluted, the book goes through iterations showing why each is needed. This is pretty much always needed the descriptor or any class that implements __get__.
from weakref import WeakKeyDictionary
class Grade:
def __init__(self):
self._values = WeakKeyDictionary()
def __get__(self, instance, instance_type):
if instance is None:
return self
return self._values.get(instance, 0)
def __set__(self, instance, value):
if not (0 <= value <= 100):
raise ValueError('Grade must be between 0 and 100')
self._values[instance] = value
class Exam:
math_grade = Grade()
writing_grade = Grade()
science_grade = Grade()
first_exam = Exam()
first_exam.writing_grade = 82
second_exam = Exam()
second_exam.writing_grade = 75
print(f'First {first_exam.writing_grade} is right') # 82
print(f'Second {second_exam.writing_grade} is right') # 7547 Spoofing/Lazy attributes with __getattr__, __getattribute__, and __setattr__
Basically these dunders let you spoof the attribute interface (user.fullname) for get and sets, when you haven’t actually hard-coded the attributes in the class.
I’m not 100% sure where I’d use this still, it does seem super useful somewhere..
Use case: Classes that you don’t know what the attribute names but want a clean attribute interface for.
__getattr__= runs on attribute read, only on a miss__getattribute__= runs on attribute read, always__setattr__= intercept the writes, always
Example
class Row:
def __init__(self, db_data):
self._data = db_data # {'email': '...', 'signup_date': '...', ...}
def __getattr__(self, name): # we didn't have the attribute name, the db did
# We've caught a missed attribute look up because email doesn't exist
# so this logic will run.
return self._data[name]
row = Row()
row.email # xyz@gmail.com48 Validate subclasses with __init_subclass__
You can validate subclasses that they meet your requirements beyond signatures with the __init_subclass__ dunder.
This makes it super easy to know what you’re required to implement to meet an inherited class’ requirements
class BetterPolygon:
sides = None # Must be specified by subclasses
def __init_subclass__(cls):
super().__init_subclass__()
if cls.sides < 3:
raise ValueError('Polygons need 3+ sides')
@classmethod
def interior_angles(cls):
return (cls.sides - 2) * 180
class Hexagon(BetterPolygon):
sides = 6
assert Hexagon.interior_angles() == 72049 Register Classes with __init_subclass__
Using the same dunder as above, we can also auto-register classes when they inherit our class.
This could be useful for a regsitry pattern when you want entire classes registered, not just functions like flask. This could be useful for a plugin pattern.
registry = {}
class Collector:
name = None
def __init_subclass__(cls, **kwargs):
super().__init_subclass__(**kwargs)
registry[cls.name] = cls # fires at class-creation, automatically
def collect(self):
raise NotImplementedError
class SuidCollector(Collector):
name = "suid"
def collect(self):
return {"suid_files": [...]}
class PasswdCollector(Collector):
name = "passwd"
def collect(self):
return {"users": [...]}
# Now everyhting will auto register and we loop through each registered class
results = {
name: cls().collect()
for name, cls in registry.items()
}51 Class decorators over Metaclasses
Definition
Class decorator is a simple function receives a class instance as a param and return a new class or a modified version of the original class.
Useful when you want to amodify every moethod or attribute of a class with minimal boiler plate.
Simpliest example of a class decorator:
def my_class_decorator(klass):
klass.extra_param = 'hello'
return klass
@my_class_decorator
class MyClass:
pass
print(MyClass)
print(MyClass.extra_param) # Hello