6 Metaclasses and Attributes

Last edited

Definition

Metaclasses: metaclasses let you intercept Python’s class statement 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.ohms

This 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 = ohms

How to use this safely

  • Modify only related object state in @property.setter methods.
  • 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 = value

Solution

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')  # 75

47 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.com

48 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() == 720

49 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