Metaclass in Python: a quick introduction
This post quickly reviews how a class is created in Python and explains how metaclass
works in an example.
How a Class is Created in Python?
In Python creating a class consists of two major steps:
- Step 1: prepare the class body and use it to populate namespace of the class
- Step 2: use the namespace created in step 1 to create the class
The sample code below demonstrates how a class is created under the cover.
>>> # Step 1: populate class namespace
>>>
>>> cls_body = """
... def __init__(self, x, y, z):
... self._x = x
... self._y = y
... self._z = z
... """
>>> cls_namespace = {}
>>> exec(cls_body, globals(), cls_namespace)
>>>
>>>
>>> # Step 2: create new class
>>>
>>> cls_name = 'PointType'
>>> cls_bases = ()
>>> PointType = type(cls_name, cls_bases, cls_namespace)
>>>
>>> PointType
<class '__main__.PointType'>
>>>
Any class is an instance of the type
object, and the type
class itself inherits from the object
class. Just like any class, type
can be inherited as well, and it is how metaclass
come into being. In next section, we demonstrate how inheriting from type
class and overriding the __new__
method can be used to manipulate class creation.
Inheriting type
Class
The sample code below demonstrate how class creation can be manipulated by inheriting from the type
class and overriding the __new__
method.
>>> class BallType(type):
... def __new__(cls, name, bases, namespace):
... cls_obj = super().__new__(cls, name, bases, namespace)
... cls_obj.volume = lambda self: 4*math.pi*self.radius**3/3.0
... cls_obj.surface_area = lambda self: 4*math.pi*self.radius**2
... return cls_obj
...
>>>
>>> # Step 1: populate class namespace
>>> cls_body = """
... def __init__(self, center: PointType, radius: float):
... self.center = center
... self.radius = radius
... """
>>> cls_namespace = {}
>>> exec(cls_body, globals(), cls_namespace)
>>>
>>> # Step 2: create new class
>>> cls_name = 'Ball'
>>> cls_bases = ()
>>> Ball = BallType(cls_name, cls_bases, cls_namespace)
>>>
>>> b = Ball(PointType(1, 1, 1), 2.0)
>>>
>>> b.volume(), b.surface_area()
(33.510321638291124, 50.26548245743669)
>>>
In the above example, when overriding the __new__
method, we injected two methods, volume
and surface_area
, into our new BallType
. Suppose during the development, we will have multiple different classes, which commonly involve calculation of ball volume and surface area. Then using the above BallType
to create these new classes, we don’t have to implement the volume
and surface_area
methods in any of the new classes.
In actual implementations, however, we should never create new classes as the above example, which is only for demonstrating how it works. In next section, we show in an example what it should look like in actual implementations, using metaclass
.
Using Metaclass
>>>
>>> class Planet(metaclass=BallType):
... def __init__(self, name: str, center: PointType, radius: float):
... self.name = name
... self.center = center
... self.radius = radius
...
>>>
>>>
>>> p = Planet('Mars', PointType(1, 2, 1), 3.39)
>>>
>>> p.volume(), p.surface_area()
(163.18780614312308, 144.41398773727704)
>>>
>>>
As demonstrated above, using metaclass
when defining a new class, we tell Python to use the specified metaclass
, which inherits from the type
class and overrides its __new__
method, to create the new class. With metaclass
we can customize class creation, and it is usually used in API or library development.
Conclusion
In this post, we have explained with example how a class is created in Python. To recap, it consists of two steps: populating the class namespace using the class body and creating the class as an instance of the type
object.
We also demonstrated how class creation can be manipulated by inheriting from the type
class and overriding its __new__
method. The we demonstrated how metaclass
is used in an example, and hence made how metaclass
works clear.