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
相关产品推荐
相关产品推荐

