You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Pydantic模型中自定义对象默认参数未正确深度拷贝的问题

Pydantic自定义类型默认值深拷贝行为解析

你观察到Pydantic对作为默认值的内置可变对象(比如lst字段的空列表)会在每个实例创建时执行深拷贝,但自定义Spam对象的默认值却不遵循这个逻辑——多个Person实例共享同一个Spam对象,且深拷贝只在类定义时执行了一次。

测试代码

from copy import deepcopy
from typing import Self

from pydantic import BaseModel, ConfigDict


class Spam:
    def __init__(self) -> None:
        self.names = ["hi"]

    def __deepcopy__(self, memo: dict) -> Self:
        print("deepcopy called")
        cls = self.__class__
        result = cls.__new__(cls)
        memo[id(self)] = result
        for k, v in self.__dict__.items():
            setattr(result, k, deepcopy(v, memo))
        return result


class Person(BaseModel):
    model_config = ConfigDict(arbitrary_types_allowed=True)

    item: Spam = Spam()
    lst: list = []


print("-----------------------------------")

obj1 = Person()
obj2 = Person()

obj1.lst.append(10)
obj1.item.names.append("bye")

print(obj1.lst)
print(obj1.item.names)
print(obj2.lst)
print(obj2.item.names)
print(id(obj1.item) == id(obj2.item))

输出结果

deepcopy called
-----------------------------------
[10]
['hi', 'bye']
[]
['hi', 'bye']
True

原因分析

  • 对于内置可变类型(如list、dict),Pydantic会自动将直接赋值的默认值(比如lst: list = [])转换为动态默认值,等效于使用Field(default_factory=list)。这意味着每次创建Person实例时,都会调用list()生成新的空列表,避免实例间共享。
  • 对于自定义类型的实例默认值(比如item: Spam = Spam()),这个赋值语句是在Person类定义阶段执行的——此时会创建一个Spam实例,Pydantic会对这个实例做一次深拷贝并缓存,后续所有Person实例都会复用这个缓存的拷贝,而不是每次实例化时重新创建并拷贝。这就导致了多个Person实例共享同一个Spam对象。

这并不是你理解有误,而是Pydantic对内置类型和自定义类型的默认值处理逻辑不同:内置可变类型被特殊处理为动态生成,而自定义类型的直接实例赋值会被当作静态默认值缓存。


解决方案

要让自定义类型的默认值在每个实例创建时都生成独立对象,需要使用default_factory来定义动态默认值:

方案1:每次实例化新建对象

from copy import deepcopy
from typing import Self

from pydantic import BaseModel, ConfigDict, Field


class Spam:
    def __init__(self) -> None:
        self.names = ["hi"]

    def __deepcopy__(self, memo: dict) -> Self:
        print("deepcopy called")
        cls = self.__class__
        result = cls.__new__(cls)
        memo[id(self)] = result
        for k, v in self.__dict__.items():
            setattr(result, k, deepcopy(v, memo))
        return result


class Person(BaseModel):
    model_config = ConfigDict(arbitrary_types_allowed=True)

    # 使用default_factory每次实例化时创建新的Spam对象
    item: Spam = Field(default_factory=Spam)
    lst: list = []


print("-----------------------------------")

obj1 = Person()
obj2 = Person()

obj1.lst.append(10)
obj1.item.names.append("bye")

print(obj1.lst)          # 输出: [10]
print(obj1.item.names)   # 输出: ['hi', 'bye']
print(obj2.lst)          # 输出: []
print(obj2.item.names)   # 输出: ['hi']
print(id(obj1.item) == id(obj2.item))  # 输出: False

方案2:基于基准实例深拷贝

如果需要基于某个基准实例做深拷贝(而非每次新建),可以把基准实例放在default_factory的匿名函数中:

from copy import deepcopy
from typing import Self

from pydantic import BaseModel, ConfigDict, Field


class Spam:
    def __init__(self) -> None:
        self.names = ["hi"]

    def __deepcopy__(self, memo: dict) -> Self:
        print("deepcopy called")
        cls = self.__class__
        result = cls.__new__(cls)
        memo[id(self)] = result
        for k, v in self.__dict__.items():
            setattr(result, k, deepcopy(v, memo))
        return result

# 定义基准实例
base_spam = Spam()

class Person(BaseModel):
    model_config = ConfigDict(arbitrary_types_allowed=True)
    # 每次实例化对基准实例执行深拷贝
    item: Spam = Field(default_factory=lambda: deepcopy(base_spam))
    lst: list = []

这样每次创建Person实例时,都会生成一个独立的Spam拷贝,避免实例间共享状态。

内容的提问来源于stack exchange,提问作者S.B

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.29 17:42:15