pydantic 机制

mypy是类型检查工具,在不真正运行程序的情况下检查类型的定义与实际可能传入的值是否匹配。同时也能检查泛型的使用是否准确。 但即便mypy提示错误时,很多时候程序依然能正常运行。
pydantic是真正的运行时检查,遇到不匹配时直接报错。它是通过class.__anotation__这个属性去检查类字段的类型注解与类实例化时传入的类型是否匹配。

类级别注解

看下面示例代码:

# 类级别的注解,运行时就存在 __annotations__ 里(不需要 inspect)
class User:
    name: str
    age: int
    is_active: bool

print("类的字段注解字典:")
print(User.__annotations__)
print()
for field_name, field_type in User.__annotations__.items():
    print(f"  字段 {field_name!r} 的目标类型是 {field_type}")
    print(type(field_type))

输出结果:

类的字段注解字典:
{'name': <class 'str'>, 'age': <class 'int'>, 'is_active': <class 'bool'>}

  字段 'name' 的目标类型是 <class 'str'>
<class 'type'>
  字段 'age' 的目标类型是 <class 'int'>
<class 'type'>
  字段 'is_active' 的目标类型是 <class 'bool'>
<class 'type'>

只有注解但没有默认值的字段不属于类属性

class User:
name: str # 只有注解,没有默认值
age: int = 0 # 注解 + 默认值
city: str = "北京" # 注解 + 默认值

print("annotations (所有声明了类型的字段都在这)😊
print(" ", User.annotations)
print()

带默认值的字段,那个默认值变成了【类属性】;不带的则没有类属性

print("有没有类属性 name?", hasattr(User, "name"))   # False:name 只有注解
print("有没有类属性 age? ", hasattr(User, "age"))    # True: age 有默认值 0
print("User.age =", User.age)
print("User.city =", User.city)
print()

# 只出现在类命名空间(__dict__)里的,才是真存了默认值的
print("类命名空间里的字段默认值:")
for k in User.__annotations__:
    if k in User.__dict__:
        print(f"  {k} 有默认值 = {User.__dict__[k]!r}")
    else:
        print(f"  {k} 无默认值(必填)")

输出结果:

__annotations__ (所有声明了类型的字段都在这):
   {'name': <class 'str'>, 'age': <class 'int'>, 'city': <class 'str'>}

有没有类属性 name? False
有没有类属性 age?  True
User.age = 0
User.city = 北京

类命名空间里的字段默认值:
  name 无默认值(必填)
  age 有默认值 = 0
  city 有默认值 = '北京'

类型注解里的联合类型和嵌套类型

from typing import get_origin, get_args, Optional, Union

types_to_probe = [
    int,                # 朴素类型
    str,
    list[int],          # 带元素类型的容器
    dict[str, int],
    str | None,         # 新式联合(= Optional[str])
    Optional[int],      # 老式写法, 等价于 int | None
    list[str] | None,   # 嵌套: 可选的 list[str]
]

print(f"{'类型':<18} {'get_origin(拆外层)':<22} {'get_args(拆里层)'}")
print("-" * 70)
for t in types_to_probe:
    origin = get_origin(t)
    args = get_args(t)
    print(f"{str(t):<18} {str(origin):<22} {args}")

print()
# 关键判定:朴素类型的 get_origin 是 None
print("int 是朴素类型吗(origin is None)?", get_origin(int) is None)
print("list[int] 的外层是 list 吗?", get_origin(list[int]) is list)

# 联合类型的 origin:新式和老式
import types, typing
print("str|None 的 origin:", get_origin(str | None))       # types.UnionType
print("Optional[int] 的 origin:", get_origin(Optional[int]))  # typing.Union
print("None 在 args 里长这样:", get_args(str | None))         # (str, NoneType)
print("NoneType 是:", type(None))

输出结果:

类型                 get_origin(拆外层)        get_args(拆里层)
----------------------------------------------------------------------
<class 'int'>      None                   ()
<class 'str'>      None                   ()
list[int]          <class 'list'>         (<class 'int'>,)
dict[str, int]     <class 'dict'>         (<class 'str'>, <class 'int'>)
str | None         <class 'types.UnionType'> (<class 'str'>, <class 'NoneType'>)
typing.Optional[int] typing.Union           (<class 'int'>, <class 'NoneType'>)
list[str] | None   <class 'types.UnionType'> (list[str], <class 'NoneType'>)

int 是朴素类型吗(origin is None)? True
list[int] 的外层是 list 吗? True
str|None 的 origin: <class 'types.UnionType'>
Optional[int] 的 origin: typing.Union
None 在 args 里长这样: (<class 'str'>, <class 'NoneType'>)
NoneType 是: <class 'NoneType'>

简单模拟pydantic

from typing import get_origin, get_args, Union
import types
NoneType = type(None)


def convert(tp, value):
    origin = get_origin(tp)
    if origin is None:                                  # 朴素类型
        return tp(value)
    if origin is types.UnionType or origin is Union:    # 联合 X|None / Optional[X]
        args = get_args(tp)
        if value is None and NoneType in args:
            return None
        candidates = [a for a in args if a is not NoneType]
        for a in candidates:                            # (1) 已匹配就保留
            if get_origin(a) is None and isinstance(value, a):
                return value
        for a in candidates:                            # (2) 否则依次尝试转换
            try:
                return convert(a, value)
            except (ValueError, TypeError):
                continue
        raise ValueError(f"{value!r} 无法匹配 {tp}")
    if origin is list:                                  # list[X]:逐元素递归
        (elem_type,) = get_args(tp)
        return [convert(elem_type, item) for item in value]
    raise Exception(f"暂不支持的类型: {tp}")


class MiniModel:
    def __init__(self, **kwargs):
        for kn, kt in self.__annotations__.items():
            if kn not in kwargs:
                if hasattr(self, kn):
                    raw = getattr(self, kn)
                else:
                    raise Exception(f"缺少必填字段: {kn}")
            else:
                raw = kwargs[kn]
            try:
                val = convert(kt, raw)
            except (ValueError, TypeError) as e:
                raise Exception(f"字段 {kn} 校验失败: {raw!r} -> {kt} ({e})")
            setattr(self, kn, val)

    def __repr__(self):
        fields = " ".join(f"{k}={getattr(self, k)!r}" for k in self.__annotations__)
        return f"<{type(self).__name__} {fields}>"


class User(MiniModel):
    name: str
    age: int = 0
    nickname: str | None = None
    scores: list[int] = []


print(User(name="小明"))
print(User(name="小红", age="25", nickname="红红"))
u = User(name="小刚", scores=[1, "2", 3])
print(u, "| scores 元素类型:", [type(x).__name__ for x in u.scores])
try:
    User(age=5)
except Exception as e:
    print("预期错误:", e)

输出结果:

<User name='小明' age=0 nickname=None scores=[]>
<User name='小红' age=25 nickname='红红' scores=[]>
<User name='小刚' age=0 nickname=None scores=[1, 2, 3]> | scores 元素类型: ['int', 'int', 'int']
预期错误: 缺少必填字段: name

真实的 pydantic

from pydantic import BaseModel, ValidationError

# 和我们玩具版一模一样的字段声明,只是基类换成真 pydantic 的 BaseModel
class User(BaseModel):
    name: str
    age: int = 0
    nickname: str | None = None
    scores: list[int] = []


# 1) 默认值 + 强制转换 + Optional + list 元素转换 —— 和我们的玩具版行为一致
print(User(name="小明"))
print(User(name="小红", age="25", nickname="红红"))
u = User(name="小刚", scores=[1, "2", 3])
print(u, "| scores 元素类型:", [type(x).__name__ for x in u.scores])

# 2) 它比我们多了什么:结构化的、超详细的错误(不是一句话)
print("\n--- 缺字段 + 类型错误,一次性报全 ---")
try:
    User(age="abc", scores=[1, "x"])   # name缺失 + age转不了 + scores[1]转不了
except ValidationError as e:
    print(e)

# 3) union smart mode:和你亲手推导的一致
class Box(BaseModel):
    v: str | int | float | None

print("\n--- union smart mode(你独立推导出的行为)---")
print("传 float 12.3 ->", repr(Box(v=12.3).v))   # 保留 float
print("传 int 99    ->", repr(Box(v=99).v))      # 保留 int
print("传 None       ->", repr(Box(v=None).v))    # 允许 None

# 4) 它还能反向导出:模型 -> dict / json,以及 JSON Schema
print("\n--- 导出能力 ---")
print("model_dump():", u.model_dump())
print("model_dump_json():", u.model_dump_json())

输出结果:

name='小明' age=0 nickname=None scores=[]
name='小红' age=25 nickname='红红' scores=[]
name='小刚' age=0 nickname=None scores=[1, 2, 3] | scores 元素类型: ['int', 'int', 'int']

--- 缺字段 + 类型错误,一次性报全 ---
3 validation errors for User
name
  Field required [type=missing, input_value={'age': 'abc', 'scores': [1, 'x']}, input_type=dict]
    For further information visit https://errors.pydantic.dev/2.13/v/missing
age
  Input should be a valid integer, unable to parse string as an integer [type=int_parsing, input_value='abc', input_type=str]
    For further information visit https://errors.pydantic.dev/2.13/v/int_parsing
scores.1
  Input should be a valid integer, unable to parse string as an integer [type=int_parsing, input_value='x', input_type=str]
    For further information visit https://errors.pydantic.dev/2.13/v/int_parsing

--- union smart mode(你独立推导出的行为)---
传 float 12.3 -> 12.3
传 int 99    -> 99
传 None       -> None

--- 导出能力 ---
model_dump(): {'name': '小刚', 'age': 0, 'nickname': None, 'scores': [1, 2, 3]}
model_dump_json(): {"name":"小刚","age":0,"nickname":null,"scores":[1,2,3]}

pydantic的Filed机制模拟

Filed用来给字段加业务规则,它被放在字段默认值的位置,而它内部将真正的默认值和其他业务规则封装成了一个对象。在解析时,先从Filed里拆解出真实的默认值和业务规则。

class Field:
    def __init__(self, default=None, gt=None, lt=None, min_length=None):
        self.default = default
        self.gt = gt
        self.lt = lt
        self.min_length = min_length
    def __repr__(self):
        return f"Field(default={self.default!r}, gt={self.gt}, min_length={self.min_length})"


class User:
    name: str
    age: int = Field(default=0, gt=0)          # 右边是 Field 对象,不是普通默认值
    bio: str = Field(default="", min_length=2)


# Field 对象坐进了类属性(和普通默认值一样的位置)
print("User.age 是什么?", User.age)
print("它是 Field 吗?", isinstance(User.age, Field))
print("从里面拆出:真默认值 =", User.age.default, ", gt =", User.age.gt)
print()

# 对比:普通默认值 vs Field 默认值,都在 __dict__ 里,但一个是裸值一个是 Field
class Mixed:
    a: int = 5                 # 普通默认值
    b: int = Field(default=5, gt=0)   # Field
for k in ("a", "b"):
    v = Mixed.__dict__[k]
    print(f"{k}: {v!r}  -> 是 Field 吗? {isinstance(v, Field)}")

输出结果:

User.age 是什么? Field(default=0, gt=0, min_length=None)
它是 Field 吗? True
从里面拆出:真默认值 = 0 , gt = 0

a: 5  -> 是 Field 吗? False
b: Field(default=5, gt=0, min_length=None)  -> 是 Field 吗? True

@field_validator 机制模拟

  1. field_validator("name") 先返回一个装饰器;
  2. 这个装饰器不改变函数行为,只往函数上贴一个标记(比如 func._validates = ("name",)),记录"我是用来校验 name 字段的";
  3. 模型在初始化时扫描自己所有方法,找出带标记的,建一张 {字段名: [校验函数]} 表;
def field_validator(*fields):
    """装饰器工厂:给函数贴上"我校验哪些字段"的标记。"""
    def deco(func):
        func._validates = fields          # ★ 打标记
        return classmethod(func)          # 包成 classmethod, 调用时自动传 cls
    return deco


class User:
    name: str
    city: str

    @field_validator("name", "city")
    def strip_and_check(cls, v):          # 校验函数:(cls, 值) -> 处理后的值
        v = v.strip()
        if len(v) < 2:
            raise ValueError("name 太短")
        return v.capitalize()             # 返回值会替换原值


# ---- 收集:扫描类,找出带 _validates 标记的方法 ----
def collect_validators(cls):
    result = {}                            # 字段名 -> [校验函数]
    for attr_name in vars(cls):            # 遍历类自己的命名空间
        attr = getattr(cls, attr_name)     # 拿到(绑定后的)方法
        fields = getattr(attr, "_validates", None)   # 标记还在吗?
        if fields:
            for f in fields:
                result.setdefault(f, []).append(attr)
    return result


validators = collect_validators(User)
print("收集到的校验器:", validators)

# 手动跑一下 name 的校验器
for fn in validators["name"]:
    print("  '  alice ' 校验后 ->", repr(fn("  alice ")))   # 注意 cls 被自动传, 只需给值

输出结果:

收集到的校验器: {'name': [<bound method User.strip_and_check of <class '__main__.User'>>], 'city': [<bound method User.strip_and_check of <class '__main__.User'>>]}
  '  alice ' 校验后 -> 'Alice'

使用Filed和@filed_validator完善一下对pydantic检查的模拟

from typing import get_origin, get_args, Union
import types

NoneType = type(None)
MISSING = object()          # 哨兵:表示"没给默认值" -> 必填


# ============ 1. 递归类型转换(已有)============
def convert(tp, value):
    origin = get_origin(tp)
    if origin is None:
        return tp(value)
    if origin is types.UnionType or origin is Union:
        args = get_args(tp)
        if value is None and NoneType in args:
            return None
        candidates = [a for a in args if a is not NoneType]
        for a in candidates:
            if get_origin(a) is None and isinstance(value, a):
                return value
        for a in candidates:
            try:
                return convert(a, value)
            except (ValueError, TypeError):
                continue
        raise ValueError(f"{value!r} 无法匹配 {tp}")
    if origin is list:
        (elem_type,) = get_args(tp)
        return [convert(elem_type, item) for item in value]
    raise Exception(f"暂不支持的类型: {tp}")


# ============ 2. 声明式约束 Field ============
class Field:
    def __init__(self, default=MISSING, gt=None, lt=None, min_length=None):
        self.default = default
        self.gt = gt
        self.lt = lt
        self.min_length = min_length

    def check(self, name, value):
        if self.gt is not None and not (value > self.gt):
            raise ValueError(f"{name} 必须 > {self.gt}, 实际 {value}")
        if self.lt is not None and not (value < self.lt):
            raise ValueError(f"{name} 必须 < {self.lt}, 实际 {value}")
        if self.min_length is not None and len(value) < self.min_length:
            raise ValueError(f"{name} 长度必须 >= {self.min_length}, 实际 {len(value)}")


# ============ 3. 自定义校验器装饰器 ============
def field_validator(*fields):
    def deco(func):
        func._validates = fields           # 打标记
        return classmethod(func)           # 调用时自动传 cls
    return deco


# ============ 4. 基类:把三样串起来 ============
class MiniModel:
    def __init__(self, **kwargs):
        cls = type(self)
        validators = self._collect_validators(cls)     # {字段名: [校验器]}

        for kn, kt in self.__annotations__.items():
            attr = getattr(cls, kn, MISSING)           # 类上的东西:Field / 普通默认值 / MISSING

            # --- 归一化成 (约束对象 field, 有无默认值, 默认值) ---
            if isinstance(attr, Field):
                field = attr
                has_default = field.default is not MISSING
                default = field.default
            elif attr is MISSING:
                field, has_default, default = None, False, None
            else:                                      # 普通默认值 age: int = 5
                field, has_default, default = None, True, attr

            # --- 取原始值 ---
            if kn in kwargs:
                raw = kwargs[kn]
            elif has_default:
                raw = default
            else:
                raise Exception(f"缺少必填字段: {kn}")

            # --- 转换 -> 声明式约束 -> 自定义校验器 ---
            val = convert(kt, raw)
            if field is not None:
                field.check(kn, val)
            for fn in validators.get(kn, []):
                val = fn(val)                          # 校验器可返回新值(转换)

            setattr(self, kn, val)

    @staticmethod
    def _collect_validators(cls):
        result = {}
        for attr_name in vars(cls):
            attr = getattr(cls, attr_name)
            for f in getattr(attr, "_validates", ()):
                result.setdefault(f, []).append(attr)
        return result

    def __repr__(self):
        fields = " ".join(f"{k}={getattr(self, k)!r}" for k in self.__annotations__)
        return f"<{type(self).__name__} {fields}>"


# ============ 用法 ============
class User(MiniModel):
    name: str = Field(min_length=2)
    age: int = Field(default=18, gt=0, lt=150)
    nickname: str | None = None
    scores: list[int] = []

    @field_validator("name")
    def normalize_name(cls, v):
        return v.strip().capitalize()      # 顺便清洗

    @field_validator("scores")
    def no_negative(cls, v):
        if any(s < 0 for s in v):
            raise ValueError("scores 不能有负数")
        return v


print(User(name="  alice ", age="25", scores=[1, "2", 3]))   # 正常 + 清洗 + 转换
print(User(name="bob"))                                       # 默认值
print()
for bad in [
    dict(name="x", age=25),           # name 太短
    dict(name="alice", age=-5),       # age <= 0
    dict(name="alice", age=200),      # age >= 150
    dict(name="alice", scores=[1, -2]),  # 自定义校验器拦截
    dict(age=25),                     # 缺 name
]:
    try:
        User(**bad)
    except Exception as e:
        print(f"{bad}  ->  {e}")

运行结果:

<User name='Alice' age=25 nickname=None scores=[1, 2, 3]>
<User name='Bob' age=18 nickname=None scores=[]>

{'name': 'x', 'age': 25}  ->  name 长度必须 >= 2, 实际 1
{'name': 'alice', 'age': -5}  ->  age 必须 > 0, 实际 -5
{'name': 'alice', 'age': 200}  ->  age 必须 < 150, 实际 200
{'name': 'alice', 'scores': [1, -2]}  ->  scores 不能有负数
{'age': 25}  ->  缺少必填字段: name

真实使用pydantic的写法

from pydantic import BaseModel, Field, field_validator, ValidationError

class User(BaseModel):
    name: str = Field(min_length=2)
    age: int = Field(default=18, gt=0, lt=150)
    nickname: str | None = None
    scores: list[int] = []

    @field_validator("name")
    @classmethod
    def normalize_name(cls, v):
        return v.strip().capitalize()

    @field_validator("scores")
    @classmethod
    def no_negative(cls, v):
        if any(s < 0 for s in v):
            raise ValueError("scores 不能有负数")
        return v


print(User(name="  alice ", age="25", scores=[1, "2", 3]))
print(User(name="bob"))
print()
for bad in [
    dict(name="x", age=25),
    dict(name="alice", age=-5),
    dict(name="alice", age=200),
    dict(name="alice", scores=[1, -2]),
    dict(age=25),
]:
    try:
        User(**bad)
    except ValidationError as e:
        # 只打印第一条错误的简短信息,方便对照
        err = e.errors()[0]
        print(f"{bad}  ->  {err['loc']} {err['msg']}")

输出结果:

name='Alice' age=25 nickname=None scores=[1, 2, 3]
name='Bob' age=18 nickname=None scores=[]

{'name': 'x', 'age': 25}  ->  ('name',) String should have at least 2 characters
{'name': 'alice', 'age': -5}  ->  ('age',) Input should be greater than 0
{'name': 'alice', 'age': 200}  ->  ('age',) Input should be less than 150
{'name': 'alice', 'scores': [1, -2]}  ->  ('scores',) Value error, scores 不能有负数
{'age': 25}  ->  ('name',) Field required

模型嵌套机制

from typing import get_origin, get_args, Union
import types

NoneType = type(None)
MISSING = object()


# ============ 递归类型转换(新增嵌套模型分支)============
def convert(tp, value):
    # 新增:字段类型是 MiniModel 子类 -> 递归构造子模型
    if isinstance(tp, type) and issubclass(tp, MiniModel):
        if isinstance(value, tp):
            return value                # 已经是模型实例,直接用
        return tp(**value)              # value 是 dict,展开成 kwargs 递归构造

    origin = get_origin(tp)
    if origin is None:
        return tp(value)
    if origin is types.UnionType or origin is Union:
        args = get_args(tp)
        if value is None and NoneType in args:
            return None
        candidates = [a for a in args if a is not NoneType]
        for a in candidates:
            if get_origin(a) is None and isinstance(value, a):
                return value
        for a in candidates:
            try:
                return convert(a, value)
            except (ValueError, TypeError):
                continue
        raise ValueError(f"{value!r} 无法匹配 {tp}")
    if origin is list:
        (elem_type,) = get_args(tp)
        return [convert(elem_type, item) for item in value]   # 元素若是模型,自动递归
    raise Exception(f"暂不支持的类型: {tp}")


# ============ 声明式约束 Field ============
class Field:
    def __init__(self, default=MISSING, gt=None, lt=None, min_length=None):
        self.default = default
        self.gt = gt
        self.lt = lt
        self.min_length = min_length

    def check(self, name, value):
        if self.gt is not None and not (value > self.gt):
            raise ValueError(f"{name} 必须 > {self.gt}, 实际 {value}")
        if self.lt is not None and not (value < self.lt):
            raise ValueError(f"{name} 必须 < {self.lt}, 实际 {value}")
        if self.min_length is not None and len(value) < self.min_length:
            raise ValueError(f"{name} 长度必须 >= {self.min_length}, 实际 {len(value)}")


# ============ 自定义校验器 ============
def field_validator(*fields):
    def deco(func):
        func._validates = fields
        return classmethod(func)
    return deco


# ============ 基类 ============
class MiniModel:
    def __init__(self, **kwargs):
        cls = type(self)
        validators = self._collect_validators(cls)

        for kn, kt in self.__annotations__.items():
            attr = getattr(cls, kn, MISSING)

            if isinstance(attr, Field):
                field = attr
                has_default = field.default is not MISSING
                default = field.default
            elif attr is MISSING:
                field, has_default, default = None, False, None
            else:
                field, has_default, default = None, True, attr

            if kn in kwargs:
                raw = kwargs[kn]
            elif has_default:
                raw = default
            else:
                raise Exception(f"缺少必填字段: {kn}")

            val = convert(kt, raw)
            if field is not None:
                field.check(kn, val)
            for fn in validators.get(kn, []):
                val = fn(val)

            setattr(self, kn, val)

    @staticmethod
    def _collect_validators(cls):
        result = {}
        for attr_name in vars(cls):
            attr = getattr(cls, attr_name)
            for f in getattr(attr, "_validates", ()):
                result.setdefault(f, []).append(attr)
        return result

    def __repr__(self):
        fields = " ".join(f"{k}={getattr(self, k)!r}" for k in self.__annotations__)
        return f"<{type(self).__name__} {fields}>"


# ============ 用法:嵌套模型 ============
class Address(MiniModel):
    city: str
    zipcode: str = ""


class User(MiniModel):
    name: str
    address: Address                       # 嵌套:单个子模型
    friends: list[Address] = []            # 嵌套:子模型的列表


u = User(
    name="小明",
    address={"city": "北京", "zipcode": "100000"},
    friends=[{"city": "上海"}, {"city": "广州", "zipcode": "510000"}],
)
print(u)
print("address 类型:", type(u.address).__name__, "| city:", u.address.city)
print("friends[0]:", u.friends[0], "| 类型:", type(u.friends[0]).__name__)
print()

# 传入已经是 Address 实例也可以(走 isinstance 旁路)
addr = Address(city="深圳")
print(User(name="小红", address=addr).address is addr, "<- 已是实例,直接复用")

# 嵌套里的校验也会递归触发:子模型缺必填字段
print("\n--- 嵌套校验递归触发 ---")
try:
    User(name="小刚", address={"zipcode": "123"})    # Address 缺 city
except Exception as e:
    print("捕获:", e)

pydantic嵌套用法

from pydantic import BaseModel, ValidationError

class Address(BaseModel):
    city: str
    zipcode: str = ""

class User(BaseModel):
    name: str
    address: Address
    friends: list[Address] = []

u = User(
    name="小明",
    address={"city": "北京", "zipcode": "100000"},
    friends=[{"city": "上海"}, {"city": "广州", "zipcode": "510000"}],
)
print("address 类型:", type(u.address).__name__, "| city:", u.address.city)
print("friends[0] 类型:", type(u.friends[0]).__name__, "| city:", u.friends[0].city)

# 嵌套校验递归 + 错误路径能精确定位到深层字段
print("\n--- 嵌套校验递归 ---")
try:
    User(name="小刚", address={"zipcode": "123"}, friends=[{"city": "x"}, {}])
except ValidationError as e:
    for err in e.errors():
        print(f"  loc={err['loc']}  {err['msg']}")

# 嵌套模型也能整体导出成嵌套 dict / json
print("\n--- 嵌套导出 ---")
print(u.model_dump())

fastapi中对pydantic的使用机制

这个 demo 真 fastapi
@post("/users") 存路由表 @app.post("/users")
inspect.signature 读 handler 参数 fastapi 启动时就用 inspect 解析每个 handler
issubclass(anno, BaseModel) 判断 fastapi 正是这样区分"这个参数是请求体模型,还是查询参数/路径参数"
anno(**body_dict) 校验 就是你写的 Model(**dict)——嵌套模型那一步的同一个动作
失败返回 422 + errors fastapi 校验失败也是 422,body 就是 e.errors()
import inspect
import json
from pydantic import BaseModel, ValidationError


# ---- 用户定义的"请求体模型"(和 fastapi 里一模一样)----
class CreateUser(BaseModel):
    name: str
    age: int
    tags: list[str] = []


# ---- 迷你框架:路由表 + 装饰器(你的 step1)----
routes = {}

def post(path):
    def deco(func):
        routes[path] = func
        return func
    return deco


# ---- 迷你框架:收到请求时,解析签名并注入(你的 step2/3 + pydantic)----
def handle_request(path: str, raw_body: str):
    func = routes[path]
    sig = inspect.signature(func)                 # 读 handler 的签名(step2/3 的老套路)
    kwargs = {}
    for pname, param in sig.parameters.items():
        anno = param.annotation
        # 关键判断:参数注解是不是一个 BaseModel 子类?
        if isinstance(anno, type) and issubclass(anno, BaseModel):
            body_dict = json.loads(raw_body)      # HTTP body(JSON 文本)-> dict
            try:
                kwargs[pname] = anno(**body_dict) # ★ 就是你写的 Model(**dict)!校验在这一步发生
            except ValidationError as e:
                return {"status": 422, "errors": e.errors()}   # fastapi 校验失败也是返回 422
    result = func(**kwargs)                        # 把校验好的模型注入 handler
    return {"status": 200, "data": result}


# ---- 用户写的 handler:参数直接声明成模型,拿到的就是校验好的对象 ----
@post("/users")
def create_user(user: CreateUser):
    # 到这里 user 已经是校验+转换好的 CreateUser 实例,业务代码无需再做任何校验
    return f"创建用户 {user.name}, 年龄 {user.age}, 标签 {user.tags}"


# ============ 模拟两个 HTTP 请求 ============
print("=== 正常请求(注意 age 是字符串,会被转成 int)===")
print(handle_request("/users", '{"name": "小明", "age": "30", "tags": ["vip", "new"]}'))

print("\n=== 非法请求(age 传了转不了的字符串)===")
print(handle_request("/users", '{"name": "小红", "age": "abc"}'))

langchain中要求模型返回结构化内容时使用pydantic方式

"""
重现 langchain with_structured_output 的机制:
  1. 模型 -> JSON Schema(告诉 LLM"请按这个结构返回")
  2. LLM 返回 JSON 文本(这里用一个假 LLM 模拟)
  3. 文本 -> dict -> Model(**dict) 校验,得到结构化对象
"""
import json
from pydantic import BaseModel, Field, ValidationError


# ---- 用户想要的输出结构 ----
class PersonInfo(BaseModel):
    name: str = Field(description="人物姓名")
    age: int = Field(description="年龄")
    hobbies: list[str] = Field(default=[], description="爱好列表")


# ===== 步骤1:模型 -> JSON Schema(发给 LLM 的"输出格式说明")=====
schema = PersonInfo.model_json_schema()
print("=== 步骤1:发给 LLM 的 JSON Schema ===")
print(json.dumps(schema, ensure_ascii=False, indent=2))


# ===== 步骤2:假 LLM。真实里这是 LLM 读了 schema + 你的问题后生成的文本 =====
def fake_llm(prompt: str) -> str:
    # 模拟 LLM 返回一段 JSON 文本(注意 age 它给成了字符串,很常见)
    return '{"name": "爱因斯坦", "age": "76", "hobbies": ["小提琴", "物理"]}'


# ===== 步骤3:解析文本 -> 校验成 pydantic 对象(就是 Model(**dict))=====
def with_structured_output(model_cls, llm_text: str):
    data = json.loads(llm_text)            # LLM 文本 -> dict
    return model_cls(**data)              # ★ 又是 Model(**dict),校验+转换在此

print("\n=== 步骤2/3:LLM 返回文本 -> 校验成对象 ===")
raw = fake_llm("介绍一下爱因斯坦")
print("LLM 原始文本:", raw)
person = with_structured_output(PersonInfo, raw)
print("解析成对象:", person)
print("person.age =", person.age, type(person.age).__name__, "  <- 字符串'76'被转成 int")

# ===== 如果 LLM 返回不合规的结构,校验会拦住(LLM 幻觉的护栏)=====
print("\n=== LLM 返回缺字段时,校验兜底 ===")
bad = '{"name": "牛顿"}'                    # 少了 age
try:
    with_structured_output(PersonInfo, bad)
except ValidationError as e:
    print("校验拦截:", e.errors()[0]['loc'], e.errors()[0]['msg'])
posted @ 2026-07-07 08:21  RolandHe  阅读(5)  评论(0)    收藏  举报