Note: phiên bản Tiếng Việt của bài này ở link dưới.
https://duongnt.com/init_subclass-metaclass-vie
Metaclasses are interesting features of the Python language. They allow us to dynamically inquire about a class and modify its behavior. But in many situations, metaclasses are overkilled and can actually complicate a class hierarchy. From version 3.6, Python provides a new method called __init_subclass__
to replace metaclasses in such cases.
Overview of metaclasses
A metaclass is a special class that inherits directly from type
. It defines a __new__
method that is run when a class is imported, but before any instance of that class is initialized.
class DummyMeta(type):
def __new__(cls, name, bases, class_dict):
# Code to run when classes that use this metaclass are imported
class DummyClass(metaclass=DummyClass):
# Code to define this class
Below is the meaning of each parameter in __new__
method.
- cls: the current metaclass, which is
DummyMeta
in this case. - name: the name of the class that uses this metaclass. It is
DummyClass
in this case. - bases: all parent classes of
DummyClass
. - class_dict: class members of
DummyClass
. They include both class attributes and methods.
Use a metaclass to enforce snake case in other classes
By convention, method names and attributes in Python use snake case. We will create a metaclass to raise exceptions when a class defines class attributes or methods using camel case or pascal case. This check will be performed right after we import that class, before any of its instances are created.
First, we define a SnakeCaseMeta
metaclass.
class SnakeCaseMeta(type):
def __new__(cls, name, bases, class_dict):
not_camel_case = set()
for ele in class_dict:
if cls._not_snake_case(ele) and ele not in not_camel_case:
not_camel_case.add(ele)
if not_camel_case:
raise ValueError(f'The following members are not using snake case: {", ".join(not_camel_case)}')
return type.__new__(cls, name, bases, class_dict)
@classmethod
def _not_snake_case(cls, txt):
return txt.lower() != txt
Inside __new__
, we access all class attributes and methods, and verify that their names are using snake case. All names not using snake case are stored in a list, so that we can display them in the error message.
Note that the _not_snake_case
method is a very simple implementation, we only check if the text contains any uppercase characters. This is enough for demonstration purposes, but is not ready for production.
Using SnakeCaseMeta
is trivial.
class Animal(metaclass=SnakeCaseMeta):
def EatMethod(self):
print('This animal can eat.')
def sleepMethod(self):
print('This animal can sleep.')
When we try to import Animal
‘s module, the interpreter will throw the following error.
ValueError: The following members are not using snake case: EatMethod, sleepMethod
Not only that, the same verification is also applied to all child class of Animal
.
The limitation of metaclasses
We need to define a metaclass
Let’s say we have a hierarchy of animal classes, all inherited from Animal
.
Because Animal
uses SnakeCaseMeta
as its metaclass, all its children are also verified. This is a good thing. But it would be even better if we could get rid of the metaclass and move all verifications into the base class itself.
A class can only have one metaclass
Now, let’s say we want to log every time an animal class is imported. We can do this by defining a new metaclass.
class LogImportMeta(type):
def __new__(cls, name, bases, class_dict):
print(f'Import class {name}')
return type.__new__(cls, name, bases, class_dict)
But there is a problem: a class cannot use two different metaclasses. Animal
cannot use LogImportMeta
because it has already used SnakeCaseMeta
. Here, we have a few solutions, but none of them is clean.
- Move the code inside
LogImportMeta
intoSnakeCaseMeta
. This violates the single-responsibility principle, because nowSnakeCaseMeta
is doing two unrelated things. - Create a metaclasses’ hierarchy as below. This increases the complexity of our code. And such hierarchy is sketchy anyway.
Replace metaclasses with init_subclass
From version 3.6, Python introduced a new feature called __init_subclass__
. In most cases, we can replace Metaclasses by implementing the __init__subclass__
method. Below is how to use it to verify that a class is only using snake case.
class VerifySnakeCase:
def __init_subclass__(cls):
super().__init_subclass__()
not_camel_case = set()
for ele in cls.__dict__:
if cls._not_snake_case(ele) and ele not in not_camel_case:
not_camel_case.add(ele)
if not_camel_case:
raise ValueError(f'The following members are not in snake case: {", ".join(not_camel_case)}')
@classmethod
def _not_snake_case(cls, txt):
return txt.lower() != txt
Now, we can apply VerifySnakeCase
to other classes by using inheritance.
class Animal(VerifySnakeCase):
# omitted
Likewise, we can define a new LogImport
class.
class LogImport:
def __init_subclass__(cls):
super().__init_subclass__()
print(f'Import {cls}')
Then we can use both VerifySnakeCase
and LogImport
at the same time.
class Animal(VerifySnakeCase, LogImport):
# omitted
Conclusion
Metaprogramming is one of the most interesting features of Python. It makes Python an extremely powerful and flexible language. And with the new __init__subclass__
method, metaprogramming becomes a little easier and simpler.