В первой части нашего глубокого погружения в аннотации типов Python мы рассмотрели такие фундаментальные инструменты, как Generic, TypeVar и TypedDict. Теперь давайте продолжим наше путешествие, углубившись в более продвинутые и тонкие инструменты подсказки типов, доступные в модуле typing.

аннотированный

Подсказка типа Annotated, представленная в Python 3.9, позволяет прикреплять к подсказке типа дополнительные метаданные. Эти метаданные не влияют напрямую на проверку типов, но могут использоваться для различных других целей, таких как проверка, документирование или другое пользовательское поведение.

По своей сути Annotated принимает тип и любое количество элементов метаданных:

from typing import Annotated

Age = Annotated[int, "value should be between 0 and 120"]

В приведенном выше примере Age по сути является int, но несет дополнительную информацию о том, что значение должно находиться в диапазоне от 0 до 120.

Проверка
Вы можете использовать метаданные в Annotated для проверки значений во время выполнения:

def validate_age(value: Age) -> None:
    _, metadata = Age.__metadata__
    if not (0 <= value <= 120):
        raise ValueError(f"Invalid age. {metadata}")

Документация
Метаданные могут использоваться для предоставления разработчикам дополнительной документации:

Coordinate = Annotated[tuple, "A tuple representing (x, y) coordinates on a 2D plane"]

Сериализация и десериализация
При работе с сериализацией данных Annotated может предоставить подсказки о том, как обрабатывать данные:

DateStr = Annotated[str, {"format": "YYYY-MM-DD"}]


def serialize_date(date: datetime.date) -> DateStr:
    format_str = DateStr.__metadata__[1]["format"]
    return date.strftime(format_str)

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

QueryParam = Annotated[str, {"required": True}]

Несколько метаданных
Annotated также могут содержать несколько элементов метаданных:

Price = Annotated[float, "The price of an item in USD", {"min_value": 0.0}]

Буквальный

Literal используется, когда параметр может иметь только определенные литеральные значения.

from typing import Literal


def draw_shape(shape: Literal["circle", "square", "triangle"]) -> None:
    if shape == "circle":
        # Draw circle
        pass
    elif shape == "square":
        # Draw square
        pass
    # ... and so on

ПарамСпец

ParamSpec — это мощный инструмент, представленный в Python 3.10 и PEP 612. Он предназначен для сбора спецификации параметров вызываемого объекта, что делает его особенно полезным для декораторов, функций высшего порядка и других функций, которые работают с другими функциями или возвращают их.

По своей сути ParamSpec фиксирует как позиционные, так и ключевые параметры вызываемого объекта:

from typing import Callable, ParamSpec

P = ParamSpec("P")


def decorator(func: Callable[P, int]) -> Callable[P, int]:
    def wrapper(*args: P.args, **kwargs: P.kwargs) -> int:
        print("Before calling the function")
        result = func(*args, **kwargs)
        print("After calling the function")
        return result
    return wrapper

В приведенном выше примере функция decorator может принимать любую функцию func независимо от ее сигнатуры и возвращать новую функцию с той же сигнатурой.

ParamSpec можно комбинировать с другими подсказками типов для создания более сложных подписей:

P = ParamSpec("P")
R = TypeVar("R")


def timed_call(func: Callable[P, R]) -> Tuple[float, R]:
    import time

    def wrapper(*args: P.args, **kwargs: P.kwargs) -> Tuple[float, R]:
        start_time = time.time()
        result = func(*args, **kwargs)
        end_time = time.time()
        duration = end_time - start_time
        return duration, result

    return wrapper

В этом примере timed_call — это декоратор, который возвращает как время, затраченное на вызов функции, так и ее результат.

Протокол

Подсказка типа Protocol, представленная в Python 3.8 вместе с PEP 544, является краеугольным камнем структурного подтипирования в Python. В отличие от традиционного наследования на основе классов, которое основано на номинальном подтипировании, Protocol допускает структурное подтипирование, при котором совместимость типов определяется структурой (т. е. атрибутами и методами) типа, а не его явным наследованием.

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

from typing import Protocol


class SupportsClose(Protocol):
    def close(self) -> None:
        pass

Любой класс, у которого есть метод close, соответствующий сигнатуре, будет считаться подтипом SupportsClose, даже если он не наследуется от него явно.

class FileResource:
    def close(self) -> None:
        print("File closed")


# This is valid, even though FileResource doesn't inherit from SupportsClose
def close_resource(resource: SupportsClose) -> None:
    resource.close()

Protocol также может указывать обязательные атрибуты:

class Named(Protocol):
    name: str

Любой класс с атрибутом name типа str будет считаться подтипом Named.

Вы можете расширить и объединить несколько протоколов:

class Readable(Protocol):
    def read(self) -> str:
        pass


class Writable(Protocol):
    def write(self, data: str) -> None:
        pass


class ReadWrite(Readable, Writable):
    pass

Подтип ReadWrite должен иметь методы read и write.

Несмотря на то, что Protocol является мощным инструментом, чрезмерное его использование может затруднить понимание кода. Очень важно найти баланс между структурной и номинальной типизацией в зависимости от конкретного варианта использования.

Последовательность

Sequence представляет любой объект, поддерживающий итерацию и индексацию, например списки или строки.

from typing import Sequence


def first_element(items: Sequence) -> Any:
    return items[0]

Картирование

Mapping представляет любой объект, который сопоставляет ключи со значениями, например словари.

from typing import Mapping


def get_value(data: Mapping, key: str) -> Any:
    return data.get(key)

Заключение

Система подсказок типов Python, особенно с ее расширенными функциями, является свидетельством эволюции языка и его стремления повысить ясность, надежность и удобство обслуживания кода. В этой серии из двух частей мы рассмотрели множество аннотаций типов, от базовых до продвинутых. Такие инструменты, как Protocol, ParamSpec и Annotated, демонстрируют глубину и гибкость Python, позволяя разработчикам выражать сложные отношения типов, поведение и ограничения.

Let's connect!
LinkedIn