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