Перейти до змісту

Вступ до типів 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_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)

Інформація

Ці внутрішні типи в квадратних дужках називаються «параметрами типу».

У цьому випадку str — це параметр типу, переданий у list.

Це означає: «змінна items — це list, і кожен з елементів у цьому списку — str».

Зробивши це, ваш редактор може надати підтримку навіть під час обробки елементів зі списку:

Без типів цього майже неможливо досягти.

Зверніть увагу, що змінна item є одним із елементів у списку items.

І все ж редактор знає, що це str, і надає підтримку для цього.

Tuple and Set

Ви повинні зробити те ж саме, щоб оголосити tuple і set:

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)

Це означає:

  • Змінна 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 types or Generics, наприклад:

Ви можете використовувати ті самі вбудовані типи як generic (з квадратними дужками та типами всередині):

  • list
  • tuple
  • set
  • dict

І так само, як і в попередніх версіях Python, з модуля typing:

  • Union
  • Optional
  • ...та інші.

У Python 3.10 як альтернативу використанню generic Union та Optional ви можете використовувати вертикальну смугу (|) для оголошення об’єднань типів — це значно краще й простіше.

Ви можете використовувати ті самі вбудовані типи як generic (з квадратними дужками та типами всередині):

  • list
  • tuple
  • set
  • dict

І generic з модуля typing:

  • 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
🤓 Other versions and variants
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

Інформація

Щоб дізнатись більше про Pydantic, перегляньте його документацію.

FastAPI повністю базується на Pydantic.

Ви побачите набагато більше цього всього на практиці в Tutorial - User Guide.

Порада

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 нічого не робить із цим Annotated. А для редакторів та інших інструментів тип усе ще є str.

Але ви можете використати це місце в Annotated, щоб надати FastAPI додаткові метадані про те, як ви хочете, щоб ваш застосунок поводився.

Важливо пам’ятати, що перший параметр типу, який ви передаєте в Annotated, — це фактичний тип. Решта — це лише метадані для інших інструментів.

Наразі вам просто потрібно знати, що Annotated існує і що це стандартний Python. 😎

Пізніше ви побачите, наскільки потужним це може бути.

Порада

Той факт, що це стандартний Python, означає, що ви й надалі отримуватимете найкращий можливий досвід розробки у вашому редакторі, з інструментами, які ви використовуєте для аналізу та рефакторингу коду тощо. ✨

А також те, що ваш код буде дуже сумісним із багатьма іншими інструментами та бібліотеками Python. 🚀

Анотації типів у FastAPI

FastAPI використовує ці підказки типів для виконання кількох речей.

З FastAPI ви оголошуєте параметри з підказками типів, і отримуєте:

  • Підтримку редактора.
  • Перевірку типів.

...і FastAPI використовує ті самі оголошення для:

  • Визначення вимог: з параметрів шляху запиту, параметрів запиту, заголовків, тіл, залежностей тощо.
  • Перетворення даних: із запиту в необхідний тип.
  • Перевірки даних: що надходять від кожного запиту:
    • Генерування автоматичних помилок, що повертаються клієнту, коли дані недійсні.
  • Документування API за допомогою OpenAPI:
    • який потім використовується для автоматичної інтерактивної документації користувальницьких інтерфейсів.

Все це може здатися абстрактним. Не хвилюйтеся. Ви побачите все це в дії в Tutorial - User Guide.

Важливо те, що за допомогою стандартних типів Python в одному місці (замість того, щоб додавати більше класів, декораторів тощо), FastAPI зробить багато роботи за вас.

Інформація

Якщо ви вже пройшли весь туторіал і повернулися, щоб дізнатися більше про типи, ось хороший ресурс: «шпаргалка» від mypy.