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

Аннотации типов (Type Hints) в Python – инструмент, позволяющий сделать код более информативным и избавиться от некоторых проблем, связанных с динамической типизацией.

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

first = 123
first = "Text"
first = [1,2,3,4,5]

Помните, что в Python сами переменные не хранят значения, а в них сохраняется лишь ссылка на объект

А что делать, если мы хотим сообщить себе и другим разработчикам, что в переменной first необходимо сохранять значения только определенного типа данных, например тип int? Ведь нужно подсказывать себе на будущее и другим разработчикам (и даже иногда самому pycharm) с каким типом данных мы хотим работать в определенной переменной. Для этого и придумали аннотации.

Синтаксис аннотаций впервые появился в Python 3.5  «PEP 484 – Type Hints» и распространялся только на параметры функции и возвращаемые значения. Затем в Python 3.6 добавили возможность аннотировать переменные в любом участке вашей программы, описана эта возможность в «PEP 526 – Syntax for Variable Annotations». С аннотации переменных мы и начнем наше обучение.

Чтобы воспользоваться аннотацией, необходимо после имени переменной поставить двоеточие и указать какой тип данных вы ожидаете в этой переменной

переменная: тип_данных

В примере ниже мы определяем для переменной first аннотацию типа int с присваиванием значения в переменную.

first: int
first = 100

Эти строки можно заменить одной короткой записью

first: int = 100

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

Теперь, если поменять значение переменной на тип данных, отличающийся от типа int,  IDE будет подсвечивать эти значения и при наведении на эти места будет сообщать о том, какой тип данных ожидается в переменной.

Аннотации в функциях Python

Аннотации широко используются в функциях с целью указания ожидаемых типов данных для параметров функции. Используется следующий синтаксис для аннотации параметров функции:

def имя_функции(параметр1: тип_параметра1, параметр2: тип_параметра2, ...):
   <тело функции>

После имени параметра ставится знак двоеточия, ставится отступ в один пробел и указывается тип данных

Возьмём к примеру функцию add_numbers, которая имеет два параметра a и b. Функция add_numbers складывает свои параметры и возвращает результат сложения:

def add_numbers(a, b):
    return a + b

Если мы ожидаем, что передавать в эту функцию будут только целые числа, тогда наша функция с аннотациями будет иметь следующий вид:

def add_numbers(a: int, b: int):
    return a + b

Если в момент вызова передавать другой тип данных, то например IDE будет подсвечивать такие значения:

Однако никаких ошибок не будет при вызове функции, если передавать неправильный тип данных.

Помимо параметров в функции можно и нужно аннотировать тип возвращаемого значения. Взгляните на синтаксис такого оформления

def имя_функции(параметр1: тип_параметра1, параметр2: тип_параметра2, ...) -> тип_возврата:
   <тело функции>

В самом конце определения функции перед последним знаком : нужно указать символ ->, обозначающий возвращаемое значение, и затем указать тип, который ожидается из функции

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

def add_numbers(a: int, b: int) -> int:
  return a + b


Атрибут __annotations__

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

Вот взгляните на пример, где мы выводим информацию по аннотациям функции add_numbers

def add_numbers(a: int, b: int) -> int:
  return a + b

 
print(add_numbers.__annotations__)

Мы получим следующий результат:

{'a': <class 'int'>, 'b': <class 'int'>, 'return': <class 'int'>}

Мы сразу видим названия параметров и их типы данных. Отдельным ключом «return» указывается тип возвращаемого значения.

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

Пока во всех наших примерах мы использовали везде тип int, но, конечно же, вы можете использовать и другие типы данных. Например, можем написать похожую функцию count_letters, которая будет принимать две строки и возвращать общее количество их символов. Вот как бы выглядела эта функция с аннотациями

def count_letters(word_1: str, word_2: str) -> int:
    return len(word_1) + len(word_2)


print(count_letters.__annotations__)

# {'word_1': <class 'str'>, 'word_2': <class 'str'>, 'return': <class 'int'>}

В атрибут __annotations__  не попадает информация об аннотации локальных переменных, созданных внутри функции. Атрибут __annotations__ хранит информацию только о параметрах и возвращаемом значении функции, если у них указана аннотация.

Варианты типизации в Python

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

   1️⃣ Использование модуля typing (актуально для кода, написанного на версиях ниже python 3.9)

   2️⃣  Встроенные возможности типизации (актуально для свежих версии python >= 3.9)

Первый способ работает как на старых версиях Python, так и на новых. Второй способ - только начиная с версии python 3.9. Но не смотря на это, мы рассмотрим оба способа аннотации, потому что высока вероятность встретить каждый из них. В этой лекции мы рассмотрим встроенные возможности типизации, а в следующей рассмотрим использование модуля typing.

 

Аннотация элементов списков и множеств в Python

Для аннотации элементов списков и множеств вы можете указывать встроенные типы данных list, set и frozenset

words: list[str] = ["hello", "world"]
numbers: list[float] = [1.1, 3.0, 4.5]
letters: set[str] = set('hello')
digits: frozenset[int] = frozenset([1, 2, 2, 1])

 

Несколько типов данных в Python

Также мы можем указать для переменной param сразу несколько типов данных для переменной или параметра. Для этого, вы можете пользоваться оператором |, который позволяет объединять типы данных во время аннотирования: 

param: int | float | bool

Также мы можем указать тип переменной или значения None.


num: int | None = None
word: str | None = None

 

Аннотация словарей

Для аннотирования словаря, мы можем применять встроенный тип данных dict:

person: dict[str, str] = { "first_name": "John", "last_name": "Doe"}

 

Аннотация элементов кортежа

По аналогии мы можем аннотировать кортеж, состоящий из двух элементов, где написать что первый элемент кортежа имеет тип строка, второй - целое число:

words: tuple[str, int] = ("hello", 300)

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

words: tuple[str, ...] = ("hello", "world", '!')

Вот так мы проаннотировали все элементы кортежа words строковым типом

В функциях используется та же самая аннотация. 

def my_func(x: int, y: int) -> tuple[int, int]:
    return x * y, y // 2

В примере выше функция my_func возвращает два значения, а значит они будут упакованы в кортеж, что и отражено в аннотации возвращаемого значения.

Функция my_func можно проаннотировать при помощи синтаксиса создания кортежей

def my_func(x: int, y: int) -> (int, int):
    return x * y, y // 2

 

Выводы

Встроенные аннотации типов очень удобны, но свежие версии Python все равно поддерживают работу с модулем typing, поэтому вы можете продолжать им пользоваться. Тем более аналогов некоторых объектов среди встроенных типов просто нет. Ярким примером такого объекта является Any.  В следующей лекции мы подробно разберем модуль typing.

Перейти к следующему шагу

Комментарии