Understanding Metaclasses in Python: A Deep Dive
Written on
Python is inherently an object-oriented programming language, where it is often stated that everything in Python is an object. To grasp Python effectively, one must delve into the foundational elements such as objects and classes to comprehend the underlying mechanics.
This article focuses on the idea of metaprogramming within Python, examining how we can manipulate classes and their instantiation. Metaprogramming can be broadly described as code that modifies other code.
An object is essentially a concrete instance of a class. Given that everything in Python is an object, a pertinent question arises: are classes also objects? And if they are, what is their means of instantiation? Let’s explore this further.
Classes and Objects
First, we need to clarify the connection between classes and objects.
Consider this simple class:
class Person:
pass
We create an empty class devoid of attributes and methods. When we instantiate this class as the object kasper, we can determine its class using the type function.
The output will reveal:
<class '__main__.Person'>
This indicates that the object kasper is an instance of the Person class, defined in the same module where we queried the type of kasper.
Since everything in Python is an object, we can apply the same logic to built-in types:
type('Medium')
This will yield:
<class 'str'>
Thus, the string 'Medium' is an instance of the str class.
Metaclasses
What, then, is the type of a class? Let’s investigate this.
When we check the type of a class, the output is:
<class 'type'>
Fascinatingly, the type of a class is type. This is because type serves as a metaclass, which instantiates classes in the same manner that classes instantiate objects.
But how can this be utilized? Since a metaclass creates a class through specific steps, it can be advantageous to manipulate this process and develop a custom metaclass that instantiates classes differently than type does. This idea of altering behavior by modifying the code prior to execution shares similarities with decorators.
Decorators in Python allow us to manipulate functions before they are called, and as we will see shortly, both concepts have much in common.
Class Instantiations
To understand metaclasses, we must first grasp how a class is constructed by the default metaclass, type.
When a class is instantiated, Python collects some variables, such as the class name and its base classes, and essentially forms a dictionary by invoking a special method on type called __prepare__. The body of the class is then executed to populate that dictionary with attributes and methods. Finally, the name, base classes, and the created dictionary are passed to the type class:
Person = type('Person', (Base1, Base2), cls_dict)
Now, Person is a class inheriting from Base1 and Base2, with attributes and methods specified in cls_dict.
To recreate the empty class called Person dynamically, we could do it as follows:
Person = type('Person', (), {})
This is equivalent to defining an empty class using the pass keyword.
Creating Custom Metaclasses
Let’s consider the following example:
class Custom(type):
def __new__(cls, name, bases, cls_dict):
methods = [key for key, value in cls_dict.items() if callable(value)]
if len(methods) > 2:
raise TypeError('More than two methods')return super().__new__(cls, name, bases, cls_dict)
In this example, we create a custom metaclass called Custom, overriding the type's special method __new__. Inside __new__, we gather the methods from the provided dictionary to count them and ensure there are no more than two methods in the class. If there are, an error is thrown before the class is instantiated.
When this is done, we simply call type's __new__ method to obtain the class and return it.
When we execute:
class Dog(metaclass=Custom):
def bark(self):
print('WOOOF!')
It works fine. However, if we attempt to run:
class Cat(metaclass=Custom):
def meow(self):
print('MEOW!')def purr(self):
print('PURR!')
It will crash with the message:
TypeError: More than two methods
Metaclasses and Decorators
Suppose we have several classes that inherit from the same base class, and we wish to implement automatic debugging for all their methods while adhering to the DRY (Don’t Repeat Yourself) principle. What’s the best approach here?
Naturally, decorators come to mind. The concept of decorators, which enables us to perform various actions before and after a function or class execution, can serve as an excellent debugging tool and a significant DRY optimizer.
Let’s create a custom debugger to count the parameters passed to the methods of a class. This is a simple example, but it will serve its purpose here.
The output of this code will show:
arguments has 3 arguments
16
The print statement in the wrapper function is executed before the arguments function is invoked.
Note that the @ syntax is merely syntactic sugar; behind the scenes, the function is passed as a parameter to the decorator when it’s called.
If we want this decorator applied to each method in a class, we could apply it manually to each one. However, to avoid redundancy, there’s a more efficient approach.
Consider this custom class decorator:
def debug_class(cls):
for key, value in cls.__dict__.items():
if callable(value):
setattr(cls, key, debug_function(value))return cls
Now, with this decorator, we can define a class with it on top to debug all methods (excluding class and static methods) in one go.
The output will show:
walk has 1 arguments
miaaw has 1 arguments
This is excellent, but recall that we have numerous classes inheriting from the same base class. Can we leverage this inheritance to reduce code duplication?
Yes, indeed! We could utilize a custom metaclass.
Assuming the common base class for all our classes is called Base, we could create a metaclass that Base can inherit from:
class DebugMeta(type):
def __new__(cls, name, bases, cls_dict):
for key, value in cls_dict.items():
if callable(value):
cls_dict[key] = debug_function(value)return super().__new__(cls, name, bases, cls_dict)
All classes that inherit from Base will now have their methods debugged using the debug_function decorator.
When Should I Use a Metaclass?
Metaclasses are seldom employed in practice because various other solutions often address the same issues that might lead one to consider using them. For example, decorators provide a generally powerful tool that is usually sufficient.
Nevertheless, understanding the internal mechanics of Python is crucial for grasping what occurs behind the scenes and knowing when to apply the appropriate tools for specific tasks.
Now that you're acquainted with metaclasses and their appropriate use cases, remember that if you have numerous classes sharing a common base class, a metaclass can ensure that functionalities are inherited throughout the entire hierarchy.
A useful guideline is this: consider employing metaclasses when you need to alter how a class is instantiated. Decorators, in contrast, do not achieve this because the class is instantiated before any decorator manipulations occur.
I trust you found this article informative. As always, feel free to reach out on LinkedIn with any questions, comments, or concerns.
Kasper Müller - Senior Consultant, Data and Analytics, FS, Technology Consulting - EY | LinkedIn
Programming, mathematics and teaching are among my primary interests. Data science, machine learning, and more.
www.linkedin.com