Типизированные списки: от typing.List до list[str]

Раздел: Основы Python -> Типизация

Аннотации типов для списков позволяют статическим анализаторам, таким как mypy, проверять корректность использования элементов. Основным инструментом является тип typing.List, представляющий собой обобщённый (generic) тип для списков. В Python 3.9 и новее можно использовать встроенный синтаксис list[X] без импорта. В этой статье рассматриваются различные способы указания типов списков, их цели и типичные ошибки.

Основное решение: typing.List

Классический способ - импортировать List из модуля typing и использовать его для аннотации. Например, функция, принимающая список целых чисел:

from typing import List

def sum_even(numbers: List[int]) -> int:
    return sum(n for n in numbers if n % 2 == 0)

Python typed list (типизированные списки в python (typing.list))

Пояснение: List[int] означает список, каждый элемент которого является целым числом. Если передать список с другими типами, mypy выдаст ошибку. Также можно аннотировать переменные:

names: List[str] = ['Alice', 'Bob']

Проблемы: если забыть импорт, возникнет NameError: name 'List' is not defined. Также при использовании List с устаревшей версией Python (ниже 3.5) модуль typing недоступен.

Типичная ошибка:

def get_first(items: List) -> int:   # не указан тип элемента
    return items[0]

mypy может не обнаружить проблем, так как List без параметра эквивалентен List[Any]. Всегда указывайте тип элемента.

Как указать тип списка без импорта typing?

Начиная с Python 3.9, можно использовать встроенный синтаксис list[Тип]. Это избавляет от необходимости импортировать List. Пример:

def greet(users: list[str]) -> None:
    for user in users:
        print(f'Привет, {user}')

Также поддерживается вложенность: list[list[int]]. Для обратной совместимости с Python 3.8 и старше можно добавить from __future__ import annotations, но лучше использовать typing.List для старых версий.

Проблема:

Если проект разрабатывается для Python 3.8 и ниже, синтаксис list[str] вызовет TypeError: 'type' object is not subscriptable. Решение: либо использовать from typing import List, либо добавить from __future__ import annotations (тогда аннотации остаются строками и не вычисляются во время выполнения).

Как создать функцию, работающую со списком любого типа с сохранением типа?

Используется TypeVar для параметризации. Определяется переменная типа, а затем аннотируется список с ней. Пример:

from typing import TypeVar, List

T = TypeVar('T')

def first_element(items: List[T]) -> T:
    return items[0]

Теперь first_element([1,2,3]) вернёт int, а first_element(['a','b']) вернёт str. mypy выводит конкретный тип. Проблемы: если не указать возвращаемый тип, mypy может вывести Any. Важно задать TypeVar с именем.

Ошибка:

Использование List[T] без импорта TypeVar или List.

Как создать класс, хранящий типизированный список?

Для классов используется Generic[T]. Пример класса стека:

from typing import Generic, TypeVar, List

T = TypeVar('T')

class Stack(Generic[T]):
    def __init__(self) -> None:
        self._items: List[T] = []

    def push(self, item: T) -> None:
        self._items.append(item)

    def pop(self) -> T:
        return self._items.pop()

Теперь Stack[int]() будет ожидать целые числа. Проблемы: требуется импорт Generic. Также можно наследовать List[T], но это приведёт к неожиданностям, если переопределить методы.

Сложность:

Новички могут забыть указать параметр типа при создании экземпляра: stack = Stack() будет Stack[Any].

Как описать список, содержащий None или элементы разных типов?

Используется Optional и Union:

from typing import List, Optional, Union

# Список может содержать None или числа
nullable_numbers: List[Optional[int]] = [1, None, 3]

# Список чисел или строк
mixed: List[Union[int, str]] = [1, 'two', 3]

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

Ошибка:

Попытка выполнить операцию, допустимую только для одного из типов, без проверки. mypy укажет на небезопасное использование.

Расширенные примеры и продвинутые сценарии

Ниже приведены более сложные примеры использования типизированных списков.

Пример 1: Вложенные списки (матрицы)

Пример
from typing import List

def transpose(matrix: List[List[int]]) -> List[List[int]]:
    return [[row[i] for row in matrix] for i in range(len(matrix[0]))]
# mypy проверит, что matrix - список списков целых чисел

Пример 2: Использование с dataclasses

Пример
from dataclasses import dataclass
from typing import List

@dataclass
class Student:
    name: str
    grades: List[int]
# Создание экземпляра: s = Student('Alice', [90, 85])

Пример 3: TypeVar с ограничением (bound)

Пример
from typing import TypeVar, List

class Animal:
    def sound(self) -> str:
        return '...'

T = TypeVar('T', bound=Animal)

def make_sounds(animals: List[T]) -> List[str]:
    return [a.sound() for a in animals]
# Функция принимает список любых наследников Animal

Пример 4: Функция высшего порядка с Callable

Пример
from typing import List, Callable, TypeVar

T = TypeVar('T')
U = TypeVar('U')

def transform(items: List[T], func: Callable[[T], U]) -> List[U]:
    return [func(item) for item in items]
# Пример: transform([1,2,3], str) -> ['1','2','3']

Пример 5: Использование Iterable вместо List для гибкости

Пример
from typing import Iterable, List

def process(items: Iterable[int]) -> List[int]:
    return [x*2 for x in items]
# Функция принимает список, кортеж, генератор и т.д.

Пример 6: Собственный generic-класс с несколькими параметрами

Пример
from typing import Generic, TypeVar, List

K = TypeVar('K')
V = TypeVar('V')

class BiMap(Generic[K, V]):
    def __init__(self) -> None:
        self._keys: List[K] = []
        self._values: List[V] = []
    def add(self, key: K, value: V) -> None:
        self._keys.append(key)
        self._values.append(value)
# Использование: bm = BiMap[int, str](); bm.add(1, 'one')

Типизированные списки в Python (typing.List) - comments

En
Python typed list (python)