Pydantic  -  гарантия надежного и безошибочного кода Python

Python  —  динамически типизированный язык. Это значит, что он проверяет типы во время выполнения. Если в коде затаилась ошибка, она будет выброшена именно в этот момент. В статически типизированных языках, таких как Java, C# и C, подобная проверка проводится во время компиляции. В этом случае ошибка выбрасывается еще до запуска программы. 

В статически типизированных языках типы конструкций не подлежат изменениям. Компилятор должен знать их заранее. Например, в C переменную, изначально объявленную как int, нельзя впоследствии преобразовать в string

А вот в Python это возможно: 

myVar = 1 
myVar = "hello" #этот прием работает

Из-за такой повышенной гибкости динамически типизированные языки уступают статически типизированным в скорости во время выполнения. Требуется провести множество проверок на определение типов переменных и других конструкций, чтобы обеспечить работу программы. А все это создает дополнительную нагрузку. 

В настоящее время Python является приоритетным языком для машинного обучения и все чаще используется для разработки API и веб-приложений, обслуживающих модели МО. Намного проще создавать модели и обертывать их в ориентированное на пользователя приложение с помощью одного языка, чем нескольких. 

Однако в случае с такими полностековыми приложениям проверка типов во время выполнения, а не компиляции сопряжена с большим числом ошибок. В этой ситуации на помощь приходят подсказки типов Python, которые позволяют объявлять типы конструкций прямо в коде.

Познакомимся с ними подробнее. 

Подсказки типов Python и автозавершение кода 

Чтобы определить подсказку типа для аргумента функции, мы пишем : (двоеточие), за которым следует тип после имени переменной. Для неодиночных типов, таких как Lists (списки), Dicts (словари) и Sets (множества), необходимо импортировать пакет typing.

Рассмотрим код:

from typing import List

def toUpper(x: str):
return x.capitalize()

def get_stuff(item:str, fridge:List[str]):
fridge.append(item)
return list(map(toUpper, fridge))

print(get_stuff("orange", ["apple", "grape", "pear"]))

Мы определяем функцию get_stuff(), которая добавляет предоставляемый item к списку элементов fridge. Затем все элементы в списке fridge прописываются с заглавных букв. 

Как и ожидалось, код возвращает список фруктов: 

['Apple', 'Grape', 'Pear', 'Orange']

Так как мы определяем fridge как список строк, VS Code с установленными расширениями PyLance и Python обеспечивает моментальное автозавершение кода. Вводим fridge. и видим предлагаемые варианты: 

Аналогично тому, как мы определили fridge, можно написать x., чтобы просмотреть все возможные операции с элементами в списке fridge, которые являются строками:  

Как видно, подсказки типов экономят массу времени, поскольку нет необходимости сновать туда-сюда, обращаясь к онлайн-документации за методами и атрибутами. 

Модели Pydantic 

Pydantic проверяет данные и управляет настройками с помощью аннотаций типов Python. Применяет подсказки типов во время выполнения и в случае некорректных данных выводит понятные сообщения об ошибках. 

Источник: Pydantic

Несмотря на поддержку подсказок типов, Python не следит за их применением. Поэтому вполне может произойти передача объекта некорректного типа и, как следствие, появление ошибки при попытке совершить неподдерживаемую операцию. Например, выполнить операцию str над типом int. Pydantic, библиотека Python, в обязательном порядке реализует подсказки типов и тем самым позволяет избегать подобных ошибок. 

Подкрепим теорию практическим примером.

Допустим, функция получает плохой ввод, и fridge наряду с int содержит еще и strings. Не трогая остальную часть кода, вызываем get_stuff() с измененным fridge:

print(get_stuff("orange", ["apple", 1, "pear"]))

В ответ на это получаем ошибку при выполнении:

Несмотря на то, что x был объявлен как тип str, функция get_stuff() спокойно принимает список List с одним элементом int, а toUpper() пытается вызвать capitalize() для объекта int.

На данный момент кажется, что преимущества подсказок типов ограничиваются только автозавершением. 

Проведем рефакторинг кода для использования Pydantic. Определяем модель данных, которая наследуется от BaseModel. В этом и состоит основной способ создания моделей в Pydantic. Поскольку у нас есть теперь схема представления данных, определяем эту модель как класс. 

Устанавливаем Pydantic: 

pip3 install pydantic

Определяем класс Frdige, наследуемый от BaseModel:

from pydantic import BaseModelclass Fridge(BaseModel):
items: List[str]

Задаем для класса Fridge атрибут с именем items, который будет представлять собой список строк. Создаем экземпляр Fridge и передаем его в качестве аргумента при вызове функции get_stuff().

После рефакторинга код выглядит так: 

from typing import List
from pydantic import BaseModel

class Fridge(BaseModel):
items: List[str]

def toUpper(x: str):
return x.capitalize()

def get_stuff(item:str, fridge:Fridge):
fridge.items.append(item)
return list(map(toUpper, fridge.items))

print(get_stuff(item="orange", fridge=Fridge(items={"apple", 1, "pear"})))

Выполнив еще раз код, мы увидим, что в нем нет ошибок! int приводится к объекту string и добавляется к списку, а в результате получается следующий возвращаемый объект: 

['1', 'Apple', 'Pear', 'Orange']

Обратите внимание, что мы передаем set Python вместо list при создании экземпляра объекта Fridge. И опять Pydantic берет на себя заботы по приведению типа set к list!

Интересно, а что делать, если нам-то как раз нужен список смешанных типов, например содержащий и строки, и целые числа. Тогда мы можем воспользоваться аннотацией типа Union, которая действует как логический оператор OR. В этом случае Fridge определяется следующим образом: 

class Fridge(BaseModel):
items: List[Union[int, str]]

Данный список успешно передается классу Fridge:

[1, "apple", "orange", "pear"]

Заметьте, что Pydantic отдает приоритет первому типу, указанному в Union. Если бы мы написали: 

class Fridge(BaseModel):
items: List[Union[str, int]]

Тогда int, присутствующий в переданном списке, был бы преобразован в string, несмотря на то, что он значится в аннотации типа. Это обернется вот таким нежелательным для нас результатом:

["1", "apple", "orange", "pear"]

Что ж, мы затронули немало вопросов! Но все же остался еще один. 

Pydantic отлично справляется с моделированием более сложных типов данных. В этой связи рассмотрим рекурсивные модели. 

Рекурсивные модели 

Для усложненных моделей данных Pydantic позволяет определять рекурсивные модели. Они содержат другие модели в качестве определения типа в одном из своих атрибутов. Это значит, что вместо List[str] мы можем воспользоваться List[Cars], где Cars  —  модель Pydantic, определенная в коде.

Обратимся к примеру! 

Допустим, мы намерены также зафиксировать количество каждого фрукта fruit в холодильнике fridge. Для этого создаем модель данных Fruit:

class Fruit(BaseModel):
name:str
num:int

В модели данных Fridge определяем список как Fruits, а не как ints.

class Fridge(BaseModel):
items: List[Fruit]

Ниже представлен полный код:

from typing import List
from pydantic import BaseModel
from fastapi.encoders import jsonable_encoder

class Fruit(BaseModel):
name:str
num:int

class Fridge(BaseModel):
items: List[Fruit]

def get_most_fruits(fridge:Fridge):
itemList = jsonable_encoder(fridge.items)
newlist = sorted(itemList, key=lambda d: d['num'])
print()
return newlist[-1]

print(get_most_fruits(
fridge=Fridge(
items=[
Fruit(name="apple", num=2),
Fruit(name="pear", num=0),
Fruit(name="orange", num=4)
])))

Вызываем get_most_fruits() с объектом Fridge, который содержит список объектов Fruit. Все просто! 

Возвращаем fruit с наибольшим числом. Перед выполнением операций со списком fruit воспользуемся методом jsonable_encoder() для преобразования данного списка в тип, совместимый с JSON. Если этого не сделать, то элемент в списке будет типом Fruit, с которым мы не сможем работать. 

После кодирования получаем список объектов dict с парами “ключ-значение”, которые соответствуют полям name и num, определенными в классе Fruit

Теперь отсортируем этот список и вернем fruit с наибольшим количеством. 

Заключение

В данной статье мы провели обзор динамически и статически типизированных языков. Рассмотрели подсказки типов в Python и использование Pydantic для их реализации. 

Обобщим преимущества подсказок типов: 

  • ускоряют разработку ПО посредством автозавершения IDE; 
  • повышают качество кода, делают его более понятным и удобочитаемым;
  • улучшают стиль кода и дизайн ПО в целом; 
  • создают более надежное и безошибочное ПО за счет сокращения ошибок во время выполнения. Особенно это касается больших и сложных проектов. 

Читайте также:

Читайте нас в TelegramVK и Яндекс.Дзен


Перевод статьи Haseeb Kamal: Writing Robust and Error-Free Python Code Using Pydantic

Предыдущая статьяПолное руководство по тестированию контрактов с помощью PACT и Go
Следующая статьяТестируя нетестируемое — битва с легаси-кодом