В первой части нашего глубокого погружения в аннотации типов 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