Введение в типы Python¶
Python поддерживает необязательные «подсказки типов» (их также называют «аннотациями типов»).
Эти «подсказки типов» или аннотации — это специальный синтаксис, позволяющий объявлять тип переменной.
Объявляя типы для ваших переменных, редакторы кода и инструменты смогут лучше вас поддерживать.
Это всего лишь краткое руководство / напоминание о подсказках типов в Python. Оно охватывает только минимум, необходимый для их использования с FastAPI... что на самом деле очень мало.
FastAPI целиком основан на этих подсказках типов — они дают ему множество преимуществ и выгод.
Но даже если вы никогда не используете FastAPI, вам будет полезно немного узнать о них.
Примечание
Если вы являетесь экспертом в Python и уже знаете всё о подсказках типов, переходите к следующей главе.
Мотивация¶
Давайте начнем с простого примера:
def get_full_name(first_name, last_name):
full_name = first_name.title() + " " + last_name.title()
return full_name
print(get_full_name("john", "doe"))
Вызов этой программы выводит:
John Doe
Функция делает следующее:
- Принимает
first_name
иlast_name
. - Преобразует первую букву каждого значения в верхний регистр с помощью
title()
. - Соединяет их пробелом посередине.
def get_full_name(first_name, last_name):
full_name = first_name.title() + " " + last_name.title()
return full_name
print(get_full_name("john", "doe"))
Отредактируем пример¶
Это очень простая программа.
А теперь представьте, что вы пишете её с нуля.
В какой-то момент вы бы начали определение функции, у вас были бы готовы параметры...
Но затем нужно вызвать «тот метод, который делает первую букву заглавной».
Это был upper
? Или uppercase
? first_uppercase
? capitalize
?
Тогда вы пробуете старого друга программиста — автозавершение редактора кода.
Вы вводите первый параметр функции, first_name
, затем точку (.
) и нажимаете Ctrl+Space
, чтобы вызвать автозавершение.
Но, к сожалению, ничего полезного не находится:
Добавим типы¶
Давайте изменим одну строку из предыдущей версии.
Мы поменяем ровно этот фрагмент — параметры функции — с:
first_name, last_name
на:
first_name: str, last_name: str
Вот и всё.
Это и есть «подсказки типов»:
def get_full_name(first_name: str, last_name: str):
full_name = first_name.title() + " " + last_name.title()
return full_name
print(get_full_name("john", "doe"))
Это не то же самое, что объявление значений по умолчанию, как, например:
first_name="john", last_name="doe"
Это другая вещь.
Здесь мы используем двоеточия (:
), а не знак равенства (=
).
И добавление подсказок типов обычно не меняет поведение программы по сравнению с вариантом без них.
Но теперь представьте, что вы снова посередине написания этой функции, только уже с подсказками типов.
В тот же момент вы пробуете вызвать автозавершение с помощью Ctrl+Space
— и видите:
С этим вы можете прокручивать варианты, пока не найдёте тот самый:
Больше мотивации¶
Посмотрите на эту функцию — у неё уже есть подсказки типов:
def get_name_with_age(name: str, age: int):
name_with_age = name + " is this old: " + age
return name_with_age
Так как редактор кода знает типы переменных, вы получаете не только автозавершение, но и проверки ошибок:
Теперь вы знаете, что нужно исправить — преобразовать age
в строку с помощью str(age)
:
def get_name_with_age(name: str, age: int):
name_with_age = name + " is this old: " + str(age)
return name_with_age
Объявление типов¶
Вы только что увидели основное место, где объявляют подсказки типов — параметры функции.
Это также основное место, где вы будете использовать их с FastAPI.
Простые типы¶
Вы можете объявлять все стандартные типы Python, не только str
.
Можно использовать, например:
int
float
bool
bytes
def get_items(item_a: str, item_b: int, item_c: float, item_d: bool, item_e: bytes):
return item_a, item_b, item_c, item_d, item_d, item_e
Generic-типы с параметрами типов¶
Есть структуры данных, которые могут содержать другие значения, например, dict
, list
, set
и tuple
. И внутренние значения тоже могут иметь свой тип.
Такие типы, которые содержат внутренние типы, называют «generic»-типами. И их можно объявлять, в том числе с указанием внутренних типов.
Чтобы объявлять эти типы и их внутренние типы, вы можете использовать стандартный модуль Python typing
. Он существует специально для поддержки подсказок типов.
Новые версии Python¶
Синтаксис с использованием typing
совместим со всеми версиями, от Python 3.6 до самых новых, включая Python 3.9, Python 3.10 и т.д.
По мере развития Python новые версии получают улучшенную поддержку этих аннотаций типов, и во многих случаях вам даже не нужно импортировать и использовать модуль typing
, чтобы объявлять аннотации типов.
Если вы можете выбрать более свежую версию Python для проекта, вы получите дополнительную простоту.
Во всей документации есть примеры, совместимые с каждой версией Python (когда есть различия).
Например, «Python 3.6+» означает совместимость с Python 3.6 и выше (включая 3.7, 3.8, 3.9, 3.10 и т.д.). А «Python 3.9+» — совместимость с Python 3.9 и выше (включая 3.10 и т.п.).
Если вы можете использовать последние версии Python, используйте примеры для самой новой версии — у них будет самый лучший и простой синтаксис, например, «Python 3.10+».
List¶
Например, давайте определим переменную как list
из str
.
Объявите переменную с тем же синтаксисом двоеточия (:
).
В качестве типа укажите list
.
Так как список — это тип, содержащий внутренние типы, укажите их в квадратных скобках:
def process_items(items: list[str]):
for item in items:
print(item)
Из typing
импортируйте List
(с заглавной L
):
from typing import List
def process_items(items: List[str]):
for item in items:
print(item)
Объявите переменную с тем же синтаксисом двоеточия (:
).
В качестве типа используйте List
, который вы импортировали из typing
.
Так как список — это тип, содержащий внутренние типы, укажите их в квадратных скобках:
from typing import List
def process_items(items: List[str]):
for item in items:
print(item)
Информация
Эти внутренние типы в квадратных скобках называются «параметрами типов».
В данном случае str
— это параметр типа, передаваемый в List
(или list
в Python 3.9 и выше).
Это означает: «переменная items
— это list
, и каждый элемент этого списка — str
».
Совет
Если вы используете Python 3.9 или выше, вам не нужно импортировать List
из typing
, можно использовать обычный встроенный тип list
.
Таким образом, ваш редактор кода сможет помогать даже при обработке элементов списка:
Без типов добиться этого почти невозможно.
Обратите внимание, что переменная item
— один из элементов списка items
.
И всё же редактор кода знает, что это str
, и даёт соответствующую поддержку.
Tuple и Set¶
Аналогично вы бы объявили tuple
и set
:
def process_items(items_t: tuple[int, int, str], items_s: set[bytes]):
return items_t, items_s
from typing import Set, Tuple
def process_items(items_t: Tuple[int, int, str], items_s: Set[bytes]):
return items_t, items_s
Это означает:
- Переменная
items_t
— этоtuple
из 3 элементов:int
, ещё одинint
иstr
. - Переменная
items_s
— этоset
, и каждый элемент имеет типbytes
.
Dict¶
Чтобы определить dict
, вы передаёте 2 параметра типов, разделённые запятой.
Первый параметр типа — для ключей dict
.
Второй параметр типа — для значений dict
:
def process_items(prices: dict[str, float]):
for item_name, item_price in prices.items():
print(item_name)
print(item_price)
from typing import Dict
def process_items(prices: Dict[str, float]):
for item_name, item_price in prices.items():
print(item_name)
print(item_price)
Это означает:
- Переменная
prices
— этоdict
:- Ключи этого
dict
имеют типstr
(скажем, название каждой позиции). - Значения этого
dict
имеют типfloat
(скажем, цена каждой позиции).
- Ключи этого
Union¶
Вы можете объявить, что переменная может быть одним из нескольких типов, например, int
или str
.
В Python 3.6 и выше (включая Python 3.10) вы можете использовать тип Union
из typing
и перечислить в квадратных скобках все допустимые типы.
В Python 3.10 также появился новый синтаксис, где допустимые типы можно указать через вертикальную черту (|
).
def process_item(item: int | str):
print(item)
from typing import Union
def process_item(item: Union[int, str]):
print(item)
В обоих случаях это означает, что item
может быть int
или str
.
Возможно None
¶
Вы можете объявить, что значение может иметь определённый тип, например str
, но также может быть и None
.
В Python 3.6 и выше (включая Python 3.10) это можно объявить, импортировав и используя Optional
из модуля typing
.
from typing import Optional
def say_hi(name: Optional[str] = None):
if name is not None:
print(f"Hey {name}!")
else:
print("Hello World")
Использование Optional[str]
вместо просто str
позволит редактору кода помочь вам обнаружить ошибки, когда вы предполагаете, что значение всегда str
, хотя на самом деле оно может быть и None
.
Optional[Something]
— это на самом деле сокращение для Union[Something, None]
, они эквивалентны.
Это также означает, что в Python 3.10 вы можете использовать Something | None
:
def say_hi(name: str | None = None):
if name is not None:
print(f"Hey {name}!")
else:
print("Hello World")
from typing import Optional
def say_hi(name: Optional[str] = None):
if name is not None:
print(f"Hey {name}!")
else:
print("Hello World")
from typing import Union
def say_hi(name: Union[str, None] = None):
if name is not None:
print(f"Hey {name}!")
else:
print("Hello World")
Использовать Union
или Optional
¶
Если вы используете версию Python ниже 3.10, вот совет с моей весьма субъективной точки зрения:
- 🚨 Избегайте использования
Optional[SomeType]
- Вместо этого ✨ используйте
Union[SomeType, None]
✨.
Оба варианта эквивалентны и внутри одинаковы, но я бы рекомендовал Union
вместо Optional
, потому что слово «optional» («необязательный») может навести на мысль, что значение необязательное, хотя на самом деле оно означает «может быть None
», даже если параметр не является необязательным и всё ещё обязателен.
Мне кажется, Union[SomeType, None]
более явно выражает смысл.
Речь только о словах и названиях. Но эти слова могут влиять на то, как вы и ваши коллеги думаете о коде.
В качестве примера возьмём эту функцию:
from typing import Optional
def say_hi(name: Optional[str]):
print(f"Hey {name}!")
🤓 Other versions and variants
def say_hi(name: str | None):
print(f"Hey {name}!")
Параметр name
определён как Optional[str]
, но он не необязательный — вы не можете вызвать функцию без этого параметра:
say_hi() # О нет, это вызывает ошибку! 😱
Параметр name
всё ещё обязателен (не optional), потому что у него нет значения по умолчанию. При этом name
принимает None
как значение:
say_hi(name=None) # Это работает, None допустим 🎉
Хорошая новость: как только вы перейдёте на Python 3.10, об этом можно не переживать — вы сможете просто использовать |
для объединения типов:
def say_hi(name: str | None):
print(f"Hey {name}!")
🤓 Other versions and variants
from typing import Optional
def say_hi(name: Optional[str]):
print(f"Hey {name}!")
И тогда вам не придётся задумываться о названиях вроде Optional
и Union
. 😎
Generic-типы¶
Типы, которые принимают параметры типов в квадратных скобках, называются Generic-типами или Generics, например:
Вы можете использовать те же встроенные типы как generics (с квадратными скобками и типами внутри):
list
tuple
set
dict
И, как и в Python 3.8, из модуля typing
:
Union
Optional
(так же, как в Python 3.8)- ...и другие.
В Python 3.10, как альтернативу generics Union
и Optional
, можно использовать вертикальную черту (|
) для объявления объединений типов — это гораздо лучше и проще.
Вы можете использовать те же встроенные типы как generics (с квадратными скобками и типами внутри):
list
tuple
set
dict
И, как и в Python 3.8, из модуля typing
:
Union
Optional
- ...и другие.
List
Tuple
Set
Dict
Union
Optional
- ...и другие.
Классы как типы¶
Вы также можете объявлять класс как тип переменной.
Допустим, у вас есть класс Person
с именем:
class Person:
def __init__(self, name: str):
self.name = name
def get_person_name(one_person: Person):
return one_person.name
Тогда вы можете объявить переменную типа Person
:
class Person:
def __init__(self, name: str):
self.name = name
def get_person_name(one_person: Person):
return one_person.name
И снова вы получите полную поддержку редактора кода:
Обратите внимание, что это означает: «one_person
— это экземпляр класса Person
».
Это не означает: «one_person
— это класс с именем Person
».
Pydantic-модели¶
Pydantic — это библиотека Python для валидации данных.
Вы объявляете «форму» данных как классы с атрибутами.
И у каждого атрибута есть тип.
Затем вы создаёте экземпляр этого класса с некоторыми значениями — он провалидирует значения, преобразует их к соответствующему типу (если это применимо) и вернёт вам объект со всеми данными.
И вы получите полную поддержку редактора кода для этого результирующего объекта.
Пример из официальной документации Pydantic:
from datetime import datetime
from pydantic import BaseModel
class User(BaseModel):
id: int
name: str = "John Doe"
signup_ts: datetime | None = None
friends: list[int] = []
external_data = {
"id": "123",
"signup_ts": "2017-06-01 12:22",
"friends": [1, "2", b"3"],
}
user = User(**external_data)
print(user)
# > User id=123 name='John Doe' signup_ts=datetime.datetime(2017, 6, 1, 12, 22) friends=[1, 2, 3]
print(user.id)
# > 123
from datetime import datetime
from typing import Union
from pydantic import BaseModel
class User(BaseModel):
id: int
name: str = "John Doe"
signup_ts: Union[datetime, None] = None
friends: list[int] = []
external_data = {
"id": "123",
"signup_ts": "2017-06-01 12:22",
"friends": [1, "2", b"3"],
}
user = User(**external_data)
print(user)
# > User id=123 name='John Doe' signup_ts=datetime.datetime(2017, 6, 1, 12, 22) friends=[1, 2, 3]
print(user.id)
# > 123
from datetime import datetime
from typing import List, Union
from pydantic import BaseModel
class User(BaseModel):
id: int
name: str = "John Doe"
signup_ts: Union[datetime, None] = None
friends: List[int] = []
external_data = {
"id": "123",
"signup_ts": "2017-06-01 12:22",
"friends": [1, "2", b"3"],
}
user = User(**external_data)
print(user)
# > User id=123 name='John Doe' signup_ts=datetime.datetime(2017, 6, 1, 12, 22) friends=[1, 2, 3]
print(user.id)
# > 123
Информация
Чтобы узнать больше о Pydantic, ознакомьтесь с его документацией.
FastAPI целиком основан на Pydantic.
Вы увидите намного больше всего этого на практике в Руководстве пользователя.
Совет
У Pydantic есть особое поведение, когда вы используете Optional
или Union[Something, None]
без значения по умолчанию. Подробнее читайте в документации Pydantic: Required Optional fields.
Подсказки типов с аннотациями метаданных¶
В Python также есть возможность добавлять дополнительные метаданные к подсказкам типов с помощью Annotated
.
В Python 3.9 Annotated
входит в стандартную библиотеку, поэтому вы можете импортировать его из typing
.
from typing import Annotated
def say_hello(name: Annotated[str, "this is just metadata"]) -> str:
return f"Hello {name}"
В версиях ниже Python 3.9 импортируйте Annotated
из typing_extensions
.
Он уже будет установлен вместе с FastAPI.
from typing_extensions import Annotated
def say_hello(name: Annotated[str, "this is just metadata"]) -> str:
return f"Hello {name}"
Сам Python ничего не делает с Annotated
. А для редакторов кода и других инструментов тип по-прежнему str
.
Но вы можете использовать это место в Annotated
, чтобы передать FastAPI дополнительные метаданные о том, как вы хотите, чтобы ваше приложение себя вело.
Важно помнить, что первый параметр типа, который вы передаёте в Annotated
, — это фактический тип. Всё остальное — просто метаданные для других инструментов.
Пока вам достаточно знать, что Annotated
существует и это — стандартный Python. 😎
Позже вы увидите, насколько это мощно.
Совет
Тот факт, что это стандартный Python, означает, что вы по-прежнему получите лучший возможный разработческий опыт в вашем редакторе кода, с инструментами для анализа и рефакторинга кода и т.д. ✨
А ещё ваш код будет очень совместим со множеством других инструментов и библиотек Python. 🚀
Аннотации типов в FastAPI¶
FastAPI использует эти подсказки типов для выполнения нескольких задач.
С FastAPI вы объявляете параметры с подсказками типов и получаете:
- Поддержку редактора кода.
- Проверки типов.
...и FastAPI использует эти же объявления для:
- Определения требований: из path-параметров, query-параметров, HTTP-заголовков, тел запросов, зависимостей и т.д.
- Преобразования данных: из HTTP-запроса к требуемому типу.
- Валидации данных: приходящих с каждого HTTP-запроса:
- Генерации автоматических ошибок, возвращаемых клиенту, когда данные некорректны.
- Документирования API с использованием OpenAPI:
- что затем используется пользовательскими интерфейсами автоматической интерактивной документации.
Всё это может звучать абстрактно. Не волнуйтесь. Вы увидите всё это в действии в Руководстве пользователя.
Важно то, что, используя стандартные типы Python в одном месте (вместо добавления дополнительных классов, декораторов и т.д.), FastAPI сделает за вас большую часть работы.
Информация
Если вы уже прошли всё руководство и вернулись, чтобы узнать больше о типах, хорошим ресурсом будет «шпаргалка» от mypy
.