Хабрахабр

Введение в аннотации типов Python

Введение


Автор иллюстрации — Magdalena Tomczyk

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

6+) поддерживает аннотации типов переменных, полей класса, аргументов и возвращаемых значений функций: Сохраняя идею динамической утиной типизации в современных версиях Python (3.

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

Меня зовут Тихонов Андрей и я занимаюсь backend-разработкой в Lamoda.

В этой статье я хочу объяснить основы использования аннотаций типов и рассмотреть типичные примеры, реализуемые аннотациями из пакета typing.

Инструменты, поддерживающие аннотации

Аннотации типов поддерживаются многими IDE для Python, которые выделяют некорректный код или выдают подсказки в процессе набора текста.

Например, так это выглядит в Pycharm:

Подсветка ошибок

Подсказки:

Так же аннотации типов обрабатываются и консольными линтерами.

Вот вывод pylint:

$ pylint example.py
************* Module example
example.py:7:6: E1101: Instance of 'int' has no 'startswith' member (no-member)

А вот для того же файла что нашел mypy:

$ mypy example.py
example.py:7: error: "int" has no attribute "startswith"
example.py:10: error: Unsupported operand types for // ("str" and "int")

Например, mypy и pycharm по разному обрабатывают смену типа переменной. Поведение разных анализаторов может отличаться. Далее в примерах я буду ориентироваться на вывод mypy.

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

Основы

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

Более сложные кейсы будут рассмотрены ниже. В простейшем случае аннотация содержит непосредственно ожидаемый тип. Однако использовать можно только те возможности, что реализованы в базовом классе. Если в качестве аннотации указан базовый класс, допустимо передача экземпляров его наследников в качестве значений.

После этого может идти инициализация значения. Аннотации для переменных пишут через двоеточие после идентификатора. Например,

price: int = 5
title: str

Например, Параметры функции аннотируются так же как переменные, а возвращаемое значение указывается после стрелки -> и до завершающего двоеточия.

def indent_right(s: str, width: int) -> str: return " " * (max(0, width - len(s))) + s

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

class Book: title: str author: str def __init__(self, title: str, author: str) -> None: self.title = title self.author = author b: Book = Book(title='Fahrenheit 451', author='Bradbury')

Подробнее про dataclass Кстати, при использовании dataclass типы полей необходимо указывать именно в классе.

Встроенные типы

Хоть вы и можете использовать стандартные типы в качестве аннотаций, много полезного сокрыто в модуле typing.

Optional

Если вы пометите переменную типом int и попытаетесь присвоить ей None, будет ошибка:

Incompatible types in assignment (expression has type "None", variable has type "int")

Обратите внимание, тип опциональной переменной указывается в квадратных скобках Для таких случаев предусмотрена в модуле typing аннотация Optional с указанием конкретного типа.

from typing import Optional amount: int
amount = None # Incompatible types in assignment (expression has type "None", variable has type "int") price: Optional[int]
price = None

Any

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

unknown_item: Any = 1
print(unknown_item)
print(unknown_item.startswith("hello"))
print(unknown_item // 0)

Однако в этом случае предполагается, что хоть передан может быть любой объект, обращаться с ним можно только как с экземпляром object. Может возникнуть вопрос, почему не использовать object?

unknown_object: object
print(unknown_object)
print(unknown_object.startswith("hello")) # error: "object" has no attribute "startswith"
print(unknown_object // 0) # error: Unsupported operand types for // ("object" and "int")

Union

Union с указанием списка типов в квадратных скобках. Для случаев, когда необходимо допустить использование не любых типов, а только некоторых, можно использовать аннотацию typing.

def hundreds(x: Union[int, float]) -> int: return (int(x) // 100) % 10 hundreds(100.0)
hundreds(100)
hundreds("100") # Argument 1 to "hundreds" has incompatible type "str"; expected "Union[int, float]"

Кстати, аннотация Optional[T] эквивалентна Union[T, None], хотя такая запись и не рекомендуется.

Коллекции

Механизм аннотаций типов поддерживает механизм дженериков (Generics, подробнее во второй части статьи), которые позволяют специфицировать для контейнеров типы элементов, хранящихся в них.

Списки

Однако если хочется конкретизировать, какие элементы содержит список, он такая аннотация уже не подойдёт. Для того, чтобы указать, что переменная содержит список можно использовать тип list в качестве аннотации. List. Для этого есть typing. Аналогично тому, как мы указывали тип опциональной переменной, мы указываем тип элементов списка в квадратных скобках.

titles: List[str] = ["hello", "world"]
titles.append(100500) # Argument 1 to "append" of "list" has incompatible type "int"; expected "str"
titles = ["hello", 1] # List item 1 has incompatible type "int"; expected "str" items: List = ["hello", 1]

Но при этом нет ограничений на аннотацию элемента: можно использовать Any, Optional, List и другие. Предполагается, что список содержит неопределенное количество однотипных элементов. Если тип элемента не указан, предполагается, что это Any.

Set и typing. Кроме списка аналогичные аннотации есть для множеств: typing. FrozenSet.

Кортежи

Синтаксис похож с одним отличием: в квадратных скобках указывается тип каждого элемента кортежа по отдельности. Кортежи в отличие от списков часто используются для разнотипных элементов.

Если же планируется использовать кортеж аналогично списку: хранить неизвестное количество однотипных элементов, можно воспользоваться многоточием (...).

Аннотация Tuple без указания типов элементов работает аналогично Tuple[Any, ...]

price_container: Tuple[int] = (1,)
price_container = ("hello") # Incompatible types in assignment (expression has type "str", variable has type "Tuple[int]")
price_container = (1, 2) # Incompatible types in assignment (expression has type "Tuple[int, int]", variable has type "Tuple[int]") price_with_title: Tuple[int, str] = (1, "hello") prices: Tuple[int, ...] = (1, 2)
prices = (1, )
prices = (1, "str") # Incompatible types in assignment (expression has type "Tuple[int, str]", variable has type "Tuple[int, ...]") something: Tuple = (1, 2, "hello")

Словари

Dict. Для словарей используется typing. Отдельно аннотируется тип ключа и тип значений:

book_authors: Dict[str, str] =
book_authors["1984"] = 0 # Incompatible types in assignment (expression has type "int", target has type "str")
book_authors[1984] = "Orwell" # Invalid index type "int" for "Dict[str, str]"; expected type "str"

DefaultDict и typing. Аналогично используются typing. OrderedDict

Результат выполнения функции

Но есть несколько особенных случаев. Для указания типа результата функции можно использовать любую аннотацию.

Для аннотации так же используем None. Если функция ничего не возвращает (например, как print), её результат всегда равен None.

Корректными вариантами завершения такой функции будут: явный возврат None, возврат без указания значения и завершение без вызова return.

def nothing(a: int) -> None: if a == 1: return elif a == 2: return None elif a == 3: return "" # No return value expected else: pass

Если же функция никогда не возвращает управление (например, как sys.exit), следует использовать аннотацию NoReturn:

def forever() -> NoReturn: while True: pass

Если это генераторная функция, то есть её тело содержит оператор yield, для возвращаемого можно воспользоватьтся аннотацией Iterable[T], либо Generator[YT, ST, RT]:

def generate_two() -> Iterable[int]: yield 1 yield "2" # Incompatible types in "yield" (actual type "str", expected type "int")

Вместо заключения

Iterator, typing. Для многих ситуаций в модуле typing есть подходящие типы, однако я не буду рассматривать все, так как поведение аналогично рассмотренным.
Например, есть Iterator как generic-версия для collections.abc. SupportsInt для того, чтобы указать что объект поддерживает метод __int__, или Callable для функций и объектов, поддерживающих метод __call__

Так же стандарт определяет формат аннотаций в виде комментариев и stub-файлы, которые содержат информацию только для статических анализаторов.

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

Теги
Показать больше

Похожие статьи

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»
Закрыть