18. Metaclasses
By Bernd Klein. Last modified: 24 Mar 2024.
A metaclass is a class whose instances are classes. Like an "ordinary" class defines the behavior of the instances of the class, a metaclass defines the behavior of classes and their instances.
Metaclasses are not supported by every object oriented programming language. Those programming language, which support metaclasses, considerably vary in way they implement them. Python is supporting them.
Some programmers see metaclasses in Python as "solutions waiting or looking for a problem".
There are numerous use cases for metaclasses. Just to name a few:
- logging and profiling
- interface checking
- registering classes at creation time
- automatically adding new methods
- automatic property creation
- proxies
- automatic resource locking/synchronization.
Relationship between Instance, Class, and Metaclass in Python
-
Instances are created from Classes:
- Instances are individual objects that represent specific data, for example
int
,float
,list
, and so on. - Instances encapsulate the attributes and methods defined in their class.
- Instances are individual objects that represent specific data, for example
-
Classes are instances of Metaclasses:
- Classes are created from metaclasses.
- The default metaclass is
type
, responsible for creating classes. - Custom metaclasses can influence the behavior of classes.
-
Metaclasses define the creation of Classes:
- Metaclasses, like regular classes, can define attributes and methods.
- They control the creation and structure of classes.
- Custom metaclasses often override the
__new__
and__init__
methods to customize class creation.
In summary, the relationship is hierarchical: Instances are created from classes, and classes are created from metaclasses. Metaclasses provide a way to customize the creation and behavior of classes, allowing developers to exert more control over the structure and functionality of their code.
Live Python training
Enjoying this page? We offer live Python training courses covering the content of this site.
Defining Metaclasses
A metaclass in Python is a class that defines the behavior of other classes, often referred to as the "class of a class." In other words, while a regular class defines the properties and methods of instances, a metaclass defines the properties and methods of classes.
In Python, the default metaclass is the built-in type. When you create a class, Python uses type as the metaclass for that class. However, you can create your own custom metaclasses to modify the behavior of how classes are create
In essence, metaclasses in Python are constructed similarly to regular classes, with the key distinction that they inherit from the "type" class. Another difference is, that a metaclass is called automatically, when the class statement using a metaclass ends. In other words: If no "metaclass" keyword is passed after the base classes (there may be no base classes either) of the class header, type() (i.e. __call__ of type) will be called. If a metaclass keyword is used, on the other hand, the class assigned to it will be called instead of type.
Now we create a very simple metaclass. It's good for nothing, except that it will print the content of its arguments in the __new__
method and returns the results of the type.__new__
call:
class LittleMeta(type):
def __new__(cls, clsname, superclasses, attributedict):
print("clsname: ", clsname)
print("superclasses: ", superclasses)
print("attributedict: ", attributedict)
return type.__new__(cls, clsname, superclasses, attributedict)
The LittleMeta class inherits from the built-in metaclass type
.
-
The
__new__
method is overridden within the LittleMeta class. This method is called when a new class is created. It takes four parameters:- cls: The metaclass itself (in this case, LittleMeta).
- clsname: The name of the class being created.
- superclasses: A tuple of the class's base classes.
- attributedict: A dictionary containing the class's attributes.
-
The print funciton calls inside the
__new__
method give us information about what is going on with the class being created. -
Finally, the
__new__
method of the built-intype
class (the metaclass from which LittleMeta inherits) is being called and returnd. This is done to ensure that the class creation process continues with the default behavior.
We will use the metaclass "LittleMeta" in the following example:
class Foo:
pass
class A(Foo, metaclass=LittleMeta):
pass
a = A()
OUTPUT:
clsname: A superclasses: (<class '__main__.Foo'>,) attributedict: {'__module__': '__main__', '__qualname__': 'A'}
We can see LittleMeta.__new__
has been called and not type.__new__
.
Resuming our thread from the last chapter: We define a metaclass EssentialAnswers
which is capable of automatically including our augment_answer
method:
x = input("Do you need the answer? (y/n): ")
required = x in ('y', 'yes')
def the_answer(self, *args):
return 42
class EssentialAnswers(type):
def __init__(cls, clsname, superclasses, attributedict):
if required:
cls.the_answer = the_answer
class Philosopher1(metaclass=EssentialAnswers):
pass
class Philosopher2(metaclass=EssentialAnswers):
pass
class Philosopher3(metaclass=EssentialAnswers):
pass
kant = Philosopher1()
# let's see what Kant has to say :-)
try:
print(kant.the_answer())
except AttributeError as e:
print("The method the_answer is not implemented")
OUTPUT:
42
We have learned in our chapter "Type and Class Relationship" that after the class definition has been processed, Python calls
type(classname, superclasses, attributes_dict)
This is not the case, if a metaclass has been declared in the header. That is what we have done in our previous example. Our classes Philosopher1, Philosopher2 and Philosopher3 have been hooked to the metaclass EssentialAnswers. That's why EssentialAnswer will be called instead of type:
EssentialAnswer(classname, superclasses, attributes_dict)
To be precise, the arguments of the calls will be set the the following values:
EssentialAnswer('Philopsopher1', (), {'__module__': '__main__', '__qualname__': 'Philosopher1'})
The other philosopher classes are treated in an analogue way.
The Singleton Pattern
Using Metaclasses
The singleton pattern is a design pattern that restricts the instantiation of a class to one object. It is used in cases where exactly one object is needed. The concept can be generalized to restrict the instantiation to a certain or fixed number of objects. The term stems from mathematics, where a singleton, - also called a unit set -, is used for sets with exactly one element.
class Singleton(type):
_instances = {}
def __call__(cls, *args, **kwargs):
if cls not in cls._instances:
cls._instances[cls] = super(Singleton, cls).__call__(*args, **kwargs)
return cls._instances[cls]
class SingletonClass(metaclass=Singleton):
pass
class RegularClass():
pass
x = SingletonClass()
y = SingletonClass()
print(x == y)
x = RegularClass()
y = RegularClass()
print(x == y)
OUTPUT:
True False
# Accessing _instances directly from the metaclass
singleton_instances = Singleton._instances
# Accessing _instances from the class
singleton_class_instances = SingletonClass._instances
# Displaying the instances
print("All Singleton Instances:", singleton_instances)
print("SingletonClass Instances:", singleton_class_instances)
OUTPUT:
All Singleton Instances: {<class '__main__.SingletonClass'>: <__main__.SingletonClass object at 0x7f341bdb5510>} SingletonClass Instances: {<class '__main__.SingletonClass'>: <__main__.SingletonClass object at 0x7f341bdb5510>}
Creating Singletons by Inheriting
Alternatively, we can create Singleton classes by inheriting from a Singleton class, which can be defined like this:
class Singleton(object):
_instance = None
def __new__(cls, *args, **kwargs):
if not cls._instance:
cls._instance = object.__new__(cls, *args, **kwargs)
return cls._instance
class SingletonClass(Singleton):
pass
class RegularClass():
pass
x = SingletonClass()
y = SingletonClass()
print(x == y)
x = RegularClass()
y = RegularClass()
print(x == y)
OUTPUT:
True False
This implementation achieves the singleton pattern without using a metaclass, providing a straightforward and commonly used alternative.
Singleton Pattern by Class Decoration
The following example implements the Singleton pattern using a class decorator:
def singleton(cls):
instances = {}
def get_instance(*args, **kwargs):
if cls not in instances:
instances[cls] = cls(*args, **kwargs)
return instances[cls]
return get_instance
@singleton
class SingletonClass:
def __init__(self, data):
self.data = data
# Creating instances
singleton_instance_1 = SingletonClass("Instance 1")
singleton_instance_2 = SingletonClass("Instance 2")
# Both instances refer to the same object
print(singleton_instance_1 is singleton_instance_2) # Outputs: True
# Accessing data from both instances
print(singleton_instance_1.data) # Outputs: Instance 1
print(singleton_instance_2.data) # Outputs: Instance 1
OUTPUT:
True Instance 1 Instance 1
When you create instances of SingletonClass
, they will refer to the same object, ensuring that only one instance of the class is created:
singleton_instance_1 = SingletonClass("Instance 1")
singleton_instance_2 = SingletonClass("Instance 2")
print(singleton_instance_1 is singleton_instance_2) # Outputs: True
print(singleton_instance_1.data) # Outputs: Instance 1
print(singleton_instance_2.data) # Outputs: Instance 1
OUTPUT:
True Instance 1 Instance 1
Live Python training
Enjoying this page? We offer live Python training courses covering the content of this site.
Upcoming online Courses
Another Example
In this example, we'll create a metaclass that ensures all methods in a class follow an underscore notation naming convention. If a method uses camel case, the metaclass will automatically convert it to underscore notation.
import re
# Custom Metaclass Definition
class CamelCaseToUnderscoreMeta(type):
def __new__(cls, name, bases, dct):
# Create a separate dictionary for modified items
modified_items = {}
# Convert camel case method names to underscore notation
for key, value in dct.items():
if callable(value) and re.match(r'^[a-z]+(?:[A-Z][a-z]*)*$', key):
# Convert camel case to underscore notation
underscore_name = re.sub(r'([a-z0-9])([A-Z])', r'\1_\2', key).lower()
# Store the modified item in the new dictionary
modified_items[underscore_name] = value
else:
# Store unmodified items in the new dictionary
modified_items[key] = value
# Create the class using the modified attributes
return super().__new__(cls, name, bases, modified_items)
# Class using the Custom Metaclass
class CamelCaseClass(metaclass=CamelCaseToUnderscoreMeta):
def processData(self):
print("Processing data...")
def transformData(self):
print("Transforming data...")
def processOutputData(self):
print("Processing output data...")
# Creating an instance of CamelCaseClass
camel_case_instance = CamelCaseClass()
# Calling methods with modified underscore notation names
camel_case_instance.process_data()
camel_case_instance.transform_data()
camel_case_instance.process_output_data()
OUTPUT:
Processing data... Transforming data... Processing output data...
Using Class Decoration
The following implementation uses a class-level attribute (_instance) to track whether an instance has been created and returns the existing instance if available. Here's the provided code explained:
import re
# Class decorator to convert camel case method names to underscore notation
def camel_case_to_underscore(cls):
modified_items = {}
# Convert camel case method names to underscore notation
for key, value in cls.__dict__.items():
if callable(value) and re.match(r'^[a-z]+(?:[A-Z][a-z]*)*$', key):
# Convert camel case to underscore notation
underscore_name = re.sub(r'([a-z0-9])([A-Z])', r'\1_\2', key).lower()
# Store the modified item in the new dictionary
modified_items[underscore_name] = value
else:
# Store unmodified items in the new dictionary
modified_items[key] = value
# Create a new class with the modified attributes
return type(cls.__name__, cls.__bases__, modified_items)
# Class using the Class Decorator
@camel_case_to_underscore
class CamelCaseClass:
def processData(self):
print("Processing data...")
def transformData(self):
print("Transforming data...")
def processOutputData(self):
print("Processing output data...")
# Creating an instance of CamelCaseClass
camel_case_instance = CamelCaseClass()
# Calling methods with modified underscore notation names
camel_case_instance.process_data()
camel_case_instance.transform_data()
camel_case_instance.process_output_data()
OUTPUT:
Processing data... Transforming data... Processing output data...
In this version, the camel_case_to_underscore
function acts as a class decorator, modifying the class dictionary to convert camel case method names to underscore notation. The CamelCaseClass is then decorated with this function using the
@` syntax. The instance creation and method calls remain the same.
Live Python training
Enjoying this page? We offer live Python training courses covering the content of this site.
Upcoming online Courses