Use metaclasses

#In-depth analysis of Python metaclass

Have you ever been curious about Python’sclassWhat exactly is done behind the keywords? Why are Python's class definitions so much more "flexible" than static languages ​​such as Java and C++? Today we will dig deeper into Python's advanced features - metaclass, and together we will uncover the mystery of dynamically creating classes and even controlling the class creation process.


Dynamic types andtype()function

Python is a purely dynamic language: its classes and functions are not templates that are hard-coded during the compilation phase, but objects that are dynamically generated when the program is running. This point is a core premise for understanding metaclasses.

type()two identities

Many people only knowtype()It can "view the type of object", but its true identity is actually the default "class factory" for all classes in Python - that is, the default metaclass.

1. Basic usage: View type

# 查看普通实例的类型
>>> type(123)
<class 'int'>
>>> type('hello')
<class 'str'>

# 那类本身的类型呢?
>>> type(int)
<class 'type'>
>>> type(str)
<class 'type'>

Note: the types of all classes aretype! This implies that "classes are also created from another thing (type)."

2. Core usage: dynamically create classes

If not usedclasskeyword, we can call it manuallytype()To generate a fully functional class:

# 1. 定义类的方法
def say_hello(self, name='world'):
    print(f'Hello, {name}.')

# 2. 用 type() 创建类
# 参数依次是:类名、继承的父类元组、包含类属性/方法的字典
Hello = type(
    'Hello',
    (object,),
    {'hello': say_hello, 'author': 'Python Blog'}
)

# 3. 正常使用
h = Hello()
h.hello()        # 输出: Hello, world.
print(h.author)  # 输出: Python Blog

In-depth understanding of metaclass

Since the class is composed oftypeCreated, then if we want to customize the "class creation rules" (for example: automatically add a method to all subclasses, modify the class name, filter attributes), we need to use the "custom metaclass".

Three-layer relationship chain

In order to avoid confusing levels, we first remember this core chain:

自定义元类 → 继承它的普通类 → 普通类的实例

Use a life-like metaphor to explain:

  • Custom metaclass = "Mold Factory" (specifies the standards for "production of molds")
  • General type = "Mold" (produced according to the standards of the mold factory and used to generate products)
  • Instance = "Product" (generated by mold)

How to write custom metaclasses

Custom metaclasses must meet two conditions:

  1. Inherited from default metaclasstype
  2. Naming convention (not mandatory but recommended) begins withMetaclassending

Usually we will override the metaclass__new__Method - This is the interception entry of the class creation process. The parameters are as follows:

  • cls: The "metaclass instance" currently being created (that is, the ordinary class itself)
  • name: Common class name to be generated
  • bases: Tuple of parent classes inherited by ordinary classes
  • attrs: Dictionary of attributes and methods of ordinary classes

Small example: automatically add to the listaddmethod

Python nativelistonlyappendmethod, noadd. We can use a metaclass to automatically have it for all list classes that inherit itadd

class ListMetaclass(type):
    def __new__(cls, name, bases, attrs):
        # 给 attrs 字典里插入一个 add 方法
        attrs['add'] = lambda self, value: self.append(value)
        # 调用父类(type)的 __new__ 完成类创建
        return super().__new__(cls, name, bases, attrs)

# 使用元类:在 class 定义里加 metaclass 参数
class MyList(list, metaclass=ListMetaclass):
    pass

# 测试
L = MyList([1, 2, 3])
L.add(4)  # 这里实际调用了 append
print(L)  # 输出: [1, 2, 3, 4]

Practical application: handwriting a minimalist ORM framework

One of the most classic and practical scenarios of metaclasses is ORM (Object-Relational Mapping) - completely mapping database tables, fields, and records into Python classes, attributes, and instances, allowing developers to operate the database without writing SQL.

Step 1: Define field mapping class

First write a few basic classes to represent "database fields":

class Field:
    """所有数据库字段的基类"""
    def __init__(self, name, column_type):
        self.name = name           # 字段在数据库里的真实名称
        self.column_type = column_type  # 字段的数据库类型

    def __str__(self):
        return f'<{self.__class__.__name__}:{self.name}>'

class StringField(Field):
    """字符串类型字段(默认varchar(100))"""
    def __init__(self, name):
        super().__init__(name, 'varchar(100)')

class IntegerField(Field):
    """整数类型字段(默认bigint)"""
    def __init__(self, name):
        super().__init__(name, 'bigint')

Step 2: Use metaclasses to process model classes

Next, write the coreModelMetaclass. It needs to be in the model class (e.g.User) complete the following tasks when creating:

  1. Skip the base classModelitself (only handles specific business table classes)
  2. Collect all objects defined in the classFieldExample
  3. Put theseFieldRemoved from class attributes (to prevent instance attributes from being overridden by class attributes)
  4. Save field mapping and table name to class attributes
class ModelMetaclass(type):
    def __new__(cls, name, bases, attrs):
        # 1. 如果是基类 Model,直接用默认方式创建,不做特殊处理
        if name == 'Model':
            return super().__new__(cls, name, bases, attrs)

        print(f'正在注册模型类: {name}')
        mappings = {}  # 存储「Python属性名: Field实例」的映射

        # 2. 遍历所有类属性,收集 Field
        for k, v in attrs.items():
            if isinstance(v, Field):
                print(f'  发现字段映射: {k}{v}')
                mappings[k] = v

        # 3. 把收集到的 Field 从类属性里移除
        # 原因:如果实例属性和类属性同名,Python会优先使用类属性
        for k in mappings.keys():
            attrs.pop(k)

        # 4. 给模型类添加两个关键的私有属性
        attrs['__mappings__'] = mappings   # 保存字段映射
        attrs['__table__'] = name          # 假设表名和类名相同
        return super().__new__(cls, name, bases, attrs)

Step 3: Define ORM base class

write lastModelBase class, which requires:

  1. Inherited fromdict(Convenient to use dictionary to store data)
  2. Implementation__getattr__and__setattr__(Let the instance access properties like a normal object)
  3. Implement common database operation methods (such assave()
class Model(dict, metaclass=ModelMetaclass):
    def __init__(self, **kwargs):
        super().__init__(**kwargs)

    def __getattr__(self, key):
        """让实例可以用 u.name 代替 u['name']"""
        try:
            return self[key]
        except KeyError:
            raise AttributeError(f"'Model' 对象没有属性 '{key}'")

    def __setattr__(self, key, value):
        """让实例可以用 u.name = 'XXX' 代替 u['name'] = 'XXX'"""
        self[key] = value

    def save(self):
        """生成 INSERT SQL(演示用,只打印)"""
        fields = []
        placeholders = []
        args = []

        # 遍历保存的字段映射
        for py_attr, field in self.__mappings__.items():
            fields.append(field.name)                    # 使用数据库真实字段名
            placeholders.append('?')                     # SQL占位符(根据数据库不同可能是%s)
            args.append(getattr(self, py_attr, None))    # 获取实例的Python属性值

        sql = f"INSERT INTO {self.__table__} ({','.join(fields)}) VALUES ({','.join(placeholders)})"
        print(f'\n生成的 SQL: {sql}')
        print(f'SQL 参数: {args}')

Step 4: Actual use of ORM

Now, we can define the database table just like a normal class:

# 定义 User 表
class User(Model):
    id = IntegerField('id')
    name = StringField('username')   # Python属性名是 name,数据库字段是 username
    email = StringField('email')
    password = StringField('password')

# 创建 User 实例(对应数据库的一条记录)
u = User(id=12345, name='Michael', email='test@orm.org', password='my-pwd')

# 调用 save() 方法(实际项目中这里会连接数据库执行)
u.save()

The output of running this code is as follows:

正在注册模型类: User
  发现字段映射: id → <IntegerField:id>
  发现字段映射: name → <StringField:username>
  发现字段映射: email → <StringField:email>
  发现字段映射: password → <StringField:password>

生成的 SQL: INSERT INTO User (id,username,email,password) VALUES (?,?,?,?)
SQL 参数: [12345, 'Michael', 'test@orm.org', 'my-pwd']

Summarize

  1. type()Dual identity: It is both a tool for "viewing object types" and the default metaclass for all classes in Python.
  2. The role of metaclass: intercept the creation process of classes and realize the requirements of "batch customization of classes" (such as ORM, Django's Admin system, etc.).
  3. Use with caution: Metaclasses are one of the most powerful features of Python, but in 99% of scenarios the problem can be solved with ordinary methods (such as decorators and inheritance). Misuse of metaclasses can make code difficult to understand and maintain.

Although there is a certain threshold for understanding metaclasses, it can help you gain a deeper grasp of Python's object-oriented mechanism, giving you one more handy tool when encountering scenarios that require "designing the class itself."