Metaprogramming is a programming technique in which computer programs have the ability to modify their own or other programs’ codes. It may sound like an idea from a science fiction story, but the idea was born and implemented in the early 1960s.
For Python, code modifications can occure while the code is being executed, and you might have already experienced it while implementing decorators, overriding operators, or even implementing the properties protocol.
It may look like syntactic sugar, because in many cases metaprogramming allows programmers to minimize the number of lines of code to express a solution, in turn reducing development time.
But the truth is that this technique could be used for tool preparation; those tools could be applied to your code to make it follow specific programming patterns, or to help you create a coherent API (Application Programming Interface).
Another example of metaprogramming is the metaclass concept, which is one of the most advanced concepts presented in this course.
Tim Peters, the Python guru who authored the Zen of Python, expressed his feelings about metaclasses in the comp.lang.python newsgroup on 12/22/2002:
[metaclasses] are deeper magic than 99% of users should ever worry about. If you wonder whether you need them, you don't (the people who actually need them know with certainty that they need them, and don't need an explanation about why).
Don’t worry, we'll touch on the “deeper magic” in a benign way. Understanding Python metaclasses is worthwhile, because it leads to a better understanding of what is happening under Python's hood when classes are created.
In Python, a metaclass is a class whose instances are classes. Just as an ordinary class defines the behavior of certain objects, a metaclass allows for the customization of class instantiation.
The functionality of the metaclass partly coincides with that of class decorators, but metaclasses act in a different way than decorators:
Metaclasses usually enter the game when we program advanced modules or frameworks, where a lot of precise automation must be provided.
The typical use cases for metaclasses:
In Python's approach, everything is an object, and every object has some type associated with it. To get the type of any object, make use of the type() function.
Run the code in the right pane to see the type() function in action.
class Dog: pass age = 10 codes = [33, 92] dog = Dog() print(type(age)) print(type(codes)) print(type(dog)) print(type(Dog))
<class 'int'> <class 'list'> <class '__main__.Dog'> <class 'type'>
We can see that objects in Python are defined by their inherent classes.
The example also shows that we can create our own classes, and those classes will be instances of the type special class, which is the default metaclass responsible for creating classes.
Let's perform one more experiment that will respond to the question: what type of objects are built-in classes and the metaclass type?
for t in (int, list, type): print(type(t))
The results are quite interesting:
<class 'type'> <class 'type'> <class 'type'>
These observations lead us to the following conclusions:
type is type – no, that is not a typo.To extend the above observations, it’s important to add:
type is a class that generates classes defined by a programmer;type class.Before we start creating our own metaclasses, it’s important to understand some more details regarding classes and the process of creating them.
We should get familiar with some special attributes:
__name__ – inherent for classes; contains the name of the class;__class__ – inherent for both classes and instances; contains information about the class to which a class instance belongs;__bases__ – inherent for classes; it’s a tuple and contains information about the base classes of a class;__dict__ – inherent for both classes and instances; contains a dictionary (or other type mapping object) of the object's attributes.class Dog: pass dog = Dog() print('"dog" is an object of class named:', Dog.__name__) print() print('class "Dog" is an instance of:', Dog.__class__) print('instance "dog" is an instance of:', dog.__class__) print() print('class "Dog" is ', Dog.__bases__) print() print('class "Dog" attributes:', Dog.__dict__) print('object "dog" attributes:', dog.__dict__)
The output of the code presented in the right pane:
"dog" is an object of class named: Dog
class "Dog" is an instance of: <class 'type'>
instance "dog" is an instance of: <class '__main__.Dog'>
class "Dog" is (<class 'object'>,)
class "Dog" attributes: {'__module__': '__main__', '__dict__': <attribute '__dict__' of 'Dog' objects>, '__weakref__': <attribute '__weakref__' of 'Dog' objects>, '__doc__': None}
object "dog" attributes: {}
The same information stored in __class__ could be retrieved by calling a type() function with one argument:
for element in (1, 'a', True): print(element, 'is', element.__class__, type(element))
1 is <class 'int'> <class 'int'> a is <class 'str'> <class 'str'> True is <class 'bool'> <class 'bool'>
When the type() function is called with three arguments, then it dynamically creates a new class.
For the invocation of type(, , ):
__name__ attribute of the class;
A very simple example, when both bases and dictionary are empty, is presented in the right pane.
Dog = type('Dog', (), {}) print('The class name is:', Dog.__name__) print('The class is an instance of:', Dog.__class__) print('The class is based on:', Dog.__bases__) print('The class attributes are:', Dog.__dict__)
As a result, we have created the simple class “Dog”.
The class name is: Dog
The class is an instance of: <class 'type'>
The class is based on: (<class 'object'>,)
The class attributes are: {'__module__': '__main__', '__dict__': <attribute '__dict__' of 'Dog' objects>, '__weakref__': <attribute '__weakref__' of 'Dog' objects>, '__doc__': None}
The more complex example that dynamically creates a fully functional class is presented in the right pane.
def bark(self): print('Woof, woof') class Animal: def feed(self): print('It is feeding time!') Dog = type('Dog', (Animal, ), {'age':0, 'bark':bark}) print('The class name is:', Dog.__name__) print('The class is an instance of:', Dog.__class__) print('The class is based on:', Dog.__bases__) print('The class attributes are:', Dog.__dict__) doggy = Dog() doggy.feed() doggy.bark()
As you can see, the Dog class is now equipped with two methods (feed() and bark()) and the instance attribute age.
The class name is: Dog
The class is an instance of: <class 'type'>
The class is based on: (<class '__main__.Animal'>,)
The class attributes are: {'age': 0, 'bark': <function bark at 0x7feb47ec20e0>, '__module__': '__main__', '__doc__': None}
It is feeding time!
Woof, woof
This way of creating classes, using the type function, is substantial for Python's way of creating classes using the class instruction:
class instruction has been identified and the class body has been executed, the class = type(, , ) code is executed;__call__ method upon class instance creation; this method calls two other methods:__new__(), responsible for creating the class instance in the computer memory; this method is run before __init__();__init__(), responsible for object initialization.
Metaclasses usually implement these two methods (__init__, __new__), taking control of the procedure of creating and initializing a new class instance. Classes receive a new layer of logic.
Now that we know what’s happening under Python's hood, it’s time to implement our own metaclass.
It’s important to remember that metaclasses are classes that are instantiated to get classes.
The first step is to define a metaclass that derives from the type type and arms the class with a 'custom_attribute', as follows:
class My_Meta(type): def __new__(mcs, name, bases, dictionary): obj = super().__new__(mcs, name, bases, dictionary) obj.custom_attribute = 'Added by My_Meta' return obj
Pay attention to the fact that:
My_Meta is derived from type. This makes our class a metaclass;__new__ method has been defined. Its role is to call the __new__ method of the parent class to create a new class;__new__ uses 'mcs' to refer to the class – it’s just a convention;Let's make use of the metaclass to create our own, domain-specific class, and check if it’s armed with the custom attribute:
class My_Object(metaclass=My_Meta): pass print(My_Object.__dict__)
Pay attention to the fact that:
My_Meta as a metaclass, not as an ordinary superclass;__dict__ attribute to check if the custom attribute is present.class My_Meta(type): def __new__(mcs, name, bases, dictionary): obj = super().__new__(mcs, name, bases, dictionary) obj.custom_attribute = 'Added by My_Meta' return obj class My_Object(metaclass=My_Meta): pass print(My_Object.__dict__)
{'__module__': '__main__', '__dict__': <attribute '__dict__' of 'My_Object' objects>, '__weakref__': <attribute '__weakref__' of 'My_Object' objects>, '__doc__': None, 'custom_attribute': 'Added by My_Meta'}
Indeed, the class attribute has been created.
Congratulations! You have just examined you first metaclass!
Let's run a more serious experiment: try to build a metaclass responsible for completing classes with a method (if missing) to ensure that all your classes are equipped with a method named 'greetings'.
As you can see, there is a greetings() function defined that greets everyone who interacts with it. In a real-life scenario, it could be a function that is obligatory for every class and is responsible for the consistency of object attributes; it could be a function returning a checksum for some of an attribute's values.
In My_Class1, by design, there is no greetings function, so when the class is constructed, it is equipped with a default function by the metaclass.
In contrast, in My_Class2 the greetings function is present from the very beginning.
Both classes rely on the same metaclass.
When you run the code, you'll see that both class instances are equipped with greetings() methods. For the “poorer” class, it is completed by the metaclass.
def greetings(self): print('Just a greeting function, but it could be something more serious like a check sum') class My_Meta(type): def __new__(mcs, name, bases, dictionary): if 'greetings' not in dictionary: dictionary['greetings'] = greetings obj = super().__new__(mcs, name, bases, dictionary) return obj class My_Class1(metaclass=My_Meta): pass class My_Class2(metaclass=My_Meta): def greetings(self): print('We are ready to greet you!') myobj1 = My_Class1() myobj1.greetings() myobj2 = My_Class2() myobj2.greetings()
Just a greeting function, but it could be something more serious like a check sum We are ready to greet you!
This is how metaclasses become very useful – they can control the process of class instantiation, and adjust created classes to conform with selected rules.
instantiation_time;get_instantiation_time() method. The method should return the value of the class attribute instantiation_time.__new__ method).import time import datetime def get_instantiation_time(self): human_readable = datetime.datetime.fromtimestamp(self.instantiation_time) print(f"Hora d'instanciament de la classe {type(self).__name__}: {human_readable.strftime('%H:%M:%S')} ({self.instantiation_time})") class MetaClassInstantation(type): instantiated = list() def __new__(mcs,name,bases,dictionary): if 'get_instantiation_time' not in dictionary: dictionary['get_instantiation_time'] = get_instantiation_time MetaClassInstantation.instantiated.append(name) obj = super().__new__(mcs,name,bases,dictionary) obj.instantiation_time = time.time() return obj class Uno(metaclass=MetaClassInstantation): pass class Dos(metaclass=MetaClassInstantation): pass uno = Uno() dos = Dos() uno.get_instantiation_time() dos.get_instantiation_time() print(MetaClassInstantation.instantiated)
Hora d'instanciament de la classe Uno: 09:02:36 (1699261356.0724955) Hora d'instanciament de la classe Dos: 09:02:36 (1699261356.0725198) ['Uno', 'Dos']
import time def get_class_instantiation_time(self): return self.class_instantiation_time class CleanCodeGuard(type): classes_created = [] def __new__(mcs, name, bases, dictionary): if 'get_class_instantiation_time' not in dictionary: dictionary['get_class_instantiation_time'] = get_class_instantiation_time obj = super().__new__(mcs, name, bases, dictionary) obj.class_instantiation_time = time.time() CleanCodeGuard.classes_created.append(name) time.sleep(1) return obj class My_Class1(metaclass=CleanCodeGuard): pass class My_Class2(metaclass=CleanCodeGuard): pass my_object1 = My_Class1() print(my_object1.get_class_instantiation_time()) my_object2 = My_Class2() print(my_object2.get_class_instantiation_time()) print(CleanCodeGuard.classes_created)
The concept of metaclasses looks hard at first glance, but if you’re responsible for API design or development, metaclasses are the magic that could help you in your work.
When you want to change your classes automatically, but decorators are not efficient, then metaclasses should help you.
In all other cases, you should agree with Tim Peters that you shouldn’t worry about metaclasses, because many problems can be solved in a simpler way than by using metaclasses.
But still, it’s beneficial to understand metaclasses in order to know when to employ them to solve a class type problem.