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

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

  • автор:

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

Частично это связано с тем, что 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, часть 2

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

Професійний курс від laba: Управління командою в бізнесі.
Створюйте ефективну робочу атмосферу.

Начало этой статьи ищите вот здесь, а далее наше продолжение.

Добавление подсказок типов к спискам

Списки Python аннотируются на основе типов элементов, которые они имеют или ожидают иметь. Начиная с Python ≥3.9, для аннотирования списка используется тип списка, за которым следует []. [] содержит тип элемента.

Например, список строк может быть аннотирован следующим образом:

names: list[str] = ["john", "stanley", "zoe"].

Если вы используете Python ≤3.8, вам необходимо импортировать List из модуля typing .

Ефективний курс від mate.academy: Frontend.
Розблокуйте світ веб-розробки.

from typing import List names: List[str] = ["john", "stanley", "zoe"]

В определениях функций документация Python рекомендует использовать тип list для аннотации возвращаемых типов:

def print_names(names: str) -> list[int]: .

Однако для параметров функций документация рекомендует использовать эти абстрактные типы коллекций:

  • Iterable.
  • Sequence.

Тип Iterable следует использовать, когда функция принимает итерабельную переменную и выполняет над ней итерацию.

Итерабельность — это свойство объекта, который может возвращать по одному элементу за раз. Примерами могут служить списки, кортежи и строки, а также все, что реализует метод __iter__ .

Вы можете аннотировать Iterable следующим образом в Python ≥3.9:

Інтенсивний курс від laba: Product management.
Від ідеї до успішного продукту.

from collections.abc import Iterable def double_elements(items: Iterable[int]) -> list[int]: return [item * 2 for item in items]. print(double_elements([2, 4, 6])) # список print(double_elements((2, 4))) # кортеж

В функции мы определяем параметр items и присваиваем ему подсказку типа Iterable, за которой следует [ int ], что указывает на то, что Iterable содержит элементы int .

Подсказка типа Iterable принимает все, у чего реализован метод __iter__ . У списков и кортежей этот метод реализован, поэтому вы можете вызвать функцию double_elements со списком или кортежем, и функция выполнит итерацию по ним.

Чтобы использовать Iterable в Python ≤3.8, вы должны импортировать его из модуля typing .

from typing import Iterable .

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

def double_elements(items: list[int]) -> list[int]: .

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

Когда следует использовать тип Sequence

Последовательность — это коллекция элементов, которая позволяет получить доступ к элементу или вычислить его длину.

Подсказка типа Sequence может принимать список, строку или кортеж. Это связано с тем, что они имеют специальные методы: __getitem__ и __len__. Когда вы обращаетесь к элементу последовательности, скажем, items[index] , используется метод __getitem__ . При получении длины последовательности len(items) используется метод __len__ .

В следующем примере мы используем тип Sequence[int] для получения последовательности, состоящей из целочисленных элементов:

from collections.abc import Sequence def get_last_element(data: Sequence[int]) -> int: return data[-1] first_item = get_last_element((3, 4, 5)) # 5 second_item = get_last_element([3, 8] # 8

В этой функции мы принимаем последовательность и получаем доступ к последнему элементу из нее с помощью data[-1] . Для доступа к последнему элементу используется метод __getitem__ последовательности.

Професійний курс від skvot: Системний геймдизайн.
Розробка ігор знати як.

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

Для Python ≤3.8 необходимо импортировать Sequence из модуля typing :

from typing import Sequence .

Теперь мы знаем, как добавлять подсказки типов к спискам. В следующем разделе мы добавим подсказки типов в словари.

Добавление подсказок типов в словари

Чтобы добавить подсказки типов к словарям, вы используете тип dict , за которым следует [тип_ключа, тип_значения] :

Например, следующий словарь имеет ключ и значение в виде строки:

Вы можете аннотировать его следующим образом:

person: dict[str, str] =

Тип dict указывает, что ключи словаря person имеют тип str , а значения — тип str . Если вы используете Python ≤3.8, вам необходимо импортировать Dict из модуля typing .

from typing import Dict person: Dict[str, str] =

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

def make_student(name: str) -> dict[str, int]: .

Для параметров функций рекомендуется использовать эти абстрактные базовые классы:

  • Mapping
  • MutableMapping
  • Когда использовать класс Mapping

В параметрах функций, когда вы используете подсказки типа dict , вы ограничиваете аргументы, которые может принимать функция, только dict, defaultDictor OrderedDict . Однако существует множество подтипов словарей, таких как UserDict и ChainMap , которые можно использовать аналогичным образом. Вы можете получить доступ к элементу и выполнить итерацию или вычислить его длину, как и в случае со словарем.

Это происходит потому, что они реализуют:

  • __getitem__: для доступа к элементу.
  • __iter__: для итерации.
  • __len__: вычисление длины.

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

  • dict
  • UserDict
  • defaultdict
  • OrderedDict
  • ChainMap

Еще одним преимуществом типа Mapping является то, что он указывает, что вы только читаете словарь, а не изменяете его.

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

from collections.abc import Mapping def get_full_name(student: Mapping[str, str]) -> str: return f' ' john = < "first_name": "John", "last_name": "Doe", >get_full_name(john)

В функции мы добавляем подсказку типа Mapping[str, str] , которая указывает, что структура данных student имеет ключи типа str и значения типа str . В качестве аргумента мы передаем dict , но UserDict, defaultdict, OrderedDict или ChainMap будут приняты без проблем.

Если вы используете Python ≤3.8, импортируйте Mapping из модуля typing :

from typing import Mapping

Теперь мы знаем, что когда мы хотим прочитать словарь, нам нужно использовать подсказку типа Mapping. Но когда вы хотите изменить словарь, больше подходит MutableMapping .

Использование класса MutableMapping

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

Класс MutableMapping принимает любой экземпляр, который реализует следующие специальные методы:

  • __getitem__
  • __setitem__
  • __delitem__
  • __iter__
  • __len__

Методы __delitem__ и __setitem__ используются для мутации, и это методы, которые отделяют тип Mapping от типа MutableMapping .

В следующем примере функция принимает словарь и мутирует его:

from collections.abc import MutableMapping def update_first_name(student: MutableMapping[str, str], first_name: str) -> None: student["first_name"] = first_name john = < "first_name": "John", "last_name": "Doe", >update_first_name(john, "james")

В параметре student функции мы добавляем подсказку типа MutableMapping[str, str] , которая указывает, что параметр student имеет ключи типа str и значения типа str , и он будет мутирован.

В теле функции мы устанавливаем значение свойства first_name в значение второго аргумента first_name . Для изменения значения ключа словаря используется метод __setitem__ .

Если вы работаете на Pyth ≤3.8, импортируйте MutableMapping из модуля typing .

from typing import MutableMapping .

Использование класса TypedDict

До сих пор мы рассматривали, как аннотировать словари с помощью dict , Mapping и MutableMapping , но большинство словарей имеют только один тип: str .

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

Вот пример словаря, ключи которого имеют разные типы:

student =

Как вы можете видеть, значения словаря варьируются от str, int и list . Если бы в нем были только значения str , мы бы аннотировали его как dict[str, str] .

Чтобы аннотировать словарь, мы будем использовать TypedDict , который был представлен в Python 3.8. Он позволяет нам аннотировать типы значений для каждого свойства с помощью синтаксиса, подобного классу:

from typing import TypedDict class StudentDict(TypedDict): first_name: str last_name: str age: int hobbies: list[str]

Мы определяем класс StudentDict , который наследуется от TypedDict . Внутри класса мы определяем каждое поле и его ожидаемый тип.

Определив TypedDict , вы можете использовать его для аннотирования словарной переменной следующим образом:

from typing import TypedDict class StudentDict(TypedDict): . student1: StudentDict =

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

def get_full_name(student: StudentDict) -> str: return f' '

Здесь мы определили функцию get_full_name , которая принимает в качестве параметра словарь типа StudentDict . Если аргумент словаря не соответствует StudentDict, mypy выдаст предупреждение.

Добавление подсказок типов к кортежам

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

Ниже приведен пример аннотации кортежа с двумя элементами:

student: tuple[str, int] = ("John Doe", 18)

Кортеж имеет два элемента типа str и int , поэтому мы указываем оба типа в типе кортежа. Если бы кортеж состоял из пяти элементов, нам пришлось бы объявить тип для каждого из них.

Тип кортежа можно использовать как подсказку типа для параметра или возвращаемого значения:

def student_info(student: tuple[str, int]) -> None: .

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

letters: tuple[str, . ] = (‘a’, ‘h’, ‘j’, ‘n’, ‘m’, ‘n’, ‘z’)

Чтобы аннотировать именованный кортеж, необходимо определить класс, который наследуется от NamedTuple . Поля класса определяют элементы и их типы:

from typing import NamedTuple class StudentTuple(NamedTuple): name: str age: int john = StudentTuple("John Doe", 33)

Здесь мы создаем StudentTuple , который является именованным кортежем. Затем мы создаем его экземпляр, передавая ему ожидаемые элементы.

Если у вас есть функция, которая принимает именованный кортеж в качестве параметра, вы можете аннотировать параметр именованным кортежем:

def student_info(student: StudentTuple) -> None: name, age = student print(f"Name: \nAge: ") student_info(john)

Создание и использование протоколов

Бывают случаи, когда вам не важен аргумент, который принимает функция. Вас волнует только то, есть ли у нее нужный вам метод.

Чтобы реализовать такое поведение, вы используете протокол. Протокол — это класс, который наследуется от класса Protocol в модуле типизации. В классе протокола вы определяете один или несколько методов, которые статическая проверка типов должна искать везде, где используется тип протокола.

Любой объект, реализующий методы класса протокола, будет принят. Протокол можно представить как интерфейс, используемый в таких языках программирования, как Java или TypeScript.

Python предоставляет предопределенные протоколы, хорошим примером является тип Sequence. Ему не важно, что это за объект, его интересует только наличие методов __getitem__ и __len__ . Если они определены, он их принимает.

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

def calc_age(current_year: int, data) -> int: return current_year - data.get_birthyear()

Функция принимает два параметра: current_year , целое число и data . В теле функции мы находим разницу между current_year и значением, возвращенным из метода get_birthyear() .

Приведем пример класса, реализующего метод get_birthyear :

class Person: def __init__(self, name, birthyear): self.name = name self.birthyear = birthyear def get_birthyear(self) -> int: return self.birthyear # create an instance john = Person("john doe", 1996)

Это один из примеров такого класса, но могут быть и другие классы, такие как Dog или Cat , реализующие метод get_birthyear . Аннотирование всех возможных типов было бы громоздким (поскольку нас интересует только метод get_birthyear() ).

Чтобы реализовать это поведение, давайте создадим наш протокол:

from typing import Protocol class HasBirthYear(Protocol): def get_birthyear(self) -> int: .

Класс HasBirthYear наследуется от Protocol , который является частью модуля typing. Чтобы протокол знал о методе get_birthyear , мы переопределим метод точно так же, как это сделано в примере класса Person, который мы рассматривали ранее. Единственным исключением будет тело функции, где мы должны заменить тело на многоточие ( . ).

Определив протокол, мы можем использовать его в функции calc_age , чтобы добавить подсказку типа к параметру данных ( data ):

rom typing import Protocol class HasBirthYear(Protocol): def get_birthyear(self) -> int: . def calc_age(current_year: int, data: HasBirthYear) -> int: return current_year - data.get_birthyear()

Теперь параметр data был аннотирован протоколом HasBirthYear. Теперь функция может принимать любой объект, если у него есть метод get_birthyear .

Вот полная реализация кода с использованием протокола:

from typing import Protocol class HasBirthYear(Protocol): def get_birthyear(self) -> int: . class Person: def __init__(self, name, birthyear): self.name = name self.birthyear = birthyear def get_birthyear(self) -> int: return self.birthyear def calc_age(current_year: int, data: HasBirthYear) -> int: return current_year - data.get_birthyear() john = Person("john doe", 1996) print(calc_age(2021, john))

Запуск кода в mypy не вызовет никаких проблем.

Аннотирование функций с помощью перегрузки

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

def add_number(value, num): if isinstance(value, int): return value + num elif isinstance(value, list): return [i + num for i in value] print(add_number(3, 4)) # 7 print(add_number([1, 2, 5], 4)) # [5, 6, 9]

Когда вы вызываете функцию с целым числом в качестве первого аргумента, она возвращает целое число. При вызове функции со списком в качестве первого аргумента она возвращает список, каждый элемент которого дополнен значением второго аргумента.

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

def add_number(value: int | list, num: int) -> int | list: .

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

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

Аналогично, передача списка в качестве первого аргумента функции никак не может привести к получению int . Она всегда будет возвращать список.

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

Для этого мы будем использовать декоратор перегрузки из модуля типизации. Определим две перегрузки перед реализацией функции add_number :

from typing import overload @overload def add_number(value: int, num: int) -> int: . @overload def add_number(value: list, num: int) -> list: . def add_number(value, num): if isinstance(value, int): return value + num elif isinstance(value, list): return [i + num for i in value] print(add_number(3, 4)) print(add_number([1, 2, 5], 4)

Перед основной функцией add_number мы определяем две перегрузки. Параметры перегрузок аннотированы соответствующими типами и типами возвращаемых значений. Их тела функций содержат многоточие ( . ).

Первая перегрузка показывает, что если в качестве первого аргумента передать int , то функция вернет int .

@overload def add_number(value: int, num: int) -> int: .

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

@overload def add_number(value: list, num: int) -> list: .

Наконец, основная реализация add_number не имеет никаких подсказок типа.

Как вы теперь видите, перегрузки аннотируют поведение функции гораздо лучше, чем использование союзов.

Аннотирование констант с помощью Final

На момент написания статьи в Python не было встроенного способа определения констант. Начиная с Python 3.10, вы можете использовать тип Final из модуля типизации. Это означает, что mypy будет выдавать предупреждения при попытках изменить значение переменной.

from typing import Final MIN: Final = 10 MIN = MIN + 3

Запуск кода с помощью mypy выдаст предупреждение:

final.py:5: error: Cannot assign to final name "MIN" Found 1 error in 1 file (checked 1 source file)

Это происходит потому, что мы пытаемся изменить значение переменной MIN на MIN = MIN + 3.

Обратите внимание, что без mypy или любой статической проверки файлов, Python не будет этого делать, и код будет выполняться без проблем:

>>> from typing import Final >>> MIN: Final = 10 >>> MIN = MIN + 3 >>> MIN >>> 13

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

Работа с проверкой типов в сторонних пакетах

В то время как вы можете добавлять аннотации в свой код, сторонние модули, которые вы используете, могут не иметь подсказок типов. В результате mypy будет предупреждать вас.

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

import third_party # type ignore

У вас также есть возможность добавлять подсказки типов с помощью заглушек. Чтобы узнать, как использовать заглушки, смотрите раздел Файлы-заглушки в документации по mypy .

Заключение

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

Как использовать аннотации типов в файлах JavaScript

Как использовать аннотации типов в файлах JavaScript главное изображение

TypeScript (TS) позволяет использовать аннотации типов в коде JavaScript. TS даже может проверять код при сборке, благодаря чему вы увидите ошибки до того, как они попадут в продакшен. Вы избавитесь от undefined is not a function навсегда.

  • Синтаксис TypeScript
  • Документируем JavaScript
    • Синтаксис JSDoc
    • Почему JSDoc
    • Как установить TypeScript
    • Как включить проверку типов JSDoc
    • Настраиваем TypeScript
    • Аннотации параметров функций
    • Документирование кода
    • Опциональные типы
    • Документируем опции
    • Переменные
    • Параметры функций
    • Импортируем типы
    • Определяем типы во внешних файлах
    • Типы объектов
    • Объединение типов
    • Функциональные компоненты
    • Компоненты на классах

    TypeScript по умолчанию требует некоторых изменений при настройке окружения. Вам придётся переименовать файлы JavaScript в .ts, .tsx, а также использовать компиляторы tsc или Babel.

    Синтаксис TypeScript

    Часто людям не нравится работать с TypeScript из-за необходимости использовать новый для них синтаксис. Если вам знакома эта ситуация, статья как раз для вас.

    Синтаксис TypeScript позволяет использовать аннотации типов инлайн. Но сначала поговорим об альтернативах.

    Документируем JavaScript

    Синтаксис JSDoc

    TypeScript можно документировать с помощью JSDoc . Это стандартный синтаксис для документирования JavaScript. JSDoc используется для создания документации, но TypeScript понимает аннотации типов, созданные с помощью этого инструмента.

     text - The text to repeat * @param count - Number of times */ function repeat(text, count)

    Это значит, что у вас есть возможности использовать преимущество TypeScript, в частности, проверку типов, без необходимости конвертировать весь код.

    Почему JSDoc

    Применение JSDoc — полезная практика, даже если вы не используете TypeScript. Фактически это стандарт документирования JavaScript, и его поддерживают разные инструменты и редакторы.

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

    Установка TypeScript

    Как установить TypeScript

    Чтобы установить в проект TypeScript, используйте такую команду:

    Как включить проверку типов JSDoc

    Теперь нужно настроить TypeScript, чтобы он проверял код в файлах JavaScript. По умолчанию он проверяет только файлы с расширением .ts . Настройки TypeScript надо указывать в файле tsconfig.json . Обратите внимание на опцию noEmit . Мы используем её, так как планируем применять TypeScript только для проверки типов.

    Настраиваем TypeScript

    В начале файлов .js , в которых вам нужна проверка типов, добавьте комментарий:

    Запустите проверку типов. Это можно сделать с помощью команды:

    Рекомендуется использовать проверку типов также в инструментах непрерывной интеграции (CI).

    Дальше поговорим о документировании кода с помощью JSDoc.

    Базовые аннотации

    Аннотации параметров функций

    Для аннотации параметров функций используйте @param . Его нужно указать в комментариях JSDoc, которые начинаются с двух идущих подряд астериксов.

     текст * @param количество */ function repeat(text, count)

    Документирование кода

    JSDoc — инструмент для документирования. Кроме добавления аннотаций типов, вы можете документировать функции.

     text - Текст * @param count - Количество повторений */ function repeat(text, count)

    Потренируемся в документировании.

    Документирование параметров

    Опциональные типы

    Чтобы показать опциональность типа, добавьте после него знак равенства. В примере ниже number= — то же самое, что и number | null | undefined . Такой синтаксис можно использовать только в типах JSDoc.

     text * @param count */ function repeat(text, count = 1) < // . >

    Документируем опции

    Вы можете документировать свойства параметров, например, options.count или options.separator . Эту возможность можно использовать для документирования props в функциональных компонентах React.

     repeat('hello', < count: 2, separator: '-' >) 

    Утверждения типов (Type Assertions)

    Переменные

    Используйте @type , когда пишете инлайн определение для аргументов функций. Это обычно избыточно для констант, так как TypeScript чётко работает с типами. Подход полезен при работе с изменяемыми данными, например, с переменными.

    Параметры функций

    @type можно использовать для определения типов аргументов функций инлайн. Это особенно удобно при работе с анонимными функциями.

    Далее поговорим о выносе определений типов в отдельные файлы.

    Импорт определений типов

    Импортируем типы

    Сложные и переиспользуемые типы лучше определять во внешних файлах. Они имеют расширение .d.ts . Обратите внимание, это должны быть именно файлы TypeScript. Импортировать определения из файлов JavaScript невозможно.

    Типы можно импортировать с помощью представленного ниже синтаксиса. Определения должны определяться во внешних файлах с расширением .d.ts , как сказано выше.

     User */ /** * @param author */ function cite(author) < // . >

    Определяем типы во внешних файлах

    Ниже представлен синтаксис определения типов во внешних файлах TypeScript. Ещё раз обратите внимание, импортировать определения типов из файлов JavaScript невозможно.

    Теперь разберёмся, можно ли определять типы в JavaScript-файлах.

    Определение типов в файлах JavaScript

    Типы объектов

    Для определения типов объектов используйте @typedef . Предпочтительно делать это во внешних файлах с расширением .d.ts . Но вы можете использовать представленный ниже синтаксис и в файлах JavaScript.

     Props * @property title - The title of the page * @property updatedAt - Last updated time */ /** * A component. * * @param props */ const ArticleLink = props => < console.log(props.title) console.log(props.updatedAt) // . >

    Объединение типов

    Используйте объединение типов ( | ) для определения двух или более возможных вариантов. Для простоты используйте @typedef .

     NumberOrString */ 

    Как насчёт React?

    Определение типов в React

    Функциональные компоненты

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

     props * @param props.title * @param props.url * @param props.image */ const ArticleLink = props => < // . >

    Подробности о функциональных компонентах можно узнать в курсе по React, который входит в профессию «Фронтенд JavaScript».

    Компоненты на классах

    Используйте @extends для определения типов props и state . Также для решения этой задачи можно использовать @typedef инлайн или с импортом.

    > */ class MyComponent extends React.Component < // . >

    Расширенные возможности

    Синтаксис JSDoc не такой выразительный, как TypeScript, но эти инструменты всё-таки похожи. Ниже перечислены некоторые дополнительные возможности TS, доступные в JSDoc.

    • Темплейты — @templates .
    • Возврат значений — @returns .
    • Условия типов — @returns .
    • Функциональные типы — @callback .
    • Перечисления — @enum .

    Ещё больше возможностей найдёте в официальной документации .

    Резюме

    Ниже представлены перечисленные в статье способы работы с аннотациями типов.

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

    Документирование функций

     n - What to square */ function square(n) < // . 

    Импорт определений типов (позволяет определять типы во внешних файлах)

     User */ 

    Опциональные типы

     user */ 

    Анонимные функции

    Документирование свойств параметров объектов

     options * @param options.count * @param options.sep */ function repeat(options) < // . options.count, options.sep 

    Адаптированный перевод статьи Type annotations in JavaScript files by Rico Sta. Cruz. Мнение администрации Хекслета может не совпадать с мнением автора оригинальной публикации.

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

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