python 动态构造class

十二月 10, 2025 [杂项] #Python #obsidian

python 动态构造class

动态执行代码(如 exec)后,用 inspect 或直接构造源码字符串

,将定义的类及其属性保存为 .py 文件。核心思路是:在动态定义类之后,提取其结构(类名、属性、方法),再生成对应的 Python 源码并写入文件。下面提供两种实用方案(推荐方案一):

✅ 方案一:动态定义后,手动构造源码字符串(最可靠,可控性强)

生成的 generated_class.py 内容为:

✅ 优点:简单、安全、可读性强;适合普通数据类型(str/int/float/list/dict 等) ⚠ 注意:repr() 对复杂对象(如函数、自定义类实例)可能不理想,需额外处理

✅ 方案二:使用 inspect + ast(高级,支持方法,但较复杂)

如果类中还有动态添加的方法,可用 inspect.getsource()(但要求方法是定义在源码中的,exec 中的 lambda/def 通常拿不到源码),更通用的是结合 ast 构建(略复杂)。

简化版支持方法(仅当方法是 def 形式且在 exec 字符串中):

🔒 安全提醒(重要!)

🎯 进阶建议(生产环境)

如果你频繁需要“动态建类 → 序列化为代码”,可考虑:

需要我帮你封装一个通用函数 save_class_to_file(cls, filename) 吗?欢迎补充你的具体场景~

非常好的问题!我们来系统讲解——如何在生产环境中安全、可靠地动态生成并持久化 Python 类定义,避免 exec 的安全与维护隐患。

核心思想:数据驱动 + 模板生成 👉 将「类的结构」抽象为数据(如字典、YAML/JSON 配置) ,再通过模板渲染成合法 Python 源码。

✅ 推荐方案总览

场景推荐工具优点
简单类(属性为主)string.Template/ f-string无依赖,轻量
复杂类(属性+方法+注解)Jinja2表达力强,模板复用
需类型提示 / 数据校验pydantic+ 动态生成类型安全,自文档化
高性能 / 编译时生成ast+compile100% 合法 AST,可 lint/format

🔧 方案一:Jinja2 模板驱动(最推荐!)

步骤 1:定义类结构为数据

# config.py 或 config.yaml
class_spec = {
    "name": "User",
    "docstring": "动态生成的用户模型",
    "attributes": [
        {"name": "id", "type": "int", "default": 0, "comment": "用户ID"},
        {"name": "name", "type": "str", "default": "''", "comment": "用户名"},
        {"name": "email", "type": "str | None", "default": "None"},
        {"name": "_created_at", "type": "float", "default": "time.time()", "private": True},
    ],
    "methods": [
        {
            "name": "greet",
            "args": ["self"],
            "return_type": "str",
            "body": [
                'return f"Hello, {self.name}!"'
            ]
        }
    ],
    "imports": ["import time"]
}

步骤 2:编写 Jinja2 模板 class_template.j2

{{ '\n'.join(imports) if imports else '' }}

class {{ name }}:
    """{{ docstring }}"""

    {% for attr in attributes %}
    {% if attr.comment %}# {{ attr.comment }}{% endif %}
    {{ attr.name }}: {{ attr.type }} = {{ attr.default }}
    {% endfor %}

    {% for method in methods %}
    def {{ method.name }}({{ ', '.join(method.args) }}){% if method.return_type %} -> {{ method.return_type }}{% endif %}:
        {% for line in method.body %}
        {{ line }}
        {% endfor %}
    {% endfor %}

步骤 3:渲染并保存

from jinja2 import Template
import time  # 仅用于示例,实际由模板 import

# 读取模板
with open("class_template.j2") as f:
    template_str = f.read()

template = Template(template_str)
source_code = template.render(**class_spec)

# 写入文件
with open("models/user.py", "w", encoding="utf-8") as f:
    f.write(source_code)

print("✅ 已生成 user.py")

✅ 输出 user.py

import time

class User:
    """动态生成的用户模型"""

    # 用户ID
    id: int = 0
    # 用户名
    name: str = ''
    email: str | None = None
    _created_at: float = time.time()

    def greet(self) -> str:
        return f"Hello, {self.name}!"

✅ 优势:

  • 完全可控、可审查
  • 支持复杂逻辑(条件渲染、循环)
  • 模板可版本管理、复用
  • 避免 exec 安全风险
  • 生成代码可通过 flake8/black 检查

🚀 方案二:pydantic 动态模型(适合数据模型)

若你的类本质是数据载体(如 API Schema、配置类),直接用 pydantic:

from pydantic import BaseModel, create_model
from typing import Optional

# 1. 用数据定义字段
fields = {
    'id': (int, 0),
    'name': (str, ''),
    'email': (Optional[str], None),
}

# 2. 动态创建模型类
UserModel = create_model('User', **fields)

# 3. 持久化为代码(利用 pydantic 的 schema 或手动)
import inspect
print(inspect.getsource(UserModel))  # ❌ 不可行!create_model 无源码

# ✅ 正确做法:用 pydantic2 的 `model_json_schema()` + 模板生成
schema = UserModel.model_json_schema()
# 再用模板(同上)生成 class 定义

或——更优:用 datamodel-code-generator 工具链

# 从 JSON Schema 生成 pydantic 模型
pip install datamodel-code-generator
datamodel-codegen --input user_schema.json --output user_model.py

⚙️ 方案三:AST 编程(工业级,100% 合法 Python)

用 ast 模块构建语法树,确保生成的代码语法绝对正确(适合 IDE 插件、代码生成器):

import ast
import astor  # pip install astor 用于 ast -> source

# 构建 class 节点
class_node = ast.ClassDef(
    name="Config",
    bases=[],
    keywords=[],
    body=[
        ast.AnnAssign(
            target=ast.Name(id="debug", ctx=ast.Store()),
            annotation=ast.Name(id="bool", ctx=ast.Load()),
            value=ast.Constant(value=True),
            simple=1
        ),
        ast.AnnAssign(
            target=ast.Name(id="host", ctx=ast.Store()),
            annotation=ast.Name(id="str", ctx=ast.Load()),
            value=ast.Constant(value="localhost"),
            simple=1
        ),
    ],
    decorator_list=[]
)

# 包装为模块
module = ast.Module(body=[class_node], type_ignores=[])

# 生成源码
source = astor.to_source(module)
with open("config.py", "w") as f:
    f.write(source)

输出:

class Config:
    debug: bool = True
    host: str = 'localhost'

✅ 优势:无字符串拼接错误,可被 ast.parse() 安全解析 🔧 适用:代码生成工具(如 ORM migration、Protobuf → Python)

📌 生产环境最佳实践建议

需求推荐方案
快速原型 / 内部工具Jinja2 模板(+ YAML 配置)
API/数据模型pydantic.create_model+ 代码生成器
IDE 插件 / 编译器后端ast+astor/libcst
避免运行时依赖生成.py文件 + Git 提交(而非运行时 exec)

🌰 附:完整可运行的 Jinja2 示例(复制即用)

# generate_class.py
from jinja2 import Template

TEMPLATE = '''
{%- if imports %}
{{ imports | join('\\n') }}
{%- endif %}

class {{ name }}:
    """{{ docstring }}"""

{%- for attr in attributes %}
    {%- if attr.comment %}# {{ attr.comment }}{% endif %}
    {{ attr.name }}: {{ attr.type }} = {{ attr.default }}
{%- endfor %}

{%- for method in methods %}
    def {{ method.name }}({{ method.args | join(', ') }}){% if method.return_type %} -> {{ method.return_type }}{% endif %}:
        {%- for line in method.body %}
        {{ line }}
        {%- endfor %}
{%- endfor %}
'''

def save_class_to_file(spec, filename):
    template = Template(TEMPLATE.strip())
    code = template.render(**spec)
    with open(filename, "w", encoding="utf-8") as f:
        f.write(code + "\n")
    print(f"✅ Saved to {filename}")

# 使用
spec = {
    "name": "DBConfig",
    "docstring": "数据库配置类",
    "imports": ["from typing import Optional"],
    "attributes": [
        {"name": "host", "type": "str", "default": "'localhost'"},
        {"name": "port", "type": "int", "default": "5432"},
        {"name": "password", "type": "Optional[str]", "default": "None", "comment": "建议使用环境变量"},
    ],
    "methods": [
        {
            "name": "url",
            "args": ["self"],
            "return_type": "str",
            "body": [
                "return f'postgresql://user:{self.password}@{self.host}:{self.port}/db'"
            ]
        }
    ]
}

save_class_to_file(spec, "db_config.py")

运行后生成 db_config.py ✅

需要我帮你:

欢迎告诉我你的具体场景,我可以定制方案!

使用 type() 动态创建类

您无需生成代码文件,可以直接在内存中创建和使用类。type() 函数可以动态地创建类,这比 exec() 更安全、更具结构性。

type(name, bases, dict):

Python

# 动态数据
class_name = "ConfigurableProcessor"
# 动态添加的方法
def run(self, data):
    return f"Processing data: {data}"

# 动态创建类的方法字典
class_methods = {
    'docstring': '一个基于配置动态创建的处理器。',
    'version': '1.0',
    'run': run  # 添加 run 方法
}

# 使用 type() 动态创建类
DynamicClassInMemory = type(
    class_name,  # 类名
    (object,),   # 继承自 object
    class_methods # 方法和属性
)

# 立即使用这个在内存中创建的类
processor = DynamicClassInMemory()
print(processor.run("report_A"))
# 检查类名和属性
print(f"Class Name: {processor.__class__.__name__}")
print(f"Class Version: {processor.version}")

Jinja 处理自定义类型

给自定义类型实现 __repr__,让模板直接使用

class Age:
    def __init__(self, years): self.years = years

    def __repr__(self):
        return f"Age({self.years})"
class User:
    age = {{ age_value }}

使用 Jinja Filter 自定义序列化方式(推荐)

如果你不想污染你的类(例如第三方类),你可以给 Jinja2 注册一个 filter:

def to_code(val):
    if hasattr(val, "to_code"):
        return val.to_code()  # 推荐方式
    return repr(val)

from jinja2 import Environment, FileSystemLoader

env = Environment(loader=FileSystemLoader("."))
env.filters["to_code"] = to_code
template = env.get_template("model.j2")

class User:
    age = {{ age | to_code }}

class Age:
    def __init__(self, years): self.years = years
    def to_code(self):
        return f"Age({self.years})"