Путь от утиной к статической типизации: примеры на Python

Раздел: Основы Python -> Система типов Python

Python - это язык с динамической типизацией, где тип переменной определяется во время выполнения, а не на этапе компиляции. Однако с появлением Python 3.5 и PEP 484 разработчики получили возможность добавлять аннотации типов (type hints), которые не влияют на выполнение, но используются статическими анализаторами для поиска ошибок. В этой статье рассмотрены различные подходы к типизации в Python: от традиционной динамической до современных аннотаций и статической проверки.

Способы работы с типами в Python

Как добавить статическую проверку типов в Python-проекте?

Основной и наиболее эффективный способ - использовать аннотации типов (PEP 484) совместно со статическим анализатором, таким как mypy. Это позволяет выявлять ошибки до запуска программы.

Пример функции с аннотациями

def greet(name: str, age: int) -> str:
    return f"Привет, {name}. Тебе {age} лет."

Python типизация (типизация в python)

Запуск mypy для проверки:

$ mypy script.py

Проблема: mypy может пропускать некоторые динамические конструкции или выдавать ложные ошибки. Решение: настройка mypy (файл mypy.ini или pyproject.toml), использование --strict, а также игнорирование отдельных строк через комментарий # type: ignore.

Как аннотировать типы в коде без использования mypy?

До официального введения аннотаций в Python 3.0 применялись комментарии вида # type:. Этот способ до сих пор поддерживается для обратной совместимости.

def add(x, y):
    # type: (int, int) -> int
    return x + y

Такие комментарии распознаются mypy, но синтаксис громоздок, особенно при большом количестве параметров. Современный код рекомендует использовать аннотации в сигнатуре функции.

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

Как использовать динамическую типизацию без аннотаций?

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

def multiply(a, b):
    return a * b

Функция будет работать как для чисел, так и для строк (повторение). Однако при неправильных типах возникнет ошибка во время выполнения. Такой подход подходит для простых скриптов, но усложняет отладку в больших проектах.

Частая проблема: передача несовместимого типа обнаруживается только в runtime. Решение: добавить проверки isinstance(), но это загромождает код. Лучше использовать аннотации.

Как точно описать структуру словаря или конкретные значения?

Для описания точной структуры словарей предназначен TypedDict (из typing). Для фиксированных значений - Literal (Python 3.8+).

from typing import TypedDict, Literal

class Person(TypedDict):
    name: str
    age: int
    city: str

def get_person() -> Person:
    return {"name": "Анна", "age": 30, "city": "Москва"}

def set_mode(mode: Literal["auto", "manual"]) -> None:
    pass

Проблема: TypedDict не поддерживает вычисляемые ключи и наследование от dict. Решение: использовать dataclasses или Pydantic для более строгой проверки.

Как реализовать утиную типизацию с проверками?

Утиная типизация - основа Python: объект считается подходящим, если он имеет необходимые методы. Для формального описания таких требований используется Protocol (PEP 544, Python 3.8+).

from typing import Protocol

class Flyable(Protocol):
    def fly(self) -> None:
        ...

class Bird:
    def fly(self) -> None:
        print("Птица летит")

def let_fly(obj: Flyable) -> None:
    obj.fly()

let_fly(Bird())  # OK

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

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

Дополнительные примеры работы с типами

Ниже приведены расширенные примеры, демонстрирующие тонкости системы типов Python.

Обобщённое программирование (Generic)

Обобщённые типы позволяют писать код, работающий с любым типом, но сохраняя информацию о конкретном типе на этапе проверки.

Пример
from typing import TypeVar, Generic, 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()

# Использование
int_stack = Stack[int]()
int_stack.push(1)
int_stack.push(2)
value = int_stack.pop()  # mypy выведет int

Результат проверки mypy: ошибок нет.

$ mypy stack.py
Success: no issues found

Использование Union и Optional (Python 3.10+)

Начиная с Python 3.10, объединение типов можно записывать через |, а Optional[X] заменяется на X | None.

Пример
def parse_number(text: str) -> int | None:
    try:
        return int(text)
    except ValueError:
        return None

result = parse_number("42")
if result is not None:
    print(result + 1)  # mypy понимает, что result не None

Старый стиль (до 3.10):

Пример
from typing import Union, Optional
def old_style(x: Union[int, str]) -> Optional[bool]: ...

Частая ошибка: забыть обработать None перед разыменованием. mypy подскажет: error: Item "None" of "Optional[int]" has no attribute ...

Callable - аннотация функций как параметров

Для передачи функции в качестве аргумента используется Callable.

Пример
from typing import Callable

def apply_operation(x: int, y: int, operation: Callable[[int, int], int]) -> int:
    return operation(x, y)

result = apply_operation(5, 3, lambda a, b: a * b)  # 15

mypy проверяет совпадение сигнатур. Ошибка: передача функции с неверными параметрами.

Dataclasses с аннотациями

Декоратор dataclass упрощает создание классов, а аннотации типов становятся обязательными для полей.

Пример
from dataclasses import dataclass

@dataclass
class Point:
    x: float
    y: float
    label: str = "A"

p = Point(1.0, 2.0, "B")
print(p)  # Point(x=1.0, y=2.0, label='B')

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

TypedDict с частичными ключами

Можно указать необязательные ключи с помощью NotRequired (Python 3.11+) или total=False.

Пример
from typing import TypedDict

class Movie(TypedDict, total=False):
    title: str
    year: int

def movie_info() -> Movie:
    return {"title": "Inception"}  # OK, year не обязателен

Протоколы с generic

Протоколы могут быть обобщёнными, что позволяет описывать типизированные интерфейсы.

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

T = TypeVar('T')
class Comparable(Protocol[T]):
    def __lt__(self, other: T) -> bool: ...

def max_of_two(a: T, b: T) -> T:
    return a if a > b else b

# mypy проверит, что у типа есть __lt__

Использование mypy в strict mode

Строгий режим включает дополнительные проверки. Настройка в mypy.ini:

Пример
[mypy]
strict = True

Пример кода, который выдаст ошибки при strict mode:

Пример
def foo(x) -> int:  # ошибка: параметр без аннотации
    return x
$ mypy --strict script.py
script.py:1: error: Function is missing a type annotation for one or more arguments

Игнорирование ошибок mypy

Иногда требуется подавить проверку в конкретном месте. Используйте комментарий # type: ignore с кодом ошибки.

Пример
x = some_dynamic_call()  # type: ignore[assignment]

типизация в Python - comments

En
Python типизация (python)