Аннотации типов коллекций

Курс по Python: https://stepik.org/course/100707

На предыдущем занятии мы с вами увидели, как можно аннотировать отдельные переменные и функции простыми базовыми типами:

int, float, str, bool и т. п.

Давайте теперь посмотрим, как делать аннотацию для коллекций языка Python:

list, tuple, dict, set

Аннотация списков

Казалось бы, нет ничего проще. Достаточно также указать нужный нам тип, например, у переменной:

lst: list = [1, 2, 3]

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

lst_str: list = ['1', '2', '3']

и вообще любые типы данных. Интегрированная среда нам не выдаст никаких предупреждений. А вот если вместо списка записать, например, кортеж:

lst: list = (1, 2, 3)

то тогда уже увидим подсветку с предупреждением. То есть, тип list в аннотации отвечает только за определение списка. Типы элементов, при этом, могут быть любыми.

Но что если нам нужно дополнительно в аннотации указать и тип элементов списка? Начиная с версии Python 3.9, это можно сделать с помощью все того же базового типа list следующим образом:

lst: list[int] = [1, 2, 3]

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

Если указать другой тип, например, str:

lst: list[str] = [1, 2, 3]

Увидим подсветку кода. Но, как только в списке оказывается хотя бы один строковый элемент:

lst: list[str] = ['1', 2, 3]

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

Вернемся к нашей нотации списка list[str]. Записывать ее в таком виде можно только, начиная с версии Python 3.9 и выше. Если версия вашего интерпретатора ниже, то для достижения того же результата придется импортировать из модуля typing следующие типы:

List, Tuple, Dict, Set

from typing import List, Tuple, Dict, Set

а, затем, вместо list прописать List:

lst: List[str] = [1, 2, 3]

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

Аннотация кортежей

С кортежами все в целом так же, как и со списками. Единственное ключевое отличие – это независимое указание типов для всех элементов кортежа. Например:

address: tuple[int, str] = (1, 'proproprogs.ru')

Возможно, здесь у вас возникает вопрос: почему у списка мы указываем один тип для всех элементов, а у кортежа для каждого? Как вы помните, кортеж – это неизменяемый тип данных. В частности, мы не можем добавлять или удалять элементы. Поэтому их число полагается известным и фиксированным. Часто кортежи в Python используются для группировки каких-либо данных в единый объект. Например, информация по книге может быть представлена в виде кортежа:

book: tuple[str, str, int]
book = ('Балакирев С.М.', 'Аннотация типов', 2022)

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

Конечно, если вам понадобится аннотировать кортеж с произвольным числом элементов, то это можно сделать так:

elems: tuple[int, ...]

И тогда все следующие строчки:

elems = (1,)
elems = (1, 2)
elems = (1, 2, 3)

будут корректны для этой аннотации.

Я думаю, вы уже догадались, что для версий языка Python ниже 3.9 следует вместо tuple использовать тип Tuple, если мы хотим дополнительно указывать тип данных:

book: Tuple[str, str, int]
book = ('Балакирев С.М.', 'Аннотация типов', 2022)

Но с версии 3.9 лучше использовать стандартный тип tuple с квадратными скобками.

Аннотация словарей и множеств

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

words: dict[str, int] = {'one': 1, 'two': 2}

Или, для версий Python ниже 3.9:

words: Dict[str, int] = {'one': 1, 'two': 2}

То есть, эта аннотация ведет себя так же, как и при аннотировании списков.

И то же самое для множеств:

persons: set[str] = {'Сергей', 'Михаил', 'Наталья'}

или для Python ниже 3.9:

persons: Set[str] = {'Сергей', 'Михаил', 'Наталья'}

Здесь также указывается единый тип для всех элементов множества. Хотя подсветка будет появляться только тогда, когда все элементы множества или словаря не соответствуют указанному типу. Но модуль mypy находит все эти несоответствия.

Комбинирование типов

Разумеется, все приведенные способы типизации можно использовать не только для отдельных переменных, но и при объявлении функций. Например, так:

def get_positive(digits: list[int]) -> list[int]:
    return list(filter(lambda x: x > 0, digits))
 
 
print(get_positive([1, -2, 3, 4, -5]))

И, смотрите, в аннотации указан один тип данных int, который ожидается увидеть у элементов списка. А что если мы собираемся хранить в списке и целые и вещественные числа. Как тогда определить аннотацию? Все очень просто. Для этого нам понадобится определить составной тип, используя Union из модуля typing:

def get_positive(digits: list[Union[int, float]]) -> list[Union[int, float]]:
    return list(filter(lambda x: x > 0, digits))

То есть, внутри квадратных скобок можно прописывать любые типы, образуя вложенные конструкции. Я напомню, что начиная с версии Python 3.10, эту же нотацию можно определить и так:

list[int | float]

Такой вариант рекомендуемый, если позволяет версия.

Давайте еще немного усложним аннотацию. Предположим, что по умолчанию параметр digits должен принимать значение None. Мы уже знаем, что для этого следует воспользоваться типом Optional модуля typing. Получим:

def get_positive(digits: Optional[list[Union[int, float]]] = None) -> list[Union[int, float]]:
    return list(filter(lambda x: x > 0, digits))

И такие комбинации можно делать сколь угодно глубокие. Только здесь возникает проблема с визуальным восприятием таких конструкций. Чтобы было нагляднее можно воспользоваться алиасами (переменными на те или иные типы). Например, вначале объявить алиас:

Digit = Union[int, float]

А, затем, прописать его при аннотировании:

def get_positive(digits: Optional[list[Digit]] = None) -> list[Digit]:
    return list(filter(lambda x: x > 0, digits))

Так воспринимается гораздо лучше. Разумеется, алиасы можно было бы и дальше определять, например:

def get_positive(digits: Optional[ListDigits] = None) -> ListDigits:
    return list(filter(lambda x: x > 0, digits))

Но увлекаться этим не стоит. Иначе, программисту постоянно придется искать определения алиасов и смотреть, что за типы в них прописаны. Во всем должна быть мера.

Аннотации вызываемых объектов (Callable)

В заключение этого занятия отмечу еще один тип Callable из модуля typing, который позволяет аннотировать вызываемые объекты. Часто это обычные функции, которые передаются как параметры. Давайте я приведу следующий пример. Пусть у нас имеется функция, которая из ряда целых чисел указанного диапазона выбирает числа по заданному критерию и формирует список:

def get_digits(flt: Callable[[int], bool], lst: Optional[list[int]] = None) -> list[int]:
    if lst is None:
        return []
 
    return list(filter(flt, lst))

Здесь первый параметр отмечен как вызываемый тип (Callable), у которого ожидается целочисленный аргумент и булево возвращаемое значение. Затем, мы можем воспользоваться этой функцией следующим образом:

def even(x):
    return bool(x % 2 == 0)
 
 
print(get_digits(even, [1, 2, 3, 4, 5, 6, 7]))

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

def even(x):
    return str(x % 2 == 0)

То появится подсветка кода из-за несоответствия возвращаемого типа и того, что указан в аннотации.

В общем случае тип Callable описывается по синтаксису:

Callable[[TypeArg1, TypeArg2, …], ReturnType]

Например, если нужно аннотировать функцию, у которой нет параметров и она ничего не возвращает:

def hello_callable():
    print("Hello Callable")

То это будет выглядеть так:

Callable[[], None]

Надеюсь, из этого занятия вы поняли, как выполнять аннотирование различных коллекций языка Python.

Курс по Python: https://stepik.org/course/100707

Видео по теме