Аннотации типов в python

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

В связи с динамической природой python, определение и проверка типа используемого объекта представляет собой достаточно большую сложность. Из-за этого разработчикам бывает сложно понять, что делает написанный кем-то другим код, и, что самое важное, ограничивается функционал инструментов проверки типов во многих средах разработки (первое, что приходит в голову - PyCharm и PyDev) из-за отсутствия какого-либо индикатора типа объектов. Как результат - IDE правильно определяет тип объекта примерно в 50% случаев.

Выделим из презентации Гвидо два важных слайда:

Для чего нужны аннотации типов?

  1. Они помогают утилитам автоматической проверки кода: определив, какой тип объекта вы ожидаете, такой анализатор сможет еще до запуска программы выдать сообщение, если в ходе работы вы ошиблись и передали объект другого типа.
  2. Они являются своего рода документацией к коду: другой человек, просматривающий ваш код, поймет где какой тип ожидается без погружения в дебри TypeError.
  3. Помогают создавать более точные и надежные IDE: анализируя аннотированный код, среда разработки сможет преложить методы, соответствующие типу объекта. Вы, вероятно, не раз сталкивались с ситуацией, когда инструменты автодополнения в IDE предлагали методы, не соответствующие выбранному объекту.

Зачем вообще использовать методы статической типизации?

  1. Значительная часть ошибок в коде будет обнаружена еще до запуска программы. Это, мне кажется, очевидно.
  2. Это упрощает процесс работы над большими проектами. И снова, сложно не согласиться. Статические языки обеспечивают контроль и надежность, чего часто не хватает динамическим языкам. Чем больше и сложнее ваше приложение, тем больше вы нуждаетесь именно в таком контроле и предсказуемости поведения.
  3. Все большие команды уже давно используют статические анализаторы. Это еще раз подтверждает первые два пункта.

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

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

Проверка типов при помощи mypy

Теперь не помешает немного демонстрации. Для этого я буду использовать mypy, библиотеку работающую с аннтоациями типов в соответствии с PEP. Она отлично подойдет для каждого, кто заинтересовался вопросом типизации в python и не знает, с чего начать.

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

Как следует из упомянутых PEP, аннотации типа могут быть оформлены тремя способами:

  • Аннотации функций (PEP 3107)
  • Stub-файлы для встроенных модулей
  • Специальные # type: type комментарии

Кроме того, аннотации типов намного удобнее использовать совместно с новым typing модулем, представленным в python3.5. Этот модуль значительно облегчит работу с типами: он содержит определения множества дополнительных ABC-типов, вспомогательные функции и декораторы для статической проверки. Типы для большинства ABC из collections.abc также входят в состав этого модуля, однако в общей (Generic) форме.

Для тех, кто хочет получить более подробное объяснение все этого - на сайте mypy имеется очень подробная документация, которая содержит множество примеров кода, демонстрирующих функционал этой утилиты проверки типов, и безусловно рекомендуется к прочтению.

Аннотации к функциям и специальные комментарии

Для начала, давайте понаблюдаем, чего мы сможем достичь при помощи специальных комментариев. Комментарии вида # type: type позволяют во время определения переменной указать тип объекта, если он не является очевидным. Типы переменных в простых определениях вроде x = 5 обычно легко выводятся, но в случае со списком (с учетом типа элементов этого списка) все не так просто.

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

# базовый List, поддерживает индексацию.
from typing import List

# В этом случае тип легко выводится как type: int.
i = 0

# И хотя тип list так же легко выводится в этом случае,
# отсутствует возможность указать содерджимое списка
# Используя type: List[str] мы указываем, что будем использовать
# список из строк
a = []  # type: List[str]

# Добавление числового значения int
# статически неверно
a.append(i)

# Добавление строки - в порядке
a.append("i")

print(a)  # [0, 'i']

Если мы сохраним эти команды в файле и выполним его при помощи базового интерпретатора, все отработает без ошибок и print(a) просто выведет в консоль содержимое списка a. Комментарии # type будут проигнорированы, обычный интерпретатор python воспринимает их как самые обычные комментарии без дополнительного семантического значения.

Но с другой стороны, запустив выполнение этого файла при помощи mypy, мы получим следующий результат:

(Python3)ubuntu@user: mypy type_hints_code.py
type_hints_code.py:14: error: Argument 1 to "append" of "list" has incompatible type "int"; expected "str"

Который нам показывает, что список из строковых объектов не может содержать элемент типа int. Это можно исправить двумя способами: сохранив тип списка и добавляя в него только элементы типа str, либо изменив тип содержимого списка a на другой, позволяющий хранить элементы различных типов (изменив определение типа списка на List[Any], предварительно импортировав его из typing).

Аннотации к функциям выполняются в виде param_name: type после каждого параметра в определении, а тип возвращаемого значения определяется при помощи -> type перед закрывающим определение функции двоеточием. Все аннотации хранятся в атрибуте функции __annotations__ в виде словаря. Ниже тривиальный пример, не требующий дополнительных импортов из модуля typing:

def annotated(x: int, y: str) -> bool:
    return x < y

Атрибут annotated.__annotations__ теперь содержит следующие значения:

{'y': <class 'str'>, 'return': <class 'bool'>, 'x': <class 'int'>}

Если мы абсолютные новички и не сталкивались с TypeError при сравнении несовместимых типов, то, выполнив статическую проверку типов в annotated функции, сможем обнаружить ошибку:

(Python3)ubuntu@user: mypy type_hints_code.py
type_hints_code.py: note: In function "annotated":
type_hints_code.py:2: error: Unsupported operand types for > ("str" and "int")

Помимо прочего, вызхов функции с неверными аргументами так же будет обнаружен во время проверки:

annotated(20, 20)

# mypy complains:
type_hints_code.py:4: error: Argument 2 to "annotated" has incompatible type "int"; expected "str"

Описанные выше примеры могут быть расширены для выявления ошибок, намного более сложных, чем базовые вызовы и операции. Я лишь поверхностно описал возможности выбора типов, изучение модуля typing, PEP-ов или документации к mypy даст намного более полное понимание методологии аннотации типов.

Stub-файлы

Stub-файлы могут быть использованы в двух случаях:

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

Stub-файлы (с расширением .pyi) по-сути, являются анноториванным интерфейсом к модулю, который вы собираетесь писать или использовать. Они содержат заголовки функций, для которых необходимо добавить проверку типов, но не содержат тела функции. Чтобы это стало понятнее, давайте рассмотрим 3 случайные функции в модуле rundfunc.py:

def message(s):
    print(s)

def alter_contents(my_iterable):
    return [i for i in my_iterable if i % 2 == 0]

def combine(message_func, it_func):
    message_func("Printing the Iterable")
    a = alter_contents(range(1, 20))
    return set(a)

Если мы хотим добавить какие-то ограничения на типы параметров функций, нам нужно создать stub-файл randfunc.pyi и указать аннотации в нем. Минус такого подхода в том, что в самом исходном коде модуля не отображаются наложенные ограничения на поддерживаемые типы.

Структура этого файла достаточно проста: добавляется определение функции с пустым телом (pass вместо логики) и указываются аннотации типов исходя из требований. Вот, предположим, что мы хотим чтобы iterable-контейнеры в randfunc содержали только объекты типа int:

# Stub для randfucn.py
from typing import Iterable, List, Set, Callable

def message(s: str) -> None: pass

def alter_contents(my_iterable: Iterable[int])-> List[int]: pass

def combine(
    message_func: Callable[[str], Any],
    it_func: Callable[[Iterable[int]], List[int]]
)-> Set[int]: pass

Функция combine дает нам понимание того, почему может возникнуть желание вынести аннтоации типов в отдельный файл: временами они очень сильно засоряют код и ухудшают читаемость.

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

Источник: StackOverflow