Аннотации типов (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 будет подсвечивать эти значения и при наведении на эти места будет сообщать о том, какой тип данных ожидается в переменной.
Аннотации широко используются в функциях с целью указания ожидаемых типов данных для параметров функции. Используется следующий синтаксис для аннотации параметров функции:
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__
хранит данные в виде словаря.
Вот взгляните на пример, где мы выводим информацию по аннотациям функции 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 существует два подхода
1️⃣ Использование модуля typing
(актуально для кода, написанного на версиях ниже python 3.9)
2️⃣ Встроенные возможности типизации (актуально для свежих версии python >= 3.9)
Первый способ работает как на старых версиях Python, так и на новых. Второй способ - только начиная с версии python 3.9. Но не смотря на это, мы рассмотрим оба способа аннотации, потому что высока вероятность встретить каждый из них. В этой лекции мы рассмотрим встроенные возможности типизации, а в следующей рассмотрим использование модуля typing
.
Для аннотации элементов списков и множеств вы можете указывать встроенные типы данных 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])
Также мы можем указать для переменной 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
.