python-course.eu

17. Metaclasses

By Bernd Klein. Last modified: 15 Mar 2024.

metaclasses: klein bottle

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:

Relationship between Instance, Class, and Metaclass in Python

metaclasses: diagram

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

instructor-led training course

Enjoying this page? We offer live Python training courses covering the content of this site.

See: Live Python courses overview

Enrol here

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.

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

instructor-led training course

Enjoying this page? We offer live Python training courses covering the content of this site.

See: Live Python courses overview

Upcoming online Courses

Enrol here

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

instructor-led training course

Enjoying this page? We offer live Python training courses covering the content of this site.

See: Live Python courses overview

Upcoming online Courses

Enrol here