Не пишите ни одной строчки кода, пока не увидите эти прорывные функции Pydantic V2
Осторожно, дата-инженеры: революционные обновления Pydantic V2 уже здесь!
Введение
Pydantic — это библиотека Python для проверки данных. С примерно 20 миллионами загрузок в неделю он входит в 100 лучших библиотек Python.
Недавно вышел Pydantic V2 (с ржавчиной) — он в 5–50 раз быстрее, чем V1! В выпуске также представлено много новых прорывных функций, о которых я буду говорить.
💡 DISCLAIMER: The LLM robot helped me write the clickbaity title for this article.
В настоящее время используете Pydantic V1?
Следуйте официальному руководству по миграции. Это видео тоже может быть полезным:
1. Проверка функций
Pydantic V2 позволяет проверять любые аргументы функции! (Да, включая методы класса.)
Пример функции со строгими требованиями к типу ввода показан ниже:
from pydantic import validate_call from pydantic.types import conint @validate_call def echo_hello(n_times: conint(gt=0, lt=11), name: str, loud: bool): """ Greets someone with an echo. Args: n_times: How many echos. Min value is 1, max is 10. name: Name to greet loud: Do you want the greeting to be loud? """ greeting = f"Hello {name}!" if loud: greeting = greeting.upper() + "!!" for i in range(n_times): print(greeting) # Call this function echo_hello(n_times=1, name="Yaakov", loud=True) # Valid echo_hello(n_times=10, name="Yaakov", loud=True) # Valid # The following will raise an error: echo_hello(n_times=20, name="Yaakov", loud=True) # Invalid! echo_hello(n_times=1, name=1234, loud=True) # Invalid!
💡 ПРИМЕЧАНИЕ. Эта функция позволяет удалить проверку аргументов из вызовов функций — благо для написания более аккуратного и простого кода.
2. Дискриминированные союзы
Дискриминированные объединения — это операторы объединения, которые определяют, какую модель или значение использовать, на основе поля дискриминатора. Это особенно полезно для разработчиков FastAPI, которым может потребоваться вернуть разные модели из конечной точки API с учетом разных параметров запроса.
Пример, демонстрирующий, как выполняются размеченные объединения, показан ниже:
from typing import Union, Literal, List from pydantic import BaseModel, Field class ModelA(BaseModel): d_type: Literal["single"] value: int = Field(default=0) class ModelB(ModelA): """Inherits from ModelA, making the union challenging""" d_type: Literal["many"] values: List[int] = Field(default_factory=list) class ModelC(BaseModel): v: Union[ModelA, ModelB] = Field(discriminator="d_type") # Populate with extra fields, see what happens m_1 = ModelC(v={"value": 123, "values": [123], "d_type": "single"}) m_2 = ModelC(v={"value": 123, "values": [123], "d_type": "many"}) print(m_1, m_2, sep="\n") # v=ModelA(d_type='single', value=123) # v=ModelB(d_type='many', value=123, values=[123])
💡 ПРИМЕЧАНИЕ. Дискриминированные объединения значительно упрощают случаи наследования моделей.
3. Валидированные типы с аннотированными валидаторами
Валидацию больше не нужно привязывать к модели! С помощью аннотированных валидаторов вы можете напрямую создать валидатор для поля (т. е. типа). (Неявные декораторы, прочь!)
Это полезно, если ваша проверка специфична для поля — или если вы хотите разделить эту проверку между модулями.
Вот простой пример пользовательского типа PositiveNumber
, который принимает на вход только положительные числа:
from typing_extensions import Annotated from pydantic.functional_validators import AfterValidator def validate(v: int): assert v > 0 PositiveNumber = Annotated[int, AfterValidator(validate)]
Более подробный пример пользовательского типа Price
показан ниже:
from typing import Any from typing_extensions import Annotated from pydantic import BaseModel from pydantic.functional_validators import AfterValidator, BeforeValidator def remove_currency(v: Any) -> int: """Remove currency symbol from any input""" if isinstance(v, str): v = v.replace('$', '') return v def truncate_max_number(v: int) -> int: """Any number greater than 100 will be set at 100""" return min(v, 100) # Create a custom type (importable!) Price = Annotated[ int, BeforeValidator(remove_currency), AfterValidator(truncate_max_number) ] class Model(BaseModel): price: Price # Instantiate the model to demonstrate m = Model(price="$12") # price=12 m = Model(price=12) # price=12 m = Model(price=101) # price=100 print(m)
Совместное использование полей между моделями и модулями:
Аннотированные валидаторы разблокируют совместное использование полей между моделями и модулями, поскольку пользовательские типы содержат валидаторы на самом объекте!
Этот декларативный подход является важным усовершенствованием и большим преимуществом для разработчиков Python, работающих с пользовательскими типами.
4. Проверка без BaseModel с использованием TypeAdapter
До сих пор Pydantic был либо
BaseModel
, либо банкротом. 💢 Проверка была ограничена экземплярамиBaseModel
.
V2 представляет TypeAdapter
, специальный класс, который позволяет трансформировать без создания BaseModel
. (Невозможно в версии 1)
TypeAdapter для юнит-тестов — тестовые поля вместо моделей
TypeAdapter
особенно полезен для написания модульных тестов для определенных полей. Эта декомпозиция позволяет проводить более аккуратные + меньшие юнит-тесты → более счастливые времена для вас!
Пример использования TypeAdapter
для преобразования/проверки объектов в NumberList
, настраиваемый тип, показан ниже:
from typing import List, Any from typing_extensions import Annotated import pytest from pydantic import TypeAdapter from pydantic.functional_validators import BeforeValidator def coerce_to_list(v: Any) -> List[Any]: if isinstance(v, list): return v else: return [v] NumberList = Annotated[ List[int], BeforeValidator(coerce_to_list) ] @pytest.mark.parameterize( ('v', 'expected'), [ pytest.param(1, [1], id="single to list"), pytest.param([1, 2, 3], [1, 2, 3], id="list, no change"), pytest.param([1, '2'], [1, 2], id="list with string nums"), ] ) def test_number_list(v: Any, expected: List[int]): ta = TypeAdapter(NumberList) res = ta.validate_python(v) assert res == expected
💡 ПРИМЕЧАНИЕ. Всякий раз, когда я тестирую поведение определенного поля модели, мне нравится писать параметризованные модульные тесты, которые вызывают TypeAdapter. Таким образом, я получаю подробное объяснение того, какие значения оказались ошибочными.
5. Пользовательская сериализация
Версия 2 предлагает новый мощный способ преобразования форматов данных в вашу модель Python и обратно.
Допустим, у вас есть объект datetime
, но вы хотите представить его по-разному при экспорте модели. С помощью настраиваемого сериализатора вы можете указать именно это.
Пример пользовательского field_serializer
показан ниже:
from datetime import datetime from pydantic import BaseModel, field_serializer class BroadwayTicket(BaseModel): show_name: str show_time: datetime @field_serializer("show_time") def transform_show_time(v) -> str: """Returns human readable show time format""" return v.strftime("%b %d, %Y, %I:%M %p") # Create an object my_tickets = BroadwayTicket( show_name="Parade", show_time=datetime(2023, 8, 5, 19) # August 8, 7:00PM ) print(my_tickets.model_dump()) # {'show_name': 'Parade', 'show_time': 'Aug 05, 2023, 07:00 PM'}
⚠️ ПРИМЕЧАНИЕ. Сериализация не выполняет «двустороннее преобразование», то есть экспортированный «сериализованный» формат не сериализуется автоматически обратно в тип данных.
Существуют продвинутые методы для завершения этого «двухстороннего преобразования», но на данный момент это не простой путь. Я могу написать дополнительную статью об этом.
Заключение
Pydantic V2 — хорошее время. Это дает вам больше контроля над вашими вещами. Это невероятно быстро (по сравнению с библиотеками Python). Он также предлагает массу новых мощных функций.
Удачного кодирования!