What is a metaclass

What are metaclasses

Python programmers often say, "everything is an object", which means that everything you can see in Python, including int, float, function and so on, is an object. But in daily development, when it comes to objects, we may not think of classes immediately. In fact, a class is also an object. Since a class is also an object, there is a way to create a class. This is where the metaclass comes out. The metaclass is the class that creates the class.

What does metaclass do

The metaclass intercepts the class creation process, modifies the class, and then returns the modified class.

Just looking at the above sentence, metaclasses seem very simple, but because metaclasses can change the creation process of a class and do some tricks in it, the whole thing will become extremely obscure and difficult to understand.

Type is the metaclass of all classes in python. If you keep calling type() on an object, you will find that it will eventually point to type. Type is special, and type itself is its own metaclass.

You can use type to create a class like this

class Base:
    def __repr__(self):
    return self.__class__.__name__
 
 
def hello(self):
    print("hello")


Test = type("Test", (Base,), {"hello": hello})

Among them, the first parameter accepted by type is the name of the class, the second parameter is a tuple, which is used to specify the class to be inherited, and the third parameter is a dictionary. In this dictionary, the attributes and methods required by a class can be put in a dictionary, so that all classes generated by this metaclass will have these attributes.

The above code is equivalent to the following code:

class Test(Base):
    def hello(self):
    print("hello")

In addition, you can create your own metaclass through type, and then use this metaclass to create classes:

class Meta(type):
    def __new__(mcs, name, bases, attrs):
        for k in list(attrs.keys()):
            if k.lower() != k:
                # Cast attribute name to lowercase
                attrs[k.lower()] = attrs.pop(k)
        return super(Meta, mcs).__new__(mcs, name, bases, attrs)


class Case(metaclass=Meta):
    def Hello(self):
        print("hello")

In the above metaclass, we check the class attributes. If the class attributes are not lowercase (not in accordance with PEP8 style), we force the class attributes to be converted to lowercase. In this way, we force subclasses to conform to a certain coding style (the properties of subclasses must be lowercase). This is just hello word in metaclass applications. Using metaclasses can do more things.

Let's implement a slightly more complex requirement.

We know that the namedtuple in Python can be easily used to express a piece of data. Suppose we want to implement a namedtuple with a metaclass. For simplicity, we only require that this implementation can accept location parameters. A possible implementation is as follows:

import itertools
import operator
 
 
def __new__(cls, *args, **kwargs):
    return tuple().__new__(cls, args)
 
 
def namedtuple(name, fields):
    fields = fields.split(",") if isinstance(fields, str) else fields
    attrs = {fld: property(itemgetter(i)) for i, fld in enumerate(fields)}
    attrs["__new__"] = __new__
    return type(name, (tuple,), attrs)
 
Student = namedtuple("Student", ["id", "name", "score"])

In this way, we have a simple named tuple that we can use just like Python's named tuple

stu = Student(1, "zhangsan", 100) # This implementation does not support keyword parameters
print(stu.id)

Several methods needing attention in metaclass

In metaclasses, we usually define__ new__ Or__ init__ Or__ call__ To control the class creation process__ new__ Before class creation is called, some modifications can be made before class creation. init__ After the class is created, make some modifications to the created class__ call__ It is called when instantiating a class. Generally, we only need to define one of them. The specific use can be selected according to the business scenario.

In addition, if we need to accept additional parameters, we also need to define__ prepare__ Method. This method will prepare the namespace for the class, but it is usually not necessary to define this method. Just use the default.

For example, we want to use metaclasses to implement a singleton pattern. Here is a simple example:

class Singleton(type):
    __instance = None
 
    def __call__(cls, *args, **kwargs):
        if cls.__instance is None:
            cls.__instance = super().__call__(*args, **kwargs)
            return cls.__instance
        else:
             # If the instance already exists, return directly
             return cls.__instance
 
 
class Test(metaclass=Singleton):
    def __init__(self):
        print('init in Test class')
 
 
if __name__ == "__main__":
    Test()
Test()

In the above example, we implemented it in the metaclass__ call__ Method to check when instantiating the class. If the class already has an instance, we will return the existing instance to ensure that the class will be instantiated only once. Of course, this metaclass has many defects. For example, when multiple classes use this metaclass, the instances will be confused. This is another problem. I won't expand it here first.

How to use metaclasses

The answer is: usually you don't need to use metaclasses

There is a widely spread explanation about the use of metaclasses in Python. The original version is as follows:

Metaclasses are deeper magic that 99% of users should never worry about it. If you wonder whether you need them, you don't (the people who actually need them to know with certainty that they need them and don't need an explanation about why). -Tim Peters

In our development work, we rarely encounter the need to dynamically create a class. In case we encounter the need to dynamically change a class, we can also implement it through class decorator and monkey patch. These two methods are easier to understand than metaclasses.

Posted on Wed, 24 Nov 2021 05:52:26 -0500 by jjoske