All Questions From This Chapter « Previously Next »
In Python, the Metaobject Protocol (MOP) refers to the set of hooks and mechanisms that let you control how the language’s object system itself behaves — things like how classes are created, how attributes are accessed, or how instances are instantiated. Instead of treating the object model as fixed, Python exposes many of its internals so you can customize the rules from within your code. The MOP is not a single class or function; it’s the collection of special methods, metaclasses, and protocols that together make the object system programmable.
What a Metaobject Protocol does
A metaobject protocol lets you answer questions like:
“What happens when I access
obj.attr?”“How is a class created when I write
class Foo:?”“Can I change what it means for an object to be callable or iterable?”
By overriding certain magic methods or using metaclasses, you can rewrite the behaviour that the interpreter normally provides behind the scenes.
The core building blocks in Python
1. Metaclasses — controlling class creation
The default class factory is type. When you write
class Dog: pass
Python roughly does:
Dog = type('Dog', (), {})You can replace type with your own metaclass by passing the metaclass keyword:
class MyMeta(type): def __new__(mcs, name, bases, namespace): # Add a class attribute automatically namespace['created_by'] = 'MyMeta' return super().__new__(mcs, name, bases, namespace) class Cat(metaclass=MyMeta): pass print(Cat.created_by) # 'MyMeta'
Metaclasses give you control over:
The namespace of the class body (via
__prepare__)The creation of the class object (
__new__)The initialisation of the class object (
__init__)How instances of the metaclass (i.e., classes) behave when called (
__call__)
Example: a singleton metaclass
class SingletonMeta(type): _instances = {} def __call__(cls, *args, **kwargs): if cls not in cls._instances: cls._instances[cls] = super().__call__(*args, **kwargs) return cls._instances[cls] class Database(metaclass=SingletonMeta): pass db1 = Database() db2 = Database() assert db1 is db2
Here, __call__ intercepts instance creation (Database()), letting us enforce the singleton pattern.
2. Attribute access — controlling obj.attr
Python’s attribute machinery is fully customisable through these methods, which belong to every object (or its class):
__getattr__(self, name)– called only when normal lookup fails.__getattribute__(self, name)– called unconditionally on every attribute read.__setattr__(self, name, value)– called onobj.x = value.__delattr__(self, name)– called ondel obj.x.
Example: logging all attribute access
class Logged: def __getattribute__(self, name): print(f"Accessing {name}") return super().__getattribute__(name) def __setattr__(self, name, value): print(f"Setting {name} = {value}") super().__setattr__(name, value) obj = Logged() obj.x = 10 # prints "Setting x = 10" print(obj.x) # prints "Accessing x" then 10
3. Descriptors — reusable attribute behaviour
Descriptors are objects that define __get__, __set__, or __delete__. They are the magic behind property, staticmethod, classmethod, and many ORM fields.
class PositiveNumber: def __set_name__(self, owner, name): self.public_name = name self.private_name = '_' + name def __get__(self, obj, objtype=None): return getattr(obj, self.private_name, 0) def __set__(self, obj, value): if value < 0: raise ValueError("Must be non-negative") setattr(obj, self.private_name, value) class BankAccount: balance = PositiveNumber() acc = BankAccount() acc.balance = 100 # ok # acc.balance = -50 # raises ValueError
Descriptors let you intercept attribute access at the class level, making the protocol even more granular.
4. Customising instance creation and deletion
Beyond the class‑level __call__, you can also modify:
__new__– allocate a new instance (rarely overridden).__init__– initialise it.__del__– define cleanup when the object is garbage collected.
5. The namespace dictionary and __slots__
Every instance normally has a __dict__ for attributes. You can replace it by defining __slots__, which trades dynamic attributes for memory efficiency. This is another MOP hook: Python changes how attribute storage works based on that declaration.
6. Method resolution order and __mro__
The MRO is computed when a class is created, but you can read cls.__mro__ and even affect it through metaclasses (by passing a custom mro() method). Libraries sometimes do this to inject mixins or enforce traits.
Why the MOP matters
The Metaobject Protocol makes Python incredibly flexible. It enables:
Frameworks like Django – model fields are descriptors; metaclasses turn model classes into SQL tables.
ORM systems – attribute access is transparently converted to database queries.
Mock libraries – easily replace any object behaviour for testing.
Aspect‑Oriented Programming – add logging, locking, or caching to whole families of objects without touching their source code.
Domain‑Specific Languages – create classes that look and behave exactly like your problem domain needs.
A note of caution
With great power comes great complexity. Changing fundamental behaviour can make code harder to read, debug, and maintain. Use the MOP sparingly — when you genuinely need to alter the rules of the language rather than just implementing business logic. Often a simple decorator or composition is a better fit. But when you do need it, Python’s open protocol is one of its most elegant features.
All Questions From This Chapter « Previously Next »

No comments:
Post a Comment