Use Cases for Metaclasses in Python
Why do we even need metaclasses? Specially when we have class inheritance
and multiple inheritance
and constructors
to granularly define what we need?
Answer is yes! We need metaclass
when we need to influence how a class is defined. In other words - when we need to mutate
a class into doing something special.
While most Python applications might never need to use metaclasses
, here is a list of use cases I can think off:
Registration
Refactoring or modifying class attributes
Wrapping class methods
Registration of classes
models = {}
class ModelMetaclass(type):
def __new__(cls, name, bases, namespace):
cls = type.__new__(cls, name, bases, namespace)
# No need to register `base` class
if name != "Model":
models[name] = cls
return cls
class Model(metaclass=ModelMetaclass):
pass
Any class subclassing Model
, will be registered in the models
dictionary.
class Foo(Model):
pass
class Bar(Model):
pass
print(models.keys())
['Model', 'Foo', 'Bar']
This can be achieved quite easily using class decorators but, imagine using class decorators with every class. So inheritance makes using metaclass
advantageous.
NOTE
A metaclass
's __new__
function gets a set of parameters -
child class, name of the child class.
bases
is a tuple containing all parent classes.child class's
namespace
which is adict
of all attributes and methods defined in class.
A class's namespace
becomes important when you want to alter or use class's attributes and methods.
Refactoring or modifying class attributes
Take a look at django ORM's ModelBase and its __new__
method. It is a metaclass
which is base class for all django models.
Here is an example of doing something similar:
class ModelMetaclass(type):
def __new__(cls, name, bases, namespace):
fields = {}
for key, value in namespace.items():
if isinstance(value, Field):
value.name = f"{name}.{key}"
fields[key] = value
namespace["_fields"] = fields
return type.__new__(meta, name, bases, namespace)
class Model(metaclass=ModelMetaclass):
pass
This adds a field's dot separated name and also adds a _fields
dictionary to namespace
to keep track of all fields. One can iterate over bases
(base classes) and do the same.
Wrapping class methods
This is useful if you need to execute some code or add extra functionality to class methods. And example would be to track run time a long running method or adding debug information.
Here is an example:
from functools import wraps
import time
class ProfilerMeta(type):
def get_wrapper(func, *args, **kwargs):
@wraps(func)
def runtime_wrapper(*args, **kwargs):
start = time.perf_counter()
rv = func(*args, **kwargs)
stop = time.perf_counter()
runtime = stop - start
print(f"Finished executing {func.__name__} in {runtime} seconds")
return rv
return runtime_wrapper
def __new__(cls, name, bases, namespace):
for name, func in list(namespace.items()):
if callable(func):
namespace[name] = cls.get_wrapper(func)
return type.__new__(cls, name, bases, namespace)
class Foo(metaclass=ProfilerMeta):
def long_running_method(self):
time.sleep(5)
print("Finished long_running_method")
def another_long_running_method(self):
time.sleep(3)
print("Finished Another long_running_method")
foo = Foo()
foo.long_running_method()
foo.another_long_running_method()
Output:
Finished long_running_method
Finished executing long_running_method in 5.004354310003691 seconds
Finished Another long_running_method
Finished executing another_long_running_method in 3.003058955000597 seconds
What is the metaclass
ProfilerMeta
doing here is, iterating over namespace
dict of the child class. It then wraps all functions it can find (long_running_method
and another_long_running_method
in this case). It wraps it with a wrapper function runtime_wrapper
which calculates the runtime
while executing the method.
Conclusion
There is no doubt that using metaclasses
is like adding some magic to Python classes, which might make things a little more complex. But there are cases where you just need it to achieve, singleton design pattern is another use case.
Subscribe to my newsletter
Read articles from Rajesh Pethe directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
Rajesh Pethe
Rajesh Pethe
Passionate software engineer with 17+ years of experience in design and development of full life cycle commercial applications. Functional experience include Financial, Telecom and E-Commerce applications. Primary technical stack includes but not limited to Python, Django, REST, SQL, Perl, Unix/Linux. Secondary technical skills include Java, Angular and React JS. DevOps skills include CiCD, AWS, Docker, Kubernetes and Terraform.