Зачем нужны аннотации типов
Перейти к содержимому

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

  • автор:

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

Аннотации типов — это возможность указать типы параметров и возвращаемое значение у функции в Python. Это не является обязательным требованием языка, но может помочь программистам в дальнейшей разработке, улучшить читаемость кода и повысить его надежность.

Давайте рассмотрим простой пример функции без аннотаций типов:

def concat(first, second): return first + second 

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

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

Теперь давайте добавим аннотации типов к функции:

def concat(first: str, second: str) -> str: return first + second 

Здесь мы указали, что аргументы first и second должны быть строкового типа ( str ). Возвращаемое значение тоже будет строковым. Когда мы будем использовать эту функцию в коде, нам будет проще понять, какие типы аргументов можно передавать и какой тип возвращаемого значения ожидается.

Аннотации типов также могут быть использованы для определения типов переменных внутри функции. Например:

def double(n: int) -> int: result: int = n * 2 return result 

В этом примере мы определили тип переменной result как int , используя аннотацию типа.

Аннотации типов — это не строгая проверка типов в Python. Их использование не гарантирует, что функция будет вызвана с аргументами и возвращаемым значением указанных типов. Все таки Python остается динамически типизированным языком. В нем аннотации типов не влияют на возможность передачи аргументов различных типов или возвращения значений других типов. Тем не менее их использование упрощает чтение и понимание кода и помогает отслеживать ошибки.

Задание

Реализуйте функцию word_multiply() . Она должна принимать два параметра:

  • Строку
  • Число, которое обозначает, сколько раз нужно повторить строку
text = 'python' print(word_multiply(text, 2)) # => pythonpython print(word_multiply(text, 0)) # => 

Укажите аннотации типов при объявлении функции.

Упражнение не проходит проверку — что делать? ��

Если вы зашли в тупик, то самое время задать вопрос в «Обсуждениях». Как правильно задать вопрос:

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

В моей среде код работает, а здесь нет ��

Тесты устроены таким образом, что они проверяют решение разными способами и на разных данных. Часто решение работает с одними входными данными, но не работает с другими. Чтобы разобраться с этим моментом, изучите вкладку «Тесты» и внимательно посмотрите на вывод ошибок, в котором есть подсказки.

Мой код отличается от решения учителя ��

Это нормально ��, в программировании одну задачу можно выполнить множеством способов. Если ваш код прошел проверку, то он соответствует условиям задачи.

В редких случаях бывает, что решение подогнано под тесты, но это видно сразу.

Прочитал урок — ничего не понятно ��

Создавать обучающие материалы, понятные для всех без исключения, довольно сложно. Мы очень стараемся, но всегда есть что улучшать. Если вы встретили материал, который вам непонятен, опишите проблему в «Обсуждениях». Идеально, если вы сформулируете непонятные моменты в виде вопросов. Обычно нам нужно несколько дней для внесения правок.

Кстати, вы тоже можете участвовать в улучшении курсов: внизу есть ссылка на исходный код уроков, который можно править прямо из браузера.

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

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

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

Раньше, когда на Python писали в основном небольшие скрипты, это не было такой уж острой проблемой, потому что всю программу за разумный промежуток времени можно было охватить взглядом и понять. В последнее время язык стал значительно популярнее.

По данным исследований StackOverflow за 2020, 2019, 2018, 2017, 2016, 2015 (там же можно посмотреть результаты за 2014 и 2013) годы, Python с каждым годом растёт в популярности.

Сегодня на Python написано много сложных систем из сотен файлов и сотен тысяч строк кода. В таких обстоятельствах документирующее свойство системы типов становится очень полезным. В достаточно крупной кодовой базе при отсутствии информации о типах очень сложно угадать (а только гадать и остаётся), какие же именно объекты циркулируют по программе.

Кроме того, даже если код без информации о типах может быть и вполне понятен человеку, например, благодаря удачно выбранным именам, то для автоматики — это в любом случае абсолютно непроницаемый черный непрозрачный ящик. В такой ситуации очень сложно, не выполняя код (мы же говорим про статический анализ), понять как он будет вести себя в ран-тайме. Аннотации типов позволяют IDE, линтерам и тайп-чекерам лучше понимать код программы, что дает возможность рано отлавливать достаточно хитрые ошибки. В конечном итоге это делает написанные программы надежнее.

По этим соображениям, в Python 3.5 появился специальный синтаксис для объявления типов параметров функций и их возвращаемых значений (PEP 484). В Python 3.6 эта возможность была расширена — стало можно объявлять типы переменных вообще в любом месте программы (PEP 526). С каждой новой версией языка эта функциональность улучшается, и писать аннотации типов становится всё проще, удобнее и естественнее, а экосистема вокруг типизированного Python развивается семимильными шагами.

Нужно отметить, что тайп-аннотации — это именно возможность, а не обязанность. У программиста есть выбор — добавлять информацию о типах или нет. Таким образом Python пытается усидеть на двух стульях — остаться языком с динамической типизацией и дать возможность для статического анализа написанных программ. Привнести в хаос немного порядка, так сказать. И, по-моему, у Python это неплохо получается.

Как это работает?

Программист при написании кода расставляет информацию о типах переменных, параметров и возвращаемых значений функций. Это никак не влияет на выполнение программы. Python сам по себе никак не использует эту информацию в ран-тайме, он лишь перекладывает её в специальные атрибуты функций или переменных, делая доступной для сторонних утилит. То есть, если указано, что функция принимает строки, то это никак не помешает вызвать её с целыми числами или списками — в зависимости от тела функции, она может отработать, а может завершиться ошибкой, но сама возможность вызова с любыми типами аргументов никак не ограничивается.

Зачем же тогда писать тайп-аннотации?

  • документация для разработчиков;
  • сторонние утилиты и библиотеки могут использовать эту информацию по своему усмотрению, например, выполняя проверки типов или приводя данные к нужному типу.

Первый пункт достаточно очевидный, а про второй мы поговорим чуть позже в разделе про тайп-чекеры.

Простые типы

Вот так, например, можно тайп-аннотировать простую функцию:

def greeting(name: str = "world") -> str: return "Hello, " + name 

Типы параметров, принимаемых функцией, записываются после имени параметра через знак двоеточия, но перед значением по умолчанию, если оно присутствует. Возвращаемое значение функции записывается после знака “стрелки”.

Теперь читатель просто взглянув на сигнатуру функции может понять, что функция принимает строку и возвращает строку. Наверное, если передать в неё другой тип, то она не сможет корректно отработать.

Точно так же можно использовать для тайп-аннотаций и любые другие базовые (примитивные, не-контейнерные) типы в Python: int , float , bool , str , bytes , None и вообще практически что угодно. Чуть позже посмотрим, как типизировать контейнерные типы данных, такие как списки, кортежи, словари и множества.

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

def body_mass_index(weight: float, height: float) -> float: return weight / height ** 2 

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

def print_hello(name: str, upper: bool = False) -> None: if upper: name = name.upper() print("Hello,", name) 

Вот так можно аннотировать любые переменные в любом месте кода (Python 3.6+):

name: str = "Andrey" age: int = 25 is_sick_with_covid19: bool = False # надеюсь pi: float = 3.1415 # можно даже аннотировать переменные, не назначая им значения foo: str 

Если мы создадим свой класс, то его тоже можно использовать для аннотаций:

class Example: pass # довольно бессмысленно, но для примера пойдет example_instance: Example = Example() 

Контейнерные типы и дженерики

Перейдем к более сложным типам, таким как списки, кортежи, словари и множества. Можно аннотировать в лоб, используя сами имена классов:

primes: list person_info: tuple stock_prices: dict valid_answers: set 

Это не слишком информативно, потому что кроме самого типа контейнера было бы ещё полезно знать, какие данные он в себе содержит. Что такое person_info ? Кортеж чего?

В Python до версии 3.9 для этого придётся использовать отдельные классы из модуля typing , потому что стандартные классы не представляют такой функциональности. Делается это при помощи квадратных скобок, как будто мы извлекаем что-то по индексу:

from typing import List, Tuple, Dict, Set # тип всех элементов списка primes: List[int] # тип каждого элемента кортежа person_info: Tuple[str, int, float, float] # тип ключей, тип значений stock_prices: Dict[str, float] # тип всех элементов множества valid_answers: Set[str] 

Начиная с Python 3.9 можно использовать стандартные классы в точно таких же целях, ничего ниоткуда не импортируя:

# будет работать только начиная с Python 3.9! # тип всех элементов списка primes: list[int] # тип каждого элемента кортежа person_info: tuple[str, int, float, float] # тип ключей, тип значений stock_prices: dict[str, float] # тип всех элементов множества valid_answers: set[str] 

Согласитесь, так намного понятнее. Сразу видно, какой тип данных лежит внутри контейнера. Такие типы называются обобщёнными (generic types).

Кстати, в типизации можно яснее увидеть разницу между тем как должны использоваться списки и кортежи ( list vs. tuple ).

  • Списки содержат однородные данные — они все должны быть одного типа, иначе с таким списком будет тяжеловато работать.
  • Кортеж, напротив, может содержать разнородные данные, которые в зависимости от позиции могут иметь тот или иной тип.
  • Список нужно использовать, когда длина заранее неизвестна либо она переменна, например, список пользователей.
  • Кортеж нужно использовать, когда длина данных известна заранее и строго фиксирована, например, как в записи из таблицы в СУБД.

Получается, кортеж — это не просто неизменяемый брат-близнец списка.

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

# кортеж из строк, длина неизвестна months: Tuple[str, . ] 

Составные типы

Часто случаются ситуации, когда нужно объединить несколько типов, например, для того, чтобы указать, что функция может принимать и строки, и числа. Этого можно достичь при помощи дженерик-типа Union из модуля typing :

from typing import Union def add_or_concatenate(a: Union[str, int], b: Union[str, int]): return a + b 

Также очень часто возникает ситуация, когда возможно либо значение определенного типа, либо None . Это настолько частая ситуация, что для этого даже сделали отдельный дженерик-тип Optional :

from typing import Optional, Union # по сути это одно и то же, но первый вариант проще читается phone: Optional[str] phone: Union[str, None] 

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

from typing import Any def func(arg: Any) -> Any: return arg 

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

Вообще советую заглянуть в документацию модуля typing , там есть много интересных классов на все случаи жизни.

Проверка типов

Допустим, что тайп-аннотации написаны. Как начать получать от этого пользу?

В экосистеме Python есть несколько конкурирующих между собой тайп-чекеров, например, mypy , pyre , pytype , pyright . Самым популярным среди них является mypy , наверное, потому что одним из ключевых его разработчиков является сам Гвидо ван Россум. Давайте на mypy и остановимся.

Установим mypy в проект. Внутри виртуального окружения проекта нужно выполнить:

$ pip install mypy 

Для pipenv и poetry соответственно вот так:

$ pipenv install --dev mypy $ poetry add --dev mypy 

Давайте напишем самый тривиальный пример программы с ошибкой:

print("qwerty" + 1) 

При выполнении, очевидно, программа завершится ошибкой:

$ python example.py Traceback (most recent call last): File "example.py", line 1, in print("qwerty" + 1) TypeError: can only concatenate str (not "int") to str 

Давайте посмотрим, сможет ли тайп-чекер обнаружить эту проблему:

$ mypy example.py example.py:1: error: Unsupported operand types for + ("str" and "int") Found 1 error in 1 file (checked 1 source file) 

Отлично! Не исполняя программу, mypy смог понять, что в ней присутствует ошибка. Давайте запрячем эту же самую ошибку чуть глубже, используя функцию:

def greet(name: str) -> None: print("Hello, " + name) # правильный вызов greet("world!") # а вот тут будет ошибка greet(5) 

Проверим типы в этой программе:

$ mypy example2.py example2.py:9: error: Argument 1 to "greet" has incompatible type "int"; expected "str" Found 1 error in 1 file (checked 1 source file) 

Тайп-чекер пропустил правильный вызов функции, но обнаружил вызов функции с ошибкой.

Заключение

Тайп-аннотации — это настолько круто и удобно, что, честно говоря, я уже плохо представляю, как раньше (до Python 3.5) без этого люди вообще программировали. Для меня это самый веский аргумент в пользу Python 3 и против Python 2. Это незаменимый инструмент при разработке насколько-нибудь крупной программы.

Обязательно нужно интегрировать тайп-чекинг в свой редактор/IDE, чтобы ошибки подсвечивались ещё на этапе написания кода. Можно интегрировать тайп-чекинг в Git-хуки и CI.

На странице «Awesome Python Typing» можно найти ещё много полезных инструментов, которые пользуются тайп-аннотациями.

Если понравилась статья, то подпишитесь на уведомления о новых постах в блоге, чтобы ничего не пропустить!

Дополнительное чтение:

  • введение в аннотации типов часть 1 и часть 2 на Хабре;
  • документация к модулю typing ;
  • документация mypy ;
  • репозиторий mypy на GitHub;
  • сайт mypy ;
  • сайт pyre ;
  • репозиторий pytype на GitHub;
  • репозиторий pyright на GitHub;
  • Awesome Python Typing;
  • ликбез по типизации в языках программирования на Хабре.

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

Python известен как язык Дикого Запада, в котором дозволено всё. Стили кода (если не считать отступы) и документации, по большей части, оставлены на усмотрение разработчика. Но это может привести к некоторому беспорядку и нечитабельности кода.

Частично это связано с тем, что Python — язык с динамической типизацией. Это означает, что типы связаны со значением переменной, а не с ней самой. Таким образом, переменные могут принимать любое значение в любой момент и проверяются только перед выполнением операций над ними.

Рассмотрим следующий код. В Python это вполне приемлемо.

age = 21 print(age) # 21 age = 'Twenty One' print(age) # Twenty One

В приведенном выше коде значение age сначала является int , но позже мы меняем его на str . Каждая переменная может представлять любое значение в любой точке программы. В этом сила динамической типизации!

Попробуем сделать то же самое на языке со статической типизацией, например на Java.

int age = 21; System.out.print(age); age = "Twenty One"; System.out.print(age);

Мы получаем следующую ошибку, потому что пытаемся назначить «Twenty One» (строку) переменной age , которая была объявлена ​​как int .

Error: incompatible types: String cannot be converted to int

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

int ageNum = 21; System.out.print(ageNum); String ageStr = ageNum.toString(); System.out.print(ageStr);

Это приемлемо, но мне очень нравится гибкость Python и, в частности, динамическая типизация, благодаря которой мне не приходится объявлять больше переменных, чем необходимо. Но мне также нравится удобочитаемость статически типизированных языков, ведь так другие программисты знают, какого типа должна быть конкретная переменная! Чтобы получить лучшее из обоих миров, в Python 3.5 были представлены аннотации типов.

От редакции Pythonist. Предлагаем также почитать статью «Проверка типов данных и «утиная» типизация в Python».

Что такое аннотации типов?

Аннотации типов – это новая возможность, описанная в PEP484, которая позволяет добавлять подсказки о типах переменных. Они используются, чтобы информировать читателя кода, каким должен быть тип переменной. Это придаёт немного статический вид коду на динамически типизированном Python. Достигается это синтаксисом: после инициализации / объявления переменной.

Ниже показан пример, в котором при объявлении переменной добавляется : int , чтобы показать, что возраст должен иметь тип int .

age: int = 5 print(age) # 5

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

Зачем и как использовать аннотации типов

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

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

Рассмотрим следующий код:

def mystery_combine(a, b, times): return (a + b) * times

Мы видим, что делает эта функция, но знаем ли мы, какими должны быть a , b или times ? Посмотрите на следующий код, особенно на две строки, в которых мы вызываем mystery_combine с разными типами аргументов. Обратите внимание на вывод каждой версии, который показан в комментариях под каждым блоком.

# Исходная функция def mystery_combine(a, b, times): return (a + b) * times print(mystery_combine(2, 3, 4)) # 20 print(mystery_combine('Hello ', 'World! ', 4)) # Hello World! Hello World! Hello World! Hello World!

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

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

def mystery_combine(a: str, b: str, times: int) -> str: return (a + b) * times

К параметрам функции добавились : str , : str и : int , чтобы показать, какого типа они должны быть. Это должно сделать код более понятным для чтения и лучше раскрыть его предназначение.

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

Функцию все еще можно вызвать неправильно, но, присмотревшись, программист должен понять, как следует ее использовать. Аннотации типов и подсказки невероятно полезны для приложений Python, над которыми работают нескольких разработчиков. Это устраняет большую часть догадок при чтении кода!

Сложные типы

Однако, не всегда всё будет так просто, поэтому давайте разберемся с некоторыми более сложными случаями.

Для чего-то большего, чем примитивные типы в Python, используйте класс typing . В нем описаны типы для аннотирования любой переменной любого типа. Он поставляется с предварительно загруженными аннотациями типов, таких как Dict , Tuple , List , Set и т. д. Затем вы можете расширить подсказки по типу до вариантов использования, как в примере ниже.

from typing import List def print_names(names: List[str]) -> None: for student in names: print(student)

Это скажет читателю, что names должен быть списком строк. Словари работают аналогично.

from typing import Dict def print_name_and_grade(grades: Dict[str, float]) -> None: for student, grade in grades.items(): print(student, grade)

Подсказка типа Dict [str, float] сообщает нам, что оценки должны быть словарем, где ключи являются строками, а значения — числами с плавающей запятой.

В других сложных примерах понадобится модуль typing .

Псевдонимы типов

Если вы хотите работать с пользовательскими именами типов, вы можете использовать псевдонимы типов. Допустим, вы работаете с группой точек [x, y] в виде кортежей. Тогда можно использовать псевдоним для сопоставления типа Tuple с типом Point .

from typing import List, Tuple # Объявление аннотации типа Point с помощью целочисленного кортежа [x, y] Point = Tuple[int, int] # Создание функции, принимающей список значений Point def print_points(points: List[Point]): for point in points: print("X:", point[0], " Y:", point[1])

Несколько возвращаемых значений

Если ваша функция возвращает несколько значений в виде кортежа, просто оберните ожидаемый результат вот так: typing.Turple[, , . ]

from typing import Tuple def get_api_response() -> Tuple[int, int]: successes, errors = . # Обращение к какому-то API return successes, errors

Приведенный код возвращает кортеж количества успешных попыток и ошибок при вызове API – оба значения имеют тип int . Используя Tuple[int, int] , мы указываем читателю, что функция действительно возвращает несколько значений int .

Несколько возможных типов возвращаемых значений

Если в вашей функции есть переменная, принимающая значения различных типов, можно использовать типы typing.Optional или typing.Union .

Используйте Optional , если значение будет либо определенного типа, либо исключительно None .

from typing import Optional def try_to_print(some_num: Optional[int]): if some_num: print(some_num) else: print('Значение было None!')

Код, приведенный выше, указывает, что some_num может иметь тип int или None .

Когда значение может принимать более конкретные типы, используйте Union .

from typing import Union def print_grade(grade: Union[int, str]): if isinstance(grade, str): print(grade + ' процентов') else: print(str(grade) + '%')

Приведенный выше код указывает, что оценка может иметь тип int или str . Это полезно в нашем примере с выводом оценок, так что мы можем вывести 98% или «Девяносто восемь процентов» без каких-либо неожиданных последствий.

Больше примеров

Дополнительные примеры вы можете найти в официальной документации Python о модуле typing – там можно проверить ещё массу различных вариантов использования аннотации типов. В этой cтатье я показал лишь верхушку айсберга, но, надеюсь, вдохновил вас на написание более чистого кода.

Введение в аннотации типов Python

Автор иллюстрации — Magdalena Tomczyk

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

Сохраняя идею динамической утиной типизации в современных версиях Python (3.6+) поддерживает аннотации типов переменных, полей класса, аргументов и возвращаемых значений функций:

  • PEP 3107 — Function Annotations
  • PEP 484 — Type Hints
  • PEP 526 — Syntax for Variable Annotations
  • Пакет typing

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

Меня зовут Тихонов Андрей и я занимаюсь backend-разработкой в Lamoda.

В этой статье я хочу объяснить основы использования аннотаций типов и рассмотреть типичные примеры, реализуемые аннотациями из пакета typing .

Инструменты, поддерживающие аннотации

Аннотации типов поддерживаются многими IDE для Python, которые выделяют некорректный код или выдают подсказки в процессе набора текста.

Например, так это выглядит в Pycharm:

Так же аннотации типов обрабатываются и консольными линтерами.

Вот вывод pylint:

$ pylint example.py ************* Module example example.py:7:6: E1101: Instance of 'int' has no 'startswith' member (no-member)

А вот для того же файла что нашел mypy:

$ mypy example.py example.py:7: error: "int" has no attribute "startswith" example.py:10: error: Unsupported operand types for // ("str" and "int")

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

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

Основы

В отличие от старых версий Python, аннотации типов пишутся не в комментариях или docstring, а непосредственно в коде. С одной стороны, это ломает обратную совместимость, с другой — явно означает что это часть кода и может обрабатываться соответственно

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

Аннотации для переменных пишут через двоеточие после идентификатора. После этого может идти инициализация значения. Например,

price: int = 5 title: str

Параметры функции аннотируются так же как переменные, а возвращаемое значение указывается после стрелки -> и до завершающего двоеточия. Например,

def indent_right(s: str, width: int) -> str: return " " * (max(0, width - len(s))) + s

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

class Book: title: str author: str def __init__(self, title: str, author: str) -> None: self.title = title self.author = author b: Book = Book(title='Fahrenheit 451', author='Bradbury')

Кстати, при использовании dataclass типы полей необходимо указывать именно в классе. Подробнее про dataclass

Встроенные типы

Хоть вы и можете использовать стандартные типы в качестве аннотаций, много полезного сокрыто в модуле typing .

Optional

Если вы пометите переменную типом int и попытаетесь присвоить ей None , будет ошибка:

Incompatible types in assignment (expression has type «None», variable has type «int»)

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

from typing import Optional amount: int amount = None # Incompatible types in assignment (expression has type "None", variable has type "int") price: Optional[int] price = None

Any

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

unknown_item: Any = 1 print(unknown_item) print(unknown_item.startswith("hello")) print(unknown_item // 0)

Может возникнуть вопрос, почему не использовать object ? Однако в этом случае предполагается, что хоть передан может быть любой объект, обращаться с ним можно только как с экземпляром object .

unknown_object: object print(unknown_object) print(unknown_object.startswith("hello")) # error: "object" has no attribute "startswith" print(unknown_object // 0) # error: Unsupported operand types for // ("object" and "int")

Union

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

def hundreds(x: Union[int, float]) -> int: return (int(x) // 100) % 10 hundreds(100.0) hundreds(100) hundreds("100") # Argument 1 to "hundreds" has incompatible type "str"; expected "Union[int, float]"

Кстати, аннотация Optional[T] эквивалентна Union[T, None] , хотя такая запись и не рекомендуется.

Коллекции

Механизм аннотаций типов поддерживает механизм дженериков (Generics, подробнее во второй части статьи), которые позволяют специфицировать для контейнеров типы элементов, хранящихся в них.

Списки

Для того, чтобы указать, что переменная содержит список можно использовать тип list в качестве аннотации. Однако если хочется конкретизировать, какие элементы содержит список, он такая аннотация уже не подойдёт. Для этого есть typing.List . Аналогично тому, как мы указывали тип опциональной переменной, мы указываем тип элементов списка в квадратных скобках.

titles: List[str] = ["hello", "world"] titles.append(100500) # Argument 1 to "append" of "list" has incompatible type "int"; expected "str" titles = ["hello", 1] # List item 1 has incompatible type "int"; expected "str" items: List = ["hello", 1]

Предполагается, что список содержит неопределенное количество однотипных элементов. Но при этом нет ограничений на аннотацию элемента: можно использовать Any , Optional , List и другие. Если тип элемента не указан, предполагается, что это Any .

Кроме списка аналогичные аннотации есть для множеств: typing.Set и typing.FrozenSet .

Кортежи

Кортежи в отличие от списков часто используются для разнотипных элементов. Синтаксис похож с одним отличием: в квадратных скобках указывается тип каждого элемента кортежа по отдельности.

Если же планируется использовать кортеж аналогично списку: хранить неизвестное количество однотипных элементов, можно воспользоваться многоточием ( . ).

Аннотация Tuple без указания типов элементов работает аналогично Tuple[Any, . ]

price_container: Tuple[int] = (1,) price_container = ("hello") # Incompatible types in assignment (expression has type "str", variable has type "Tuple[int]") price_container = (1, 2) # Incompatible types in assignment (expression has type "Tuple[int, int]", variable has type "Tuple[int]") price_with_title: Tuple[int, str] = (1, "hello") prices: Tuple[int, . ] = (1, 2) prices = (1, ) prices = (1, "str") # Incompatible types in assignment (expression has type "Tuple[int, str]", variable has type "Tuple[int, . ]") something: Tuple = (1, 2, "hello")
Словари

Для словарей используется typing.Dict . Отдельно аннотируется тип ключа и тип значений:

book_authors: Dict[str, str] = book_authors["1984"] = 0 # Incompatible types in assignment (expression has type "int", target has type "str") book_authors[1984] = "Orwell" # Invalid index type "int" for "Dict[str, str]"; expected type "str" 

Аналогично используются typing.DefaultDict и typing.OrderedDict

Результат выполнения функции

Для указания типа результата функции можно использовать любую аннотацию. Но есть несколько особенных случаев.

Если функция ничего не возвращает (например, как print ), её результат всегда равен None . Для аннотации так же используем None .

Корректными вариантами завершения такой функции будут: явный возврат None , возврат без указания значения и завершение без вызова return .

def nothing(a: int) -> None: if a == 1: return elif a == 2: return None elif a == 3: return "" # No return value expected else: pass

Если же функция никогда не возвращает управление (например, как sys.exit ), следует использовать аннотацию NoReturn :

def forever() -> NoReturn: while True: pass

Если это генераторная функция, то есть её тело содержит оператор yield , для возвращаемого можно воспользоватьтся аннотацией Iterable[T] , либо Generator[YT, ST, RT] :

def generate_two() -> Iterable[int]: yield 1 yield "2" # Incompatible types in "yield" (actual type "str", expected type "int") 

Вместо заключения

Для многих ситуаций в модуле typing есть подходящие типы, однако я не буду рассматривать все, так как поведение аналогично рассмотренным.
Например, есть Iterator как generic-версия для collections.abc.Iterator , typing.SupportsInt для того, чтобы указать что объект поддерживает метод __int__ , или Callable для функций и объектов, поддерживающих метод __call__

Так же стандарт определяет формат аннотаций в виде комментариев и stub-файлы, которые содержат информацию только для статических анализаторов.

В следующей статье я бы хотел остановиться на механизме работы дженериков и обработке аннотаций в рантайме.

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

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