Real world metaclass usages inside django
Recently, here at Seelk, we faced an issue that motivated us to look at metaclasses and we discovered that it is a complicated subject confusing lots of people. This is even more true when you mix this with django’s own usage of metaclasses.
After eventually figuring things out (thanks to readings, trials and errors), we’re writing this article to clear up common misconceptions people might have about them. And what better way to show than a few real world examples ?
Before diving head first into the code and the actual examples, let’s first review together what actually is a meta class.
So, what is a metaclass ?
In order to quickly skim through the tedious details and jump directly to the parts we’re interested in, I’ll assume that you already did your homework and have some basic understanding of what is a metaclass in Python. If you don’t know anything about metaclasses, don’t worry, you can scroll down to the reference section, read the few listed articles and come back here as soon as you’re done.
The same way you’re using a class to define properties, methods and more on an object, a metaclass will be used to define the exact same things on a class.
A class is used to instanciate instances.
A metaclass is used to instanciate classes.
This is basically the gist of it.
Let's look at some examples to better understand how metaclasses are working.
Note that the following examples have no real world utility and that the same results could be achieved using simple inheritance. They're used to play with metaclasses not to justify their usages.
Let's start simple by changing an attribute and a class name
class RedAnimal:
color = "red"
RedAnimal.color
# 'red'
RedAnimal.__name__
# 'RedAnimal'
type(RedAnimal)
# type
class MakeBlue(type):
def __new__(cls, name, bases, attrs):
attrs['color'] = "blue"
return super().__new__(cls, 'BlueAnimal', bases, attrs)
class RedAnimal(metaclass=MakeBlue):
color = "red"
RedAnimal.color
# 'blue'
RedAnimal.__name__
# 'BlueAnimal'
type(RedAnimal)
# __main__.MakeBlue
Nothing too shiny for now.
Let's spice things up a bit.
class Animal:
def make_sound(self):
return self.sound
class Dog(Animal):
sound = "woof"
class Cat(Animal):
sound = "meow"
Dog().make_sound()
# 'woof'
Cat().make_sound()
# 'meow'
class MakeLouder(type):
def __new__(cls, name, bases, attrs):
def make_into_loud_sound(func):
def make_loud_sound(self):
return f'{func(self)} !!!!!!!'
return make_loud_sound
attrs['make_sound'] = make_into_loud_sound(attrs['make_sound'])
return super().__new__(cls, f'Loud{name}', bases, attrs)
class Dog(Animal, metaclass=MakeLouder):
sound = "woof"
# KeyError: 'make_sound'
# ----> attrs['make_sound'] = make_into_loud_sound(attrs['make_sound'])
Uh oh, what happened here ?
attrs
refers to the attributes that are defined on Dog
and if we look closely, Dog
does not define anything besides sound
.
make_sound
is actually defined on Animal
so we need to get the make_sound
method from it and we can do it two ways.
For the first one we can get the method directly from the base class
class MakeLouder(type):
def __new__(cls, name, bases, attrs):
def make_into_loud_sound(func):
def make_loud_sound(self):
return f'{func(self)} !!!!!!!'
return make_loud_sound
attrs['make_sound'] = make_into_loud_sound(bases[0].make_sound)
return super().__new__(cls, name, bases, attrs)
class Dog(Animal, metaclass=MakeLouder):
sound = "woof"
class Cat(Animal, metaclass=MakeLouder):
sound = "meow"
Dog().make_sound()
# 'woof !!!!!!!'
Cat().make_sound()
# 'meow !!!!!!!'
For the second one we can simply move the metaclass to the Animal
class
class MakeLouder(type):
def __new__(cls, name, bases, attrs):
def make_into_loud_sound(func):
def make_loud_sound(self):
return f'{func(self)} !!!!!!!'
return make_loud_sound
print(name)
attrs['make_sound'] = make_into_loud_sound(attrs['make_sound'])
return super().__new__(cls, name, bases, attrs)
class Animal(metaclass=MakeLouder):
def make_sound(self):
return self.sound
class Dog(Animal):
sound = "woof"
# KeyError: 'make_sound'
# ----> attrs['make_sound'] = make_into_loud_sound(attrs['make_sound'])
Whoops ! We got the same crash as before. But Why ?
If you were to put print(name)
inside MakeLouder.__new__
, you'd see two lines : Animal
and Dog
. The metaclass is actually called once for each class it is featured on. That means it is called when you're defining Animal but it is also called when you're defining Dog because it inherits from Animal which does feature the metaclass.
We can fix this two ways, right or wrong. We could add an if that checks that the name is 'Animal' before overriding make_sound
but if Dog
would be to redefine it then our change would not be taken into account so obviously, wrong way. Or we could simply add an if that checks that make_sound
is inside attrs
before overriding it which is the right way to go.
We end up with this
A few gotchas :
Beware of attributes defined on the bases instead of the class the metaclass is applied on
If a base class already uses a metaclass and your class must inherit this metaclass
What’s the difference with django’s class Meta ?
How to enhance a django model with a metaclass ?
Examples
Currency conversion
References :
Aaccessible StackOverflow answer of what is a metaclass in Python
Very deep article detailing the inner workings of metaclasses
Article that approaches metaclasses with examples in other libraries
Philip Garnero
More from Seelk
Know your audience on Amazon: The power of reviews
Thu Nov 19 2020
6 min read
Marjorie Borreda-Martinez
January 2020 inside Seelk Studio: A few updates...And an in-depth look at Competition & Share of Voice!
Tue Jan 28 2020
5 min read
Nathaniel Daudrich
Real world metaclass usages inside django
Wed Oct 09 2019
3 min read
Philip Garnero