【Fastapi学习笔记(3)】——资源的层级关系、安全性-幂等性、Field、工厂函数

资源的层级关系

示例代码:

bash 复制代码
# 用户和文章的关系设计
class User(BaseModel):
    id: int
    name: str
    email: str

class Post(BaseModel):
    id: int
    title: str
    content: str
    author_id: int

# 主资源操作
@app.get("/users")
async def list_users() -> List[User]:
    return users

@app.get("/posts")
async def list_posts() -> List[Post]:
    return posts

# 关联资源操作
@app.get("/users/{user_id}/posts")
async def get_user_posts(user_id: int) -> List[Post]:
    """获取特定用户的所有文章"""
    return [post for post in posts if post.author_id == user_id]

@app.get("/posts/{post_id}/author")
async def get_post_author(post_id: int) -> User:
    """获取文章的作者信息"""
    post = find_post(post_id)
    if not post:
        raise HTTPException(status_code=404, detail="Post not found")
    return find_user(post.author_id)

先一句话讲透:这是 RESTful 风格的「资源嵌套路由」,用来体现「用户 ↔ 文章」一对多的层级从属关系,下面逐层拆解,结合代码、关系、路由规则讲明白。


一、先理清业务实体关系

User(用户) 和 Post(文章)是 一对多

  • 一个用户可以发布多篇文章

  • 一篇文章只属于一个作者(用户)

  • Post.author_id 外键关联:文章里存作者ID,实现两者绑定

    User(1) ──┬── Post(1)
    ├── Post(2)
    └── Post(3)


二、两类路由的区别:平级路由 vs 嵌套路由

代码里分了两种接口,对应 REST 两种资源访问形式:

1. 平级路由(独立资源)

python 复制代码
# 查所有用户
@app.get("/users")
# 查所有文章
@app.get("/posts")

含义:把用户、文章当成两个独立顶级资源 ,直接全量查询,不体现从属关系。

访问示例:

  • GET /users → 全部用户
  • GET /posts → 全部文章

2. 嵌套路由(体现层级/从属关系,重点)

REST 规范里,从属资源会把父资源ID写在URL路径里,用来表达「谁的东西」。

/users/{user_id}/posts → 用户 → 旗下文章

路由语义:获取「指定用户」所拥有的全部文章

  • {user_id}:先定位父资源(某个用户)
  • 后面 /posts:再取该用户下的子资源(文章)

执行逻辑:

  1. 路径拿到 user_id
  2. 遍历所有文章,筛选出 author_id == user_id 的记录返回

访问示例:

GET /users/1/posts → 查询 ID=1 这个用户发的所有文章

/posts/{post_id}/author → 文章 → 所属作者

路由语义:获取「指定文章」对应的作者

  • {post_id}:先定位父资源(某篇文章)
  • 后面 /author:取该文章关联的子/关联资源(作者)

执行逻辑:

  1. 根据 post_id 找到对应文章
  2. 取出文章的 author_id
  3. 再根据作者ID查到对应用户并返回

访问示例:

GET /posts/5/author → 查询 ID=5 这篇文章的作者信息


三、为什么要这么设计?核心目的

  1. 语义直观,URL 自带业务关系

    看 URL 就知道层级:/用户ID/文章,一眼明白是「该用户的文章」,符合 REST 设计思想。

  2. 天然做数据过滤

    不用在查询参数里传 ?user_id=xxx,而是把归属关系融入路径,语义更标准。

    对比:

    • 不嵌套:/posts?user_id=1(参数传过滤条件)
    • 嵌套:/users/1/posts(路径表达归属)
  3. 统一资源访问范式

    后续新增操作也可以沿用这套层级,比如:

    • 给指定用户新增文章:POST /users/{user_id}/posts
    • 删除某用户下某篇文章:DELETE /users/{user_id}/posts/{post_id}

四、逐行解读关联接口代码

接口1:获取某个用户的所有文章

python 复制代码
@app.get("/users/{user_id}/posts")
async def get_user_posts(user_id: int) -> List[Post]:
    # 遍历全部文章,只返回作者ID等于当前用户ID的文章
    return [post for post in posts if post.author_id == user_id]

逻辑极简:靠外键 author_id 匹配,实现父子资源关联查询

接口2:获取某篇文章的作者

python 复制代码
@app.get("/posts/{post_id}/author")
async def get_post_author(post_id: int) -> User:
    # 1. 先根据文章ID找到文章
    post = find_post(post_id)
    if not post:
        raise HTTPException(status_code=404, detail="Post not found")
    # 2. 拿文章里的作者ID,反查用户
    return find_user(post.author_id)

反向关联:从子资源(文章)通过外键,回查父资源(用户)。


五、补充:两种设计风格对比(面试/实战常用)

方式 路由示例 特点 适用场景
平级路由 + 查询参数 /posts?user_id=1 简单灵活,URL短 简单查询、多条件筛选
嵌套层级路由 /users/1/posts 体现资源从属关系,REST 标准 一对多固定从属关系(用户-文章、分类-商品等)

你这段代码,就是典型 RESTful 嵌套资源路由,用 URL 结构 + 模型外键,双重表达「用户和文章的一对多层级关系」。


六、快速总结

  1. 实体关系:User 一对多 Post ,靠 author_id 字段关联;
  2. 路由设计:
    • /users / /posts:独立顶级资源,查全量;
    • /users/{id}/posts父→子,查某个用户的所有文章;
    • /posts/{id}/author子→父,查某篇文章的作者;
  3. 核心思想:用 URL 路径体现资源层级,用数据表字段实现数据关联

安全性-幂等性

一、两个核心定义

1. 安全性(Safe)

是否修改服务端资源状态

  • 安全 :只读操作,不会新增/修改/删除数据,服务端状态不变
  • 不安全:会改动服务端数据(增、改、删)

注意:安全 ≠ 不会报错,只是不改变业务数据。

2. 幂等性(Idempotent)

多次执行同一个请求,效果和返回结果 是否和执行一次完全一致

  • 幂等:请求发 1 次、N 次,最终数据、响应状态都一样
  • 非幂等:多发几次,数据/结果会变(重复请求会产生副作用)

二、逐 HTTP 方法拆解(结合你代码)

1. GET /users/{user_id}

标签:安全且幂等

  • 安全:只查询用户,不改动任何数据
  • 幂等:不管刷多少次接口,查到的数据不变,服务端无变化
  • 场景:纯查询类接口统一遵循该特性。

2. POST /users(创建用户)

标签:不安全且非幂等

  • 不安全:新增用户,修改了服务端数据
  • 非幂等:连续发两次相同请求 → 创建两条一模一样的用户,数据被重复新增,结果不同
  • 补充:所以创建接口一般用 POST,前端要做防重复提交。

3. PUT /users/{user_id}(全量替换资源)

标签:不安全但幂等

  • 不安全:覆盖/新建用户,改动数据
  • 幂等:
    1. 用户已存在:多次全量更新,最终数据完全一致;
    2. 用户不存在:按指定 ID 创建,多次请求也只会生成这一条数据;
  • 特点:PUT 语义是用请求体完整替换目标资源,天生设计为幂等。

4. PATCH /users/{user_id}(部分更新)

标签:不安全且通常非幂等

  • 不安全:修改部分字段,改动数据
  • 大多非幂等:
    例:接口逻辑是 积分 +1阅读数 +1,多次请求数值会不断累加,结果不一样;
  • 特例:如果只是 把昵称统一改成"test",这种简单赋值也能做到幂等,但业务上极少,所以约定为「通常非幂等」。

5. DELETE /users/{user_id}(删除用户)

标签:不安全但幂等

  • 不安全:删除数据,改变服务端状态
  • 幂等:
    1. 第一次请求:用户被删除,返回 204;
    2. 后续再删同个 ID:用户已不存在,依旧返回 204;
      多次请求最终状态一致,符合幂等。
      你代码里也体现了这一点:不存在也正常返回成功。

三、汇总对照表(方便记忆)

请求方法 安全性 幂等性 核心语义
GET 安全 幂等 查询资源
POST 不安全 非幂等 新建资源
PUT 不安全 幂等 全量替换资源
PATCH 不安全 通常非幂等 局部更新资源
DELETE 不安全 幂等 删除资源

四、补充实战要点(面试/开发常用)

  1. 为什么要关心这两个特性?

    用于接口设计、重试策略、网关限流、防重、分布式请求处理:

    • GET/ PUT/ DELETE 可放心做自动重试
    • POST、累加类 PATCH 不能随意重试,必须前端/业务层做防重。
  2. 易错点区分

    • 不要把「安全」理解成「防攻击」,REST 里的安全特指是否改数据
    • PUT 可以创建资源(按指定 ID),这是规范允许的,和 POST 创建(服务端分配ID)是两种设计。
  3. 结合你代码的细节

    DELETE 里判断用户不存在仍返回 204,就是刻意保证幂等的标准写法。

Field------Pydantic 字段增强核心工具

前置说明:

导入:from pydantic import Field

作用:对模型字段做元数据配置、默认值、数据校验、别名、文档、序列化控制,是 Pydantic 字段增强核心工具。


一、基础语法

python 复制代码
字段名: 类型 = Field(
    # 位置/关键字参数
    default=...,        # 静态默认值
    default_factory=...,# 动态工厂函数
    title=...,
    description=...,
    example=...,
    alias=...,
    validation规则...,
    exclude=...,
    include=...
)

两个特殊占位符

  1. Field(...) / ...:表示必填字段(无默认值,必须传参)
  2. None:显式默认值为 None(可选字段)

二、核心参数分类 + 逐参数详解 + 示例

按功能分为 6 大类:必填/默认值、数据校验、别名、文档注释、序列化控制、扩展配置

1. 默认值相关(最常用)

1.1 default:静态固定默认值

适用于字符串、数字、布尔等不可变类型,实例化时直接使用固定值。

python 复制代码
from pydantic import BaseModel, Field

class User(BaseModel):
    # 静态默认:性别默认男
    gender: str = Field(default="male")
    # 等价简写(简单固定值可直接写)
    age: int = 18

1.2 default_factory:动态生成默认值

重点 :接收无参函数每次实例化模型时执行一次 ,生成全新值。

适用场景:

  • 时间 datetime.now
  • 可变容器 list / dict / set(解决Python默认值共享坑)
  • 自定义对象、随机值等动态数据
示例1:时间戳(对应你之前的响应体)
python 复制代码
from datetime import datetime

class APIResp(BaseModel):
    # 每次创建实例,都获取当前时间
    timestamp: datetime = Field(default_factory=datetime.now)
示例2:可变容器 list/dict(经典避坑)
python 复制代码
# ❌ 错误写法:所有实例共享同一个列表,数据串扰
# tags: list = []

# ✅ 正确写法
class Article(BaseModel):
    tags: list[str] = Field(default_factory=list)
    extra: dict = Field(default_factory=dict)

a1 = Article()
a1.tags.append("技术")
a2 = Article()
print(a2.tags)  # [] 互不干扰
示例3:自定义工厂函数
python 复制代码
def get_random_code() -> str:
    import random
    return f"CODE_{random.randint(1000,9999)}"

class Order(BaseModel):
    code: str = Field(default_factory=get_random_code)

1.3 字段设为【必填】

使用 ... 表示该字段必须传值,无默认

python 复制代码
class User(BaseModel):
    # 必填,不允许为空
    username: str = Field(...)
    # 等价简写(推荐简单场景)
    password: str

2. 数据校验参数(约束字段格式/范围)

Pydantic 内置大量校验规则,Field 直接挂载使用,自动抛出校验异常,FastAPI 会自动捕获并返回 422。

2.1 字符串校验

  • min_length:最小长度
  • max_length:最大长度
  • pattern:正则表达式
python 复制代码
class User(BaseModel):
    # 用户名:3~20位
    username: str = Field(min_length=3, max_length=20, description="账号名称")
    # 手机号正则校验
    phone: str = Field(pattern=r"^1[3-9]\d{9}$")

2.2 数字校验(int/float)

  • gt:大于(greater than)
  • ge:大于等于(greater or equal)
  • lt:小于(less than)
  • le:小于等于(less or equal)
  • multiple_of:是某个数的倍数
python 复制代码
class Goods(BaseModel):
    # 价格 > 0
    price: float = Field(gt=0)
    # 数量 1 ~ 100
    count: int = Field(ge=1, le=100)

2.3 组合校验示例(FastAPI 表单常用)

python 复制代码
class RegisterForm(BaseModel):
    email: str = Field(max_length=50)
    pwd: str = Field(min_length=6, max_length=32, description="登录密码")

3. 别名相关:alias / alias_priority

3.1 alias 字段别名

作用:前端传参名 和 后端模型字段名不一致时做映射。

  • 接收请求时:优先使用 alias 名称解析
  • 响应返回时:默认返回原字段名(可配合序列化修改)
python 复制代码
class User(BaseModel):
    # 前端传 user_name,后端模型用 username
    username: str = Field(alias="user_name")

# 前端传 {"user_name": "张三"}
u = User(user_name="张三")
print(u.username)  # 张三

3.2 alias_priority 别名优先级

控制「别名」和「原字段名」的解析优先级,一般配合模型继承、多数据源使用。


4. 文档相关参数(FastAPI 自动生成接口文档)

FastAPI 的 Swagger / ReDoc 文档,完全读取 Field 中的文档配置

  • title:字段简短标题
  • description:字段详细描述
  • example:示例值(文档展示、调试默认填充)
  • examples:多组示例(数组)
python 复制代码
class LoginForm(BaseModel):
    account: str = Field(
        title="登录账号",
        description="支持手机号/邮箱登录",
        example="13800138000"
    )
    pwd: str = Field(
        description="6-32位密码",
        examples=["123456", "Abc@123456"]
    )

效果:打开 /docs 接口文档,会自动展示描述和示例。


5. 序列化控制:exclude / include

控制模型转字典/JSON 时,是否忽略该字段

常用于:隐藏密码、内部字段、临时字段。

5.1 exclude=True 序列化时排除字段

python 复制代码
class User(BaseModel):
    id: int
    name: str
    # 序列化时隐藏密码,不返回前端
    password: str = Field(exclude=True)

u = User(id=1, name="李四", password="123456")
print(u.model_dump())
# {'id': 1, 'name': '李四'}  无 password

5.2 include=True 强制包含(默认都包含,极少用)


6. 其他常用实用参数

6.1 repr=False 不在打印日志中显示

敏感字段(密码、密钥)禁止在 print()、日志中输出:

python 复制代码
class User(BaseModel):
    name: str
    # 打印对象时隐藏该字段
    secret_key: str = Field(repr=False)

u = User(name="王五", secret_key="abc123")
print(u)
# name='王五'  看不到 secret_key

6.2 frozen=True 字段只读(不可修改)

设置后字段实例化完成后禁止重新赋值

python 复制代码
class Config(BaseModel):
    app_id: str = Field(frozen=True)

c = Config(app_id="APP001")
# c.app_id = "APP002"  # 报错:字段已冻结,无法修改

三、Field 三种典型写法对比(必分清)

python 复制代码
from pydantic import BaseModel, Field
from datetime import datetime

class Demo(BaseModel):
    # 1. 简单固定默认值 → 直接赋值(推荐)
    name: str = "默认名称"

    # 2. 复杂配置 + 固定默认 → Field + default
    age: int = Field(default=18, ge=0, description="年龄")

    # 3. 动态值/可变容器 → Field + default_factory
    create_time: datetime = Field(default_factory=datetime.now)
    tags: list[str] = Field(default_factory=list)

    # 4. 必填字段(无默认)
    email: str = Field(..., description="邮箱,必填")

四、高频踩坑点(面试/实战易错)

坑1:可变类型直接写默认值

python 复制代码
# ❌ 错误:list/dict 不能直接当默认值
data: list = []

# ✅ 正确:必须用 default_factory
data: list = Field(default_factory=list)

原因:Python 函数/类默认值只会在定义时创建一次,所有实例共享同一个对象。

坑2:defaultdefault_factory 不能同时使用

二选一,同时写会报错。

坑3:default_factory 函数不能传参

如果需要传参,用嵌套函数/闭包包装:

python 复制代码
# 需求:默认拼接前缀
def make_prefix(prefix: str):
    def inner():
        return f"{prefix}_001"
    return inner

class Demo(BaseModel):
    code: str = Field(default_factory=make_prefix("ORDER"))

坑4:区分「安全默认值」

  • 不可变类型(int/str/bool)→ 直接赋值
  • 动态值/可变容器(datetime/list/dict)→ default_factory

五、FastAPI 综合实战完整示例

结合标准化响应体 + 校验 + 文档 + 动态时间,复刻你之前的代码并强化:

python 复制代码
from datetime import datetime
from typing import Optional, List
from pydantic import BaseModel, Field

class APIResponse(BaseModel):
    # 静态默认值
    success: bool = Field(default=True, description="请求是否成功")
    message: str = Field(default="操作成功", description="提示信息")
    
    # 可选字段,默认None
    data: Optional[dict] = Field(None, description="业务数据")
    errors: Optional[List[str]] = Field(None, description="错误详情列表")
    
    # 动态生成当前时间戳
    timestamp: datetime = Field(
        default_factory=datetime.now,
        description="接口响应时间"
    )

六、面试精简总结(背诵版)

  1. Field 是 Pydantic 用来增强模型字段的工具,扩展默认值、校验、别名、文档、序列化能力。
  2. default:设置静态固定默认值,用于普通类型。
  3. default_factory:接收无参函数,每次实例化动态生成值 ,专门解决 datetimelist/dict 等可变/动态类型的共享问题。
  4. 内置 gt/lt/min_length/pattern 等参数做自动数据校验,FastAPI 自动返回 422。
  5. alias 实现前后端字段名映射;description/example 驱动接口文档;exclude 控制序列化字段显隐。
  6. 核心原则:静态值用 default,动态/可变容器必须用 default_factory

工厂函数

一、最简定义

工厂函数

本身就是普通函数 ,作用是 专门用来「创建并返回对象/值」

它不做复杂业务逻辑,只负责生产数据、实例,像一个"加工厂"。

关键词:调用它 → 得到一个新值/新对象


二、先看普通函数 vs 工厂函数

1. 普通函数(做计算、逻辑)

python 复制代码
def add(a, b):
    return a + b  # 做加法运算,不是工厂

2. 工厂函数(只负责产出新数据)

无参、调用就返回新内容,就是最典型的工厂函数:

python 复制代码
# 工厂函数1:返回当前时间
from datetime import datetime
def get_now():
    return datetime.now()

# 工厂函数2:返回空列表
def empty_list():
    return []

# 工厂函数3:生成随机编号
import random
def gen_code():
    return f"ORD_{random.randint(1000,9999)}"

调用一次,产出一个全新结果,这就是工厂函数的核心。


三、结合你之前的代码:default_factory 为什么要传工厂函数?

回顾代码:

python 复制代码
from pydantic import BaseModel, Field
from datetime import datetime

class APIResponse(BaseModel):
    # default_factory 要求传入一个【工厂函数】
    timestamp: datetime = Field(default_factory=datetime.now)

1. 拆解 datetime.now

  • datetime.now函数本身(工厂函数)
  • datetime.now()函数调用,得到时间对象

2. 两种写法本质区别(重点)

写法1(错误:直接传调用结果)
python 复制代码
# ❌ 模块加载时,只执行 1 次,时间永久固定
timestamp: datetime = datetime.now()

程序一启动,立刻执行 datetime.now(),生成唯一固定时间,后面所有实例共用。

写法2(正确:传工厂函数本身)
python 复制代码
# ✅ 每次创建模型实例时,Pydantic 自动调用这个工厂函数
timestamp: datetime = Field(default_factory=datetime.now)

流程:

  1. 你创建 APIResponse() 实例
  2. Pydantic 内部自动执行 datetime.now()
  3. 拿到当前最新时间 赋值给 timestamp

👉 default_factory 接收的就是无参工厂函数,框架帮你在合适时机调用它生产值。


四、为什么列表/字典也必须用工厂函数?

Python 经典坑:可变对象默认值全局共享

Python 类属性的默认值,是在「定义类的时候」就一次性创建好,而非实例化对象时创建

因此需要注意:类属性默认值,不可变类型直接写,可变容器 (list/dict/set) 绝对不要直接赋值,优先用构造函数 / default_factory。

错误示范

python 复制代码
class Demo(BaseModel):
    # 加载类时创建唯一空列表,所有实例共用
    tags: list[str] = []  # tags在定义Demo类时就会被创建,而非实例化Demo时才创建

d1 = Demo()
d1.tags.append("python")

d2 = Demo()
print(d2.tags)  # ['python'] 数据串了!
用工厂函数修复
python 复制代码
class Demo(BaseModel):
    # 每次实例化 → 调用 list() → 生成全新空列表
    tags: list[str] = Field(default_factory=list)

d1 = Demo()
d1.tags.append("python")

d2 = Demo()
print(d2.tags)  # [] 正常隔离

这里 list 本身也是 Python 内置工厂函数 ,调用 list() 就生成新列表。


五、自定义工厂函数(实战扩展)

如果默认逻辑不够,自己写工厂函数:

python 复制代码
from pydantic import BaseModel, Field
import random

# 自定义工厂函数:生成随机订单号
def create_order_no():
    return f"ORDER_{random.randint(10000, 99999)}"

class Order(BaseModel):
    # 每次新建订单,自动调用工厂函数生成单号
    order_no: str = Field(default_factory=create_order_no)
    status: int = 0

o1 = Order()
o2 = Order()
print(o1.order_no)
print(o2.order_no)  # 两个编号完全不同

六、补充:两个容易混淆的概念(面试区分)

1. 本节讲的:简单工厂函数(Pydantic 场景)

  • 就是普通函数
  • 职责:生成一个值/内置对象(时间、列表、字符串)
  • 用途:给字段动态默认值

2. 设计模式:工厂模式 / 工厂方法(面向对象)

属于代码架构设计,用来创建复杂类实例,和上面不是一回事,只是名字都带"工厂"。

日常写 FastAPI + Pydantic 时,提到「工厂函数」,基本都是指 default_factory 这种值生成函数


七、一句话总结(背诵版)

  1. 工厂函数 = 专门用来生成新值/新对象的普通函数
  2. default_factory 要求传入无参工厂函数 ,框架会在每次实例化模型时自动调用
  3. 作用:解决动态时间、列表、字典这类可变/动态数据的默认值共享问题;
  4. 写法区别:传函数名 datetime.now,不要传调用 datetime.now()

详细解释------为什么列表/字典也必须用工厂函数?

核心原因:Python 类属性的默认值,是在「定义类的时候」就一次性创建好,而非实例化对象时创建 ,加上列表是可变对象,所有实例会共用这同一个列表。


一、先梳理完整执行顺序

第1步:解释器加载代码、定义 Demo

当 Python 读到 class Demo(BaseModel): 这一整块代码时,类就完成定义

python 复制代码
class Demo(BaseModel):
    tags: list[str] = []

此时会执行:

  1. 创建一个空列表对象 []
  2. 把类属性 Demo.tags 指向这个列表
    👉 这个过程只执行一次,和后续创建多少个实例无关。

第2步:创建实例 d1 = Demo()

创建实例时:

  • 实例 d1 本身没有独立的 tags 属性
  • 当访问 d1.tags,Python 会向上查找,找到类身上的那个全局列表
  • 执行 d1.tags.append("python"):直接修改原列表内容

第3步:创建实例 d2 = Demo()

同理,d2 也没有自己的 tags,访问时同样指向类里那唯一一个列表

所以打印 d2.tags,自然能看到 python


二、内存示意图(直观理解)

复制代码
# 类定义阶段,生成唯一列表内存地址
Demo 类 ────→ 列表对象 []  (内存地址:0x123)

# 创建 d1、d2 实例,都不新建列表
d1 实例 ───┐
           ├──→ 共用 0x123 这个列表
d2 实例 ───┘

# d1.append("python") → 0x123 列表变成 ["python"]
# 所有引用这个地址的实例,看到的数据都会同步变化

三、关键区分:可变对象 vs 不可变对象

这个坑只出现在 list / dict / set 这类可变对象上,不可变类型(str/int/bool)不会有问题。

1. 不可变类型(正常,不会串数据)

python 复制代码
class Demo:
    num: int = 0  # 类属性,定义时创建

d1 = Demo()
d1.num = 10   # 注意:这里是「给实例新增独立属性」,不是修改类属性
d2 = Demo()
print(d2.num) # 0  正常

原因:

int/str 赋值 d1.num = 10,Python 会直接在实例上新建一个专属属性,不再走类属性。

2. 可变类型(列表/字典,踩坑根源)

python 复制代码
class Demo:
    tags: list = []

d1 = Demo()
d1.tags.append("python") 
# 重点:append 是「原地修改对象内容」,没有给实例新建属性
# 所以依旧操作的是类上的共享列表

d2 = Demo()
print(d2.tags) # ['python']

总结一句话:

  • 可变对象调用方法改内容append/pop/字典增删键值):修改的是共享对象,所有实例受影响;
  • 变量直接赋值d1.tags = [1,2]):会给实例创建独立属性,不再共享。

四、两种修复方案(对应你之前的代码)

方案1:Pydantic 专属 → Field(default_factory)(推荐)

每次实例化,都会调用工厂函数生成全新列表,每个实例独有一份:

python 复制代码
from pydantic import BaseModel, Field

class Demo(BaseModel):
    tags: list[str] = Field(default_factory=list)

d1 = Demo()
d1.tags.append("python")
d2 = Demo()
print(d2.tags) # []  互不干扰

方案2:原生 Python 类写法(不用 Pydantic)

在构造函数 __init__ 里初始化列表,实例化时才创建:

python 复制代码
class Demo:
    def __init__(self):
        # 每个实例创建时,单独生成空列表
        self.tags = []

d1 = Demo()
d1.tags.append("python")
d2 = Demo()
print(d2.tags) # []

五、补充延伸(面试常问)

  1. 为什么 Pydantic 不推荐直接写 tags: list = []

    本质就是上面的共享可变对象问题,属于 Python 语法坑,Pydantic 模型同样受这个规则约束。

  2. default_factory=list 做了什么?

    Pydantic 在每次实例化对象时 ,自动执行 list(),生成新列表并绑定到当前实例,等价于原生类在 __init__ 里初始化。

  3. 避坑口诀:

    类属性默认值,不可变类型直接写,可变容器(list/dict/set) 绝对不要直接赋值 ,优先用构造函数 / default_factory

相关推荐
星恒随风2 小时前
Python 基础语法详解(一):从表达式、变量到数据类型
开发语言·笔记·python·学习
暴躁小师兄数据学院3 小时前
【AI大数据工程师特训笔记】第14讲:Linux操作系统与shell脚本
大数据·人工智能·笔记
tedcloud1234 小时前
cc-switch评测:多AI Coding Agent管理工具详解
数据库·人工智能·sql·学习·自动化
土狗TuGou4 小时前
SQL内功笔记 · 第8篇:事务的四大特性与隔离级别
数据库·笔记·后端·sql·mysql·oracle
胡图图不糊涂^_^4 小时前
测试BUG篇
学习·bug·测试
智者知已应修善业5 小时前
【51单片机用T0定时器方式1,实现0.5S的时间间隔实现第一次一个灯亮、第二次二个灯亮,直到全部灯亮,然后重复整个过程】2023-12-29
c++·经验分享·笔记·算法·51单片机
智者知已应修善业5 小时前
【51单片机4位静态数码管显示1234】2023-11-14
c++·经验分享·笔记·算法·51单片机
情绪总是阴雨天~5 小时前
智能语音分析Agent项目
python·自动化·fastapi·langgraph
whyTeaFo6 小时前
MIT6.1810: xv6 book Chapter4: Traps and system calls 笔记
笔记