Python. Урок 18. Аннотация типов в Python

Автор: | 25.09.2018

Этот урок посвящен аннотациям типов в Python. Рассмотрен вопрос контроля типов переменных и функций с использованием комментариев и аннотаций. Приведено описание PEP‘ов, регламентирующих работу с аннотациями, и представлены примеры работы с инструментом mypy для анализа Python кода.

Зачем нужны аннотации?

Для начала ответим на вопрос: зачем нужны аннотации в 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 484 — Type Hints

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 эта программа выполняться не будет.

 

Поделиться
Share on VK
VK
Tweet about this on Twitter
Twitter
Share on Facebook
Facebook
Share on Google+
Google+

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

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


Нажимая на кнопку "Отправить комментарий", я даю согласие обработку персональных данных и принимаю политику конфиденциальности.