Building Warehouse Management Software: A...
November 18, 2024
Welcome to Python Metaclasses! To truly grasp the concept of metaclasses, we need to start by understanding a fundamental principle of Python: everything is an object. Yes, that includes classes themselves. We’ll explore the nature of objects, the functionality of the type() function, how classes instantiate objects, and the intriguing process of instantiating classes, which are objects in their own right. We’ll also discover how to tap into this mechanism to achieve remarkable results in our code.
Once we’ve covered the fundamental concepts of metaclasses, we’ll dive into a real-world example: the Enum class and its companion, the EnumType. This case study will showcase how metaclasses can be used effectively in practice.
Python considers everything to be an object, and each object has a type that describes its nature. Numbers, for example, are of the type “int”, text is of the type “str”, and instances of a custom Person object are of the type “Person” class.
The type() method is a useful tool for determining the type of an object. Let’s experiment with it in the Python REPL. For example, when we create a list and use type() on it, we discover that it is an object instance of the “list” class. We can also ascertain the type of a string, such as “cat,” which is, predictably, “str”. In Python, even individual letters are considered strings, unlike some other programming languages that distinguish between characters and strings.
In fact, the “type” function is at the top of the class hierarchy. Just as calling “str()” on an object returns its string representation, calling “type()” on an object returns its type equivalent. It’s worth reiterating that “type” is the highest point in this hierarchy. We can confirm this by observing that the type of “type” is also “type”.
>>> print(type(1))
<type 'int'>
>>> print(type("1"))
<type 'str'>
>>> print(type(ObjectCreator))
<type 'type'>
>>> print(type(ObjectCreator()))
<class '__main__.ObjectCreator'>
Well, type has also a completely different ability: it can create classes on the fly. type can take the description of a class as parameters, and return a class.
type works this way:
type(name, bases, attrs)
Where:
type accepts a dictionary to define the attributes of the class. So:
>>> class Foo(object):
... bar = True
Can be translated to:
>>> Foo = type('Foo', (), {'bar':True})
And used as a normal class:
>>> print(Foo)
<class '__main__.Foo'>
>>> print(Foo.bar)
True
>>> f = Foo()
>>> print(f)
<__main__.Foo object at 0x8a9b84c>
>>> print(f.bar)
True
You see where we are going: in Python, classes are objects, and you can create a class on the fly, dynamically.
This is what Python does when you use the keyword class, and it does so by using a metaclass.
Dynamic class creation is a great Python feature that allows us to construct classes on the fly, giving our code flexibility and extensibility. In this section, we will look at how to construct classes dynamically using metaprogramming techniques.
First, you can create a class in a function using class:
>>> def choose_class(name):
... if name == 'foo':
... class Foo(object):
... pass
... return Foo # return the class, not an instance
... else:
... class Bar(object):
... pass
... return Bar
...
>>> MyClass = choose_class('foo')
>>> print(MyClass) # the function returns a class, not an instance
<class '__main__.Foo'>
>>> print(MyClass()) # you can create an object from this class
<__main__.Foo object at 0x89c6d4c>
But it’s not so dynamic, since you still have to write the whole class yourself.
Since classes are objects, they must be generated by something.
When you use the class keyword, Python creates this object automatically. But as with most things in Python, it gives you a way to do it manually.
Dynamic class creation is the foundation of metaclasses, which are classes that define the behavior of other classes. Metaclasses allow us to intercept class creation and modify attributes, methods, and behavior before the class is fully formed. However, exploring metaclasses goes beyond the scope of this section, as it requires a deeper understanding of Python’s metaprogramming capabilities.
Metaclasses are the ‘stuff’ that creates classes.
You define classes in order to create objects, right?
But we learned that Python classes are objects.
Well, metaclasses are what create these objects. They are the classes’ classes, you can picture them this way:
MyClass = MetaClass()
my_object = MyClass()
Everything, and I mean everything, is an object in Python. That includes integers, strings, functions and classes. All of them are objects. And all of them have been created from a class:
>>> age = 35
>>> age.__class__
<type 'int'>
>>> name = 'bob'
>>> name.__class__
<type 'str'>
>>> def foo(): pass
>>> foo.__class__
<type 'function'>
>>> class Bar(object): pass
>>> b = Bar()
>>> b.__class__
<class '__main__.Bar'>
Now, what is the __class__ of any __class__ ?
>>> age.__class__.__class__
<type 'type'>
>>> name.__class__.__class__
<type 'type'>
>>> foo.__class__.__class__
<type 'type'>
>>> b.__class__.__class__
<type 'type'>
So, a metaclass is just the stuff that creates class objects.
You can call it a ‘class factory’ if you wish.
type is the built-in metaclass Python uses, but of course, you can create your own metaclass.
The main use case for a metaclass is creating an API. A typical example of this is the Django ORM. It allows you to define something like this:
class Person(models.Model):
name = models.CharField(max_length=30)
age = models.IntegerField()
But if you do this:
person = Person(name='bob', age='35')
print(person.age)
It won’t return an IntegerField object. It will return an int, and can even take it directly from the database.
This is possible because models.Model defines __metaclass__ and it uses some magic that will turn the Person you just defined with simple statements into a complex hook to a database field.
Django makes something complex look simple by exposing a simple API and using metaclasses, recreating code from this API to do the real job behind the scenes.
Although magicians are not meant to share their secrets, understanding metaclasses allows you to solve the puzzle for yourself. You’ve learned the key behind several of Python’s finest techniques, including class instantiation and object-relational mapping (ORM) models, as well as Enum.
It’s worth mentioning that creating bespoke metaclasses isn’t always necessary. If you can address the problem in a more straightforward manner, you should probably do so. Still, understanding metaclasses will help you understand Python classes in general and recognise when a metaclass is the best tool to utilize.