Metaclasses are a very basic but very advanced technique in Python. Even if you understand this technique, you will hardly ever use it in practice because it can be very useful but very destructive!
When it comes to metaclasses, it's like a chicken-and-egg or egg-and-chicken question. We all know how to create and instantiate a class, but who created the class?
def createClass(x):
class Cls:
def __init__(self, x):
self.x = x
return Cls(x)
cls = createClass(10)
print(type(cls)) # <class '__main__.createClass.<locals>.Cls'>
print(cls.x) # 10
For example, above we can use function to create a class, which is created by the createClass
method.
But what about the following example? Who allowed us to use class
syntax to create the class?
# Who created Cls?
class Cls:
def __init__(self, x):
self.x = x
We all know that you can use type()
to know the class of an object, so can't we use type()
to find the class of the Cls class?
cls = Cls(10)
type(cls) # <class '__main__.Cls'>
type(type(cls)) # <class 'type'>
Wow, it turns out that all classes are created from type
! We can use type(cls_name, bases, attrs)
to do exactly the same thing as class
syntax, which is to create a class.
Creating a class with type()
requires three input parameters:
cls_name
: name of classbases
: inherited objectsattrs
: the properties (variables and methods) owned by this class
cls = type("Cls", (object, ), {"x": 10})
print(cls) # <class '__main__.Cls'>
print(cls.x) # 10
Here's the kicker, our metaclasses
are actually interfaces between type
and class
!
Let's create a metaclass inherited from type
, if the metaclass does not inherit type
, then it is just a normal class generated by type
. The metaclass is created using the magic method __new__
and returned using type()
. Don't forget to pass three main parameters of type
, the first parameter mtcls
is the metaclass itself.
class Meta(type):
def __new__(mtcls, name, bases, attrs):
print('name: ', name)
print('bases: ', bases)
print('attrs: ', attrs)
return super().__new__(mtcls, name, bases, attrs)
class Cls(object, metaclass=Meta):
x = 0
def __init__(self, val):
self.x = val
# name: Cls
# bases: (<class 'object'>,)
# attrs: {'__module__': '__main__', '__qualname__': 'Cls', 'x': 0, '__init__': <function Cls.__init__ at 0x00000156333413A8>}
As you can see, the Cls
implemented with the metaclass automatically print out all the contents of the __new__
in metaclass Meta
, and even Cls
hasn't been instantiated yet.
Metaclasses can do so many crazy things, so be very careful when using them!
For example, I can set the attrs
of a metaclass to a blank dict
, then the class which implemented the metaclass will be wrong no matter what I do.
class Meta(type):
def __new__(mtcls, name, bases, attrs):
return super().__new__(mtcls, name, bases, {}) # <------ blank attrs
class Cls(object, metaclass=Meta):
x = 0
def __init__(self, val):
self.x = val
cls = Cls(10) # TypeError: Cls() takes no arguments
...
cls = Cls()
print(cls.x) # AttributeError: 'Cls' object has no attribute 'x'
But suppose we want someone who inherits our class to have the function must_to_do()
when they write their own class. We can then check beforehand in the metaclass's __new__
method and allow the user to successfully create a class or raise exception, as the case may be.
class Meta(type):
def __new__(mtcls, name, bases, attrs):
if name != "Base" and "must_to_do" not in attrs:
raise TypeError("Bad User Class: must_to_do() is needed")
return super().__new__(mtcls, name, bases, attrs)
class Base(metaclass=Meta):
def server_func(self):
return self.must_to_do()
class UserClass(Base):
pass
# TypeError: Bad User Class: must_to_do() is needed
Article | Link |
---|---|
What Does It Take To Be An Expert At Python? | https://youtu.be/7lmCu8wz8ro?t=2311 |
Metaclasses & How Classes Really Work | https://www.youtube.com/watch?v=NAQEj-c2CI8 |
淺談 Python Metaclass | https://medium.com/@dboyliao/淺談-python-metaclass-dfacf24d6dd5 |