Quantcast
Channel: Planet Python
Viewing all articles
Browse latest Browse all 22462

Abu Ashraf Masnun: Python: Metaclass explained

$
0
0

One of the key features of the Python language is that everything is an object. These objects are instances of classes.

class MyClass:
    pass

a = int('5')
b = MyClass()

print(type(a))
print(type(b))

But hey, classes are objects too, no? Yes, they are.

class MyClass:
    pass

print(type(MyClass))

So the classes we define are of the type type. So meta! But how are the classes constructed from the type class? And also a moment ago, we saw that type is a function that returns the type of an object?

Yes, when we pass *just* an object to type, it returns us the type of that object. But if we pass it more details, it creates the class for us. Like this:

MyClass = type('MyClass', (), {})

instance = MyClass()
print(type(instance))

We can pass name, bases as tuple and the attributes as a dictionary to type and we get back a class. The class extends the provided bases and has the attributes we provided.

When we define a class like this:

class MyClass(int):
    name = "MyClass"

It’s internally equivalent of MyClass = type("MyClass", (int,), {"name": "MyClass"}). Here type is the metaclass of MyClass.

Objects are instances of Classes and the Classes are instances of Metaclasses.

So basically, that’s the basic – we create objects from classes and then we create classes out of metaclasses. In Python, type is the default metaclass for all the classes but this can be customized as we need.

Metaclass Hook

So what if we do not want to use type as the metaclass of our classes? We want to customize the way our classes are created and we don’t have any good way of modifying how the type metaclass works. So how do we roll our own metaclass and use them?

The pretty obvious way is to use the MyClass = MyMetaClass(name, bases, attrs) approach. But there’s another way to hook in a custom metaclass for a class. In Python 2, classes could define a __metaclass__ method which would be responsible for creating the class. In Python 3, we pass the metaclass callable as a keyword based argument in the base class list:

# Python 3
class MyClass(metaclass=MetaClass):
    pass

# Python 2
class MyClass():
    __metaclass__ = MetaClass

This metaclass argument has to be a callable which takes the name, bases and attributes as it’s arguments and returns a class object instance. Please note, the metaclass argument itself does not need to be a metaclass as long as it is a factory like callable that creates classes out of metaclasses.

def func_metaclass(name, bases, attrs):
    attrs['is_meta'] = True
    return type(name, bases, attrs)

class MyClass(metaclass=func_metaclass):
    pass

That is a very simple example of a function being used as a metaclass callable. Now let’s use classes.

class MetaClass(type):
    def __init__(cls, name, bases, attrs):
        super().__init__(name, bases, attrs)
        cls.is_meta = True

class MyClass(metaclass=MetaClass):
    pass


print(MyClass.is_meta)

Here, MetaClass is called with the arguments, which are in effect passed to it’s __new__ method and we get a class. We subclassed from type, so we didn’t need to provide our own implementation for the __new__ method. After __new__ is called, the __init__ method is called for initialization purposes. We added an extra attribute to the class in our overridden __init__ method.

In our function example, we directly used the type metaclass. So all the classes generated from that function would be of the type type. On the other hand, we extended type in our class based example. So the type of the generated classes would be our metaclass. So it’s beneficial to use the class based approach.

Use Cases

Let’s keep track of subclasses:

class TrackSubclasses(type):
    subclasses = {}

    def __init__(cls, name, bases, attrs):
        for base in bases:
            cls.subclasses[base] = cls.subclasses.get(base, 0) + 1

        super().__init__(name, bases, attrs)


class A(metaclass=TrackSubclasses):
    pass


class B(A):
    pass


class C(A):
    pass


class D(B):
    pass

print(TrackSubclasses.subclasses)

Or make a class final:

class Final(type):
    def __init__(cls, name, bases, attrs):
        super().__init__(name, bases, attrs)

        for klass in bases:
            if isinstance(klass, Final):
                raise TypeError("{} is final".format(klass.__name__))

class FinalClass(metaclass=Final):
    pass

class ChildClass(FinalClass):
    pass


Viewing all articles
Browse latest Browse all 22462

Trending Articles



<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>