为什么 pydantic 中的可变对象没有随着修改变化呢?

from datetime import datetime
from typing import List, Optional
from pydantic import BaseModel


class User(BaseModel):
    friends: List[int] = []


user_1 = User()
user_1.friends.append(1)
print(user_1.friends)

user_2 = User()
print(user_2.friends)

上面的代码,运行后输出如下:

[1]
[]

我有一个疑问,就是 friends 的默认值是一个 [] 空列表,通过前后两次实例化,两个实例对象持有的 friends 为什么指向的不是同一个 list 呢?


如果去掉继承 BaseModel,输出的两个就都是 [1]

from datetime import datetime
from typing import List, Optional
from pydantic import BaseModel


class User():
    friends: List[int] = []


user_1 = User()
user_1.friends.append(1)
print(user_1.friends)

user_2 = User()
print(user_2.friends)

输出

[1]
[1]

pydantic 的 BaseModel 施加了什么“魔法”?

阅读 1.8k
2 个回答

主要是默认值带来的问题。

列表是可变对象,当没有指定默认值时,每个实例都会创建一个新的空列表对象。但是,在指定了默认值后,所有实例都会共享同一个默认值对象,因为默认值只会创建一次并在所有实例之间共享。因此,user_1和user_2的属性friends内存指向相同。

如果继承了BaseModel,每个实例都会创建一个新的默认值对象,因为Pydantic会在内部创建一个新的配置类,以确保每个实例都有自己的默认值。因此如果实例化多个User类,并访问它们的friends属性,每个实例的friends属性都应该具有不同的内存地址。

pydantic/main.py(早期的实现,为了更容易说明这个问题)中,可以看到这个过程:

class ModelMetaclass(type):
    def __new__(mcs, name, bases, namespace):
        cls = super().__new__(mcs, name, bases, namespace)
        if not is_base(cls):
            config = getattr(cls, 'Config', BaseConfig)
            cls.__config__ = config
            cls.__fields__ = {}
            cls.__validators__ = {}
            cls.__initialised__ = False
            cls.__post_init_original__ = getattr(cls, '__post_init__', None)
            cls.__pre_root_validators__ = getattr(cls, '__pre_root_validators__', [])
            cls.__pre_validators__ = getattr(cls, '__pre_validators__', {})
            cls.__root_validators__ = getattr(cls, '__root_validators__', [])
            cls.__validators__ = getattr(cls, '__validators__', {})
            cls.__custom_root_type__ = None
            cls.__custom_root_type_params__ = {}
            cls.__json_encoder__ = getattr(cls, '__json_encoder__', None)
            cls.__fields_set__ = set()
            cls.__hash__ = None
            cls.__module__ = namespace.get('__module__')
            cls.__name__ = namespace.get('__qualname__') or name
            cls.__annotations__ = namespace.get('__annotations__', {})

            # Set up fields and validators
            for name, value in namespace.items():
                if isinstance(value, Field):
                    value.name = name
                    cls.__fields__[name] = value
                elif isinstance(value, Validator):
                    cls.__validators__.setdefault(name, []).append(value)
            cls.__validators__.update(cls.__pre_validators__)
            cls.__validators__['__root__'] = cls.__pre_root_validators__ + cls.__root_validators__

            # Set up field defaults
            for f in cls.__fields__.values():
                if f.has_default():
                    setattr(cls, f.name, f.default)
            cls.__initialised__ = True

        return cls

在这个元类中,当创建一个新的类时,会检查这个类是否继承自BaseModel。如果是,则在内部创建一个新的配置类,并将该类的默认值、验证器等信息保存在类的属性中。这样,每个实例都会有自己的默认值对象,从而确保其属性内存指向不同。

如果是最新的版本,可能是出于性能以及声明式范式的考虑,对这个过程实现做了分离。

新手上路,请多包涵
class User(BaseModel):
    id: int
    name = 'John Doe'
    signup_ts: Optional[datetime] = None
    friends: list[int] = []


external_data = {
    'id': '123',
    'signup_ts': '2019-06-01 12:22',
    'friends': [1, 2, '3'],
}

print(repr(user.signup_ts))
#> datetime.datetime(2019, 6, 1, 12, 22)

这个是官方的示例,可以看到在定义了signup_ts为datetime对象之后,传入了'2019-06-01 12:22'这么一个字符串,最后会将这个字符串转为datetime对象

我虽然没有深入pydantic的源码,但大致猜测一下,内部有针对常用的对象做转换。
[]这个也就不奇怪了,可能内部在构造对象的时候有一个copy.deepcopy的逻辑

推荐问题
宣传栏