Pydantic: как передать значение по умолчанию переменной, если не было передано значение None?

Могу ли я сделать значение по умолчанию в Pydantic, если в поле ничего не передается?

У меня есть следующий код, но мне кажется, что здесь валидатор работает только при инициализации модели, а не иначе.

Мой код:

class User(BaseModel):
     name: Optional[str] = ''
     password: Optional[str] = ''
     email: EmailStr
    

    @validator('name')
    def set_name(cls, name):
        return name or 'foo'

Обнаружена проблема:

user = User(name=None, password='some_password', email='[email protected]')
print("Name is ", user.name)
# > 'Name is foo'

user.name = None
print("Name is ", user.name)
# > 'Name is None'

Желаемый результат:

user = User(name='some_name', password='some_password', email='[email protected]')
user.name = None
print("Name is ", user.name)
# > 'Name is foo'

Любые идеи о том, как я могу получить желаемый результат? Я думаю, что наличие геттеров и сеттеров поможет в решении этой проблемы. Однако мне не удалось заставить их работать в модели Pydantic:

Попытка реализовать геттеры и сеттеры:

class User(BaseModel):
    name: Optional[str] = ''
    password: Optional[str] = ''
    email: EmailStr

    def get_password(self):
        return self.password

    def set_password(self, password):
        self.password = hash_password(password)

    password = property(get_password, set_password)

user = User(name='some_name', password='some_password', email='[email protected]')
# > RecursionError: maximum recursion depth exceeded

Я также попробовал декоратор свойств:

class User(BaseModel):
     name: Optional[str] = ''
     password: Optional[str] = ''
     email: EmailStr

    @property
    def password(self):
        return self._password

    @password.setter
    def password(self, password):
        pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
        self._password = pwd_context.hash(password)

user = User(name='some_name', email='[email protected]')
user.password = 'some_password'
# > ValueError: "User" object has no field "password"

Я также пытался перезаписать init:

class User(BaseModel):
name: Optional[str] = ""
password: Optional[str] = ""
email: EmailStr

def __init__(self, name, password, email):
    pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
    password = pwd_context.hash(password)
    super().__init__(name=name, password=password, email=email)


user = User(name="some_name", password="some_password", email='[email protected]')
print(user.password)
# > AYylwSnbQgCHrl4uue6kO7yiuT20lazSzK7x # Works as expected

user.password = "some_other_password"
print(user.password)
# > "some_other_password" # Does not work

user.password = None
print(user.password)
# > None # Does not work either

person Manas Sambare    schedule 27.08.2020    source источник


Ответы (4)


Вам необходимо включить опцию validate_assignment в конфигурации модели:

from typing import Optional

from pydantic import BaseModel, validator


class User(BaseModel):
    name: Optional[str] = ''
    password: Optional[str] = ''

    class Config:
        validate_assignment = True

    @validator('name')
    def set_name(cls, name):
        return name or 'foo'


user = User(name=None, password='some_password', )
print("Name is ", user.name)


user.name = None
print("Name is ", user.name)
Name is  foo
Name is  foo
person alex_noname    schedule 27.08.2020

Этот вопрос задан идеально, поэтому я хотел бы привести более широкий пример, потому что существует много способов динамического присвоения значения.

Ответ Алекса правильный, но он работает только тогда, когда поле напрямую наследует класс данных, в частности, что-то подобное не сработает.

class User(BaseModel):
    name: Optional[str] = ""
    password: Optional[str] = ""

    class Config:
        validate_assignment = True

    @validator("name")
    def set_name(cls, name):
        return name or "bar"


user_dict = {"password": "so_secret"}
user_one = User(**user_dict)
Out: name='' password='so_secret'

Проверять всегда

По соображениям производительности по умолчанию валидаторы не вызываются для полей, если значение не указано. Но в подобных ситуациях, когда вам нужно установить динамическое значение по умолчанию, мы можем установить для него значение True.

class User(BaseModel):
    name: Optional[str] = ""

    @validator("name", pre=True, always=True)
    def set_name(cls, name):
        return name or "bar"

In: user_one = User(name=None)
In: user_two = User()
Out: name='bar'
Out: name='bar'

Но есть одна важная загвоздка с always, поскольку мы используем always = True, pydantic попытается проверить значение по умолчанию None, что вызовет ошибку.

Если для Pre установлено значение True, это поле будет вызываться до того, как произойдет ошибка проверки, по умолчанию для предварительного валидатора установлено значение False, и в этом случае они вызываются после проверки поля.

Использование Config

Но у этого есть недостатки.

class User(BaseModel):
    name: Optional[str] = ""

    class Config:
        validate_assignment = True

    @validator("name")
    def set_name(cls, name):
        return name or "foo"

In:  user = User(name=None)
Out: name='foo'

Когда вы устанавливаете для него значение None, он возвращает динамическое значение правильно, но в некоторых ситуациях, например, полностью None, он не работает.

In:  user = User()
Out: name=''

Снова вам нужно настроить, чтобы это сработало.

pre=True
always=True

Использование default_factory

Это в основном полезно в тех случаях, когда вы хотите установить значение по умолчанию, например UUID или datetime и т. Д. В этих случаях вы можете использовать default_factory, но есть большая проблема: вы не можете назначить Callable аргумент фабрики по умолчанию.

class User(BaseModel):
    created_at: datetime = Field(default_factory=datetime.now)

In: user = User()
Out: created_at=datetime.datetime(2020, 8, 29, 2, 40, 12, 780986)
person Yagiz Degirmenci    schedule 28.08.2020

Множество способов присвоить значение по умолчанию

Метод №1: обязательное поле id со значением по умолчанию

class User(BaseModel):
    id: str = uuid.uuid4()

Метод № 2: необязательное поле id со значением по умолчанию

class User(BaseModel):
    id: Optional[str] = uuid.uuid4()

Метод № 3: обязательное поле id со значением по умолчанию

class User(BaseModel):
    id: str = Field(default=uuid.uuid4())

Метод №4: обязательное поле id со значением по умолчанию из вызываемого. Это полезно для генерации значений по запросу, таких как unique UUIDs или Timestamps. См. Ответ @ yagiz-degirmenci.

class User(BaseModel):
    id: str = Field(default_factory=uuid.uuid4)  # uuid.uuid4 is not executed immediately
person Dev Sareno    schedule 04.02.2021

Рассмотрите возможность добавления метода __init__ в свой класс. Что-то вроде

def __init__(self, name, pass, email):
    if pass is None:
        self.password = "foo"
    else:
        self.password = pass
    self.name = name
    self.email = email
person Eric Canton    schedule 27.08.2020
comment
Это невозможно сделать в пидантической модели. Я пробовал. Для этого вам придется перегрузить метод init суперкласса. Однако это тоже не помогает. - person Manas Sambare; 27.08.2020
comment
Ах, моя ошибка. Я не знаком с Pydantic; Полагаю, это меняет систему классов Python? Удачи! - person Eric Canton; 27.08.2020