Этот урок посвящен аннотациям типов в Python. Рассмотрен вопрос контроля типов переменных и функций с использованием комментариев и аннотаций. Приведено описание PEP‘ов, регламентирующих работу с аннотациями, и представлены примеры работы с инструментом mypy для анализа Python кода.
- Зачем нужны аннотации?
- Контроль типов в Python
- Обзор PEP’ов регламентирующий работу с аннотациями
- Использование аннотаций в функциях
- Аннотация переменных
- Отложенная проверка аннотаций
Зачем нужны аннотации?
Для начала ответим на вопрос: зачем нужны аннотации в Python? Если кратко, то ответ будет таким: для того чтобы повысить информативность исходного кода, и иметь возможность с помощью сторонних инструментов производить его анализ. Одной из наиболее востребованных, в этом смысле, тем является контроль типов переменных. Несмотря на то, что Python – это язык с динамической типизацией, иногда возникает необходимость в контроле типов.
Согласно PEP 3107 могут быть следующие варианты использования аннотаций:
- проверка типов;
- расширение функционала IDE в части предоставления информации об ожидаемых типах аргументов и типе возвращаемого значения у функций;
- перегрузка функций и работа с дженериками;
- взаимодействие с другими языками;
- использование в предикатных логических функциях;
- маппинг запросов в базах данных;
- маршалинг параметров в RPC (удаленный вызов процедур).
Контроль типов в Python
Рассмотрим небольшую демонстрацию того, как решался вопрос контроля типов в Python без использования аннотаций. Один из возможных вариантов (наверное самый логичный) решения данной задачи – это использование комментариев, составленных определенным образом.
Выглядит это так:
name = "John" # type: str
Мы создали переменную с именем name и предполагаем, что ее тип – str. Естественно, для самого интерпретатора Python это не имеет значения, мы, без труда можем продолжить нашу мини программу таким образом.
name = "John" # type: str print(name) name = 10 print(name)
И это будет корректно с точки зрения Python. Но если необходимо проконтролировать, что переменной name будут присваиваться значения только строкового типа, мы должны: во-первых указать в комментарии о нашем намерение – это мы сделали, во-вторых использовать специальный инструмент, который выполнит соответствующую проверку. Таким инструментом является mypy.
Установить его можно с помощью pip:
python -m pip install mypy
Если мы сохраним приведенный выше код в файле type_tester.py и выполним следующую команду:
python -m mypy test_type.py
Получим такое сообщение:
test_type.py:3: error: Incompatible types in assignment (expression has type “int”, variable has type “str”)
Оно говорит о том, что обнаружено несоответствие типов в операции присваивание: переменная имеет тип “str“, а ей присвоено значение типа “int“.
Обзор PEP’ов регламентирующий работу с аннотациями
Начнем наше введение в тему аннотаций в Python с краткого обзора четырех ключевых документов:
PEP 3107 — Function Annotations
PEP 526 — Syntax for Variable Annotations
PEP 563 — Postponed Evaluation of Annotations
Первый из них PEP 3107 — Function Annotations, является исторически первым из перечисленных выше документов. В нем описывается синтаксис использования аннотаций в функциях Python. Важным является то, что аннотации не имеют никакого семантического значения для интерпретатора Python и предназначены только для анализа сторонними приложениями. Аннотировать можно аргументы функции и возвращаемое ей значение.
Следующий документ – PEP 484 — Type Hints. В нем представлены рекомендации по использованию аннотаций типов. Аннотация типов упрощает статический анализ кода, рефакторинг, контроль типов в рантайме и кодогенерацию, использующую информацию о типах. В рамках данного документа, определены следующие варианты работы с аннотациями: использование аннотаций в функциях согласно PEP 3107, аннотация типов переменных через комментарии в формате # type: type_name и использование stub-файлов (см. разделы Использование аннотаций в функциях и Аннотация переменных).
В PEP 526 — Syntax for Variable Annotations приводится описание синтаксиса для аннотации типов переменных (базируется на PEP 484), использующего языковые конструкции, встроенные в Python (см. раздел Аннотация переменных).
PEP 563 — Postponed Evaluation of Annotations. Данный PEP вступил в силу с выходом Python 3.7. У подхода работы с аннотация до этого PEP’а был ряд проблем связанных с тем, что определение типов переменных (в функциях, классах и т.п.) происходит во время импорта модуля, и может сложится такая ситуация, что тип переменной объявлен, но информации об этом типе ещё нет, в таком случае тип указывают в виде строки – в кавычках. В PEP 563 предлагается использовать отложенную обработку аннотаций, это позволяет определять переменные до получения информации об их типах и ускоряет выполнение программы, т.к. при загрузке модулей не будет тратится время на проверку типов – это будет сделано перед работой с переменными.
Теперь более подробно остановимся на использовании аннотаций, опираясь на перечисленные выше PEP’ы.
Использование аннотаций в функциях
Указание типов аргументов и возвращаемого значения
В функциях мы можем аннотировать аргументы и возвращаемое значение. Выглядеть это может так.
def repeater(s: str, n: int) -> str: return s * n
Аннотация для аргумента определяется через двоеточие после его имени.
имя_аргумента: аннотация
Аннотация, определяющая тип возвращаемого функцией значения, указывается после ее имени с использованием символов ->
def имя_функции() -> тип
Для лямбд аннотации не поддерживаются.
Доступ к аннотациям функции
Доступ к использованным в функции аннотациям можно получить через атрибут __annotations__, в котором аннотации представлены в виде словаря, где ключами являются атрибуты, а значениями – аннотации. Возвращаемое функцией значение хранится в записи с ключом return.
Содержимое repeater.__annotations__
{'n': int, 'return': str, 's': str}
Аннотация переменных
Создание аннотированных переменных
Можно использовать один из трех способов создания аннотированных переменных.
var = value # type: annotation var: annotation; var = value var: annotation = value
Рассмотрим это на примере работы со строковой переменной с именем name.
name = “John” # type: str name:str; name = “John” name: str = “John”
Приведем еще несколько примеров.
# список scores: List[int] = [] scores.append(1) # кортеж pack: Tuple[int, …] = (1, 2, 3) # логическая переменная flag: bool flag = True # класс class Point: x: int y: int def __init__(self, x: int, y: int): self.x = x self.y = y
Контроль типов с использованием аннотаций
Для проверки можно использовать уже знакомый нам инструмент mypy. Напишем вот такой код.
a:int = 10 b:int = 15 def sq_sum(v1:int, v2:int) -> int: return v1**2 + v2**2 print(sq_sum(a, b))
Сохраним его в файле с именем work.py и запустим mypy для анализа.
python -m mypy work.py
Если не указывать дополнительные ключи, то окно консоли будет чистым, т.к. mypy не найдет никаких ошибок в вашем коде.
Но если заменить первую строку
a: int = 10
на такую:
a = 10.3
и вновь запустить mypy, то увидим вот такое сообщение:
work.py:7: error: Argument 1 to “sq_sum” has incompatible type “float”; expected “int”
При этом, естественно, код будет выполняться без всяких проблем, потому что интерпретатор Python в данном случае не обращает внимание на аннотации.
Отложенная проверка аннотаций
До выхода Python 3.7 определение типов в аннотациях происходило во время импорта модуля, что приводило к проблеме. Например, если выполнить следующий код:
class Rectangle: def __init__(self, height: int, width: int, color: Color) -> None: self.height = height self.width = width self.color = color
То возникнет ошибка NameError: name ‘Color’ is not defined. Она связана с тем, что переменная color имеет тип Color, который пока ещё не объявлен.
В таком случае, мы можем указать тип Color в кавычках, но это не очень удобно.
class Rectangle: def __init__(self, height: int, width: int, color: "Color") -> None: self.height = height self.width = width self.color = color
Эту проблему можно решить воспользовавшись отложенной обработкой аннотаций из Python 3.7.
from __future__ import annotations class Rectangle: def __init__(self, height: int, width: int, color: Color) -> None: self.height = height self.width = width self.color = color class Color: def __init__(self, r: int, g: int, b: int) -> None: self.R = r self.G = g self.B = b rect = Rectangle(1, 2, Color(255, 255, 255))
Можете проверить, что без строки from __future__ import annotations эта программа выполняться не будет.
P.S.
Если вам интересна тема анализа данных, то мы рекомендуем ознакомиться с библиотекой Pandas. На нашем сайте вы можете найти вводные уроки по этой теме. Все уроки по библиотеке Pandas собраны в книге “Pandas. Работа с данными”.
<<<Python. Урок 17. Виртуальные окружения Python. Урок 19. Декораторы функций в Python>>>
Благодарю, хорошее объяснение.
Если мы сохраним приведенный выше код в файле type_tester.py и выполним следующую команду:
python -m mypy test_type.py
type_tester и test_type. Ошибка?
Спасибо, статья интересная.