Рубріки: Теория

Глубокое понимание аннотаций типов в Python, часть 1

Игорь Грегорченко

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

С помощью подсказок типов можно аннотировать переменные и функции типами. Python не проверяет типы во время выполнения; вместо этого инструменты статической проверки типов, такие как mypy, pyright или IDE, проверяют на соответствие типы и выдают предупреждения, когда типы используются несогласованно.

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

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

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

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

Прежде чем начать

Чтобы получить максимальную пользу от этого руководства, у вас должны быть:

  • Установленный Python ≥3.10.
  • Знания о том, как писать функции, f-строки и выполнять код Python.
  • Знать, как использовать командную строку.

Мы рекомендуем Python ≥3.10, так как в этой версии есть новые и лучшие возможности подсказки типов. Если вы используете Python ≤3.9, Python предоставляет альтернативный синтаксис подсказки типов; мы упомянем о нем в учебнике.

Что такое mypy?

mypy — это необязательная статическая проверка типов, созданная Юккой Лехтосало. Он проверяет аннотированный код в Python и выдает предупреждения, если аннотированные типы используются непоследовательно. mypy также проверяет синтаксис кода и выдает синтаксические ошибки, если встречает неправильный синтаксис.

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

Что такое сильная динамическая типизация?

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

Например, в Python вы не можете добавить целое число к строке, иначе возникнет ошибка типа:

>>> 3 + "hello"

TypeError: unsupported operand type(s) for +: 'int' and 'str'

Для сравнения, язык со слабой типизацией, такой как JavaScript, выполнит неявное преобразование типов, и выполнение операции 3 + “hello” приведет к корректному результату:

> 3 + "hello";

 "3hello"; // вывод

Динамическая типизация в Python означает, что интерпретатор проверяет типы только во время выполнения программы, и вы можете в любой момент изменить тип переменной:

>>> x = "hello"
>>> type(x)
<class 'str'> # тип переменной — строка
>>> x = 3
>>> type(x)
<class 'int'> # тип переменной изменен на целое число

Что такое статическая проверка типов?

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

int x = 4;
x = "hello"; // это вызовет ошибку типа.

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

Все является объектами Python

В Python все является объектами. Определяете ли вы строку, целое число, float, список или словарь — все они являются объектами.

Мы можем убедиться в этом, проверив метод isinstance:

>>> isinstance(str, object)
True
>>> isinstance(int, object)
True

Как видите, и строка (str), и целое число (int) подтвердили, что являются экземплярами объекта.

Поскольку все является объектом, все также имеет атрибуты и методы. Например, если вы определите целое число, вы можете увидеть все его атрибуты, используя dir:

>>> x = 8
>>> dir(x)
[...'__add__', '__mul__', '__str__', '__divmod_', ''real', ...] # отредактировано для краткости

Методы с двойным ведущим и последующим подчеркиванием показывают операции, которые поддерживает тип int. Например, метод __mul__ выполняет операцию умножения.

>>> x = 8
>>> x.__mul__(2)
16

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

Например, объекты относятся к типу Sequence, если у них есть методы __getitem__ и __len__. Примерами таких объектов являются список, строка, кортеж (вывод отредактирован для краткости):

>>> dir([2,3]) # list
[...'__getitem__', '__len__'...]
>>> dir((3,2))    # кортеж
[...'__getitem__', '__len__'...]
>>> dir('hello') # строка
[...'__get_item__', '__len__'...]

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

Исходя из этого, мы начнем аннотировать переменные в следующем разделе.

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

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

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

имя_переменной: тип = значение

Рассмотрим следующую переменную:

name = "rocket"

Мы присваиваем переменной name строковое значение "rocket".

Чтобы аннотировать переменную, нам нужно добавить двоеточие (:) после имени переменной и объявить тип str:

name: str = "rocket"

В Python подсказки типов, определенных для переменных, можно прочитать с помощью словаря __annotations__:

>>> name: str = "rocket"
>>> __annotations__
{'name': <class 'str'>}

Словарь __annotations__ покажет вам подсказки типов для всех глобальных переменных.

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

>>> name: int = "rocket"
>>>

С другой стороны, статическая проверка типов, например mypy, отметит это как ошибку:

error: Incompatible types in assignment (expression has type "str", variable has type "int")

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

  • float: плавающие значения, например, 3.10
  • int: целые числа, такие как 3, 7
  • str: строки, например, ‘hello’
  • bool: булево значение, которое может быть True или False
  • bytes: представляет байтовые значения, например, b’hello’.

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

Однако mypy с трудом определяет типы переменных, хранящих сложные структуры, такие как списки, словари или кортежи. И именно здесь подсказки типов для переменных становятся более важными.

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

Добавление подсказок типов в функции

Теперь мы добавим подсказки типов в функции. Чтобы аннотировать функцию, мы аннотируем каждый параметр и возвращаемое значение:

def function_name(param1: param1_type, param2: param2_type) -> return_type:

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

def announcement(language, version):
    return f"{language} {version} has been released"

announcement("Python", 3.10)

Функция принимает строку в качестве первого параметра и float в качестве второго параметра. Затем она возвращает строку как результат.

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

  • язык: str.
  • версия: float.

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

def announcement(language: str, version: float) -> str:

    ...

Теперь функция имеет подсказки типа, показывающие, что она принимает аргументы str и float, а возвращает str.

Когда вы вызовете функцию, вы получите:

result = announcement("Python", 4.11)
print(result) # Python 4.11 был выпущен

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

result = announcement(True, "Python")
print(result) # True Python был выпущен

Функция выполняется успешно, несмотря на то, что в качестве первого аргумента мы передали булево True, а в качестве второго — строку “Python”. Чтобы получать предупреждения об этих ошибках, нам нужно использовать статическую проверку типов, например mypy.

Статическая проверка типов с помощью mypy

Сейчас мы покажем, как проводить статическую проверку типов с помощью mypy, чтобы получать предупреждения об ошибках типа в нашем коде.

Создайте каталог type_hints и переместите его в каталог:

mkdir type_hints && cd type_hints

Создайте и активируйте виртуальную среду:

python3.10 -m venv venv
source venv/bin/activate

Установите последнюю версию mypy с помощью pip:

pip install mypy

После установки mypy создайте файл announcement.py и введите в него следующий код:

def announcement(language, version):
    return f"{language} {version} has been released"

announcement("Python", 3.10)


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

Далее запустите файл с помощью mypy:

mypy announcement.py
Success: no issues found in 1 source file

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

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

Тип Any

Как мы уже отмечали, mypy игнорирует код без подсказок типов. Это происходит потому, что он предполагает тип Any в коде без подсказок.

Ниже показано, как mypy видит функцию:

def announcement(language: Any, version: Any) -> Any:
    return f"{language} {version} has been released"

announcement("Python", 3.10)

Тип Any — это динамический тип, который совместим с любым типом. Поэтому mypy не будет жаловаться на то, являются ли типы аргументов функции bool, int, bytes и т.д.

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

Настройка mypy для проверки типов

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

mypy --strict announcement.py

announcement.py:1: error: Function is missing a type annotation
announcement.py:4: error: Call to untyped function "print_release" in typed context
Found 2 errors in 1 file (checked 1 source file)

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

mypy также предоставляет опцию --disallow-incomplete-defs. Эта опция помечает функции, у которых не аннотированы все параметры и возвращаемые значения. Эта опция очень удобна, потому что если вы забыли аннотировать возвращаемое значение или вновь добавленный параметр, mypy предупредит вас об этом.

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

def announcement(language: str, version: float):
    return f"{language} {version} has been released"

announcement("Python", 3.10)

Запустите файл с помощью mypy без каких-либо опций командной строки:

mypy announcement.py
Success: no issues found in 1 source file

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

Чтобы защитить себя от этих проблем, мы передадим опцию --disallow-incomplete-defs в mypy:

mypy --disallow-incomplete-defs announcement.py

announcement.py:1: error: Function is missing a return type annotation
Found 1 error in 1 file (checked 1 source file

Теперь mypy предупреждает нас, что мы пропустили аннотацию возвращаемого типа, поэтому давайте добавим ее:

def announcement(language: str, version: float) -> str:

    ...

Запустите файл снова с включенной опцией --disallow-incomplete-defs:

mypy --disallow-incomplete-defs announcement.py
Success: no issues found in 1 source file

На этот раз mypy работает без проблем, потому что мы закончили аннотирование определений функций.

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

ef announcement(language: str, version: float) -> str:
    return f"{language} {version} has been released"

announcement(True, "Python")  # bad arguments

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

mypy --disallow-incomplete-defs announcement.py
announcement.py:4: error: Argument 1 to "print_release" has incompatible type "bool"; expected "str"
announcement.py:4: error: Argument 2 to "print_release" has incompatible type "str"; expected "float"
Found 2 errors in 1 file (checked 1 source file)

Отлично! mypy предупреждает нас, что мы передали в функцию неправильные аргументы.

Теперь давайте избавимся от необходимости набирать mypy с опцией —disallow-incomplete-defs.

mypy позволяет сохранять опции в файле mypy.ini. При запуске mypy будет проверять этот файл и запускаться с опциями, сохраненными в файле.

Создайте файл mypy.ini в корневом каталоге проекта и введите следующий код:

[mypy]
python_version = 3.10
disallow_incomplete_defs = True

В файле mypy.ini мы сообщаем mypy, что мы используем Python 3.10 и что мы хотим запретить неполные определения функций.

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

mypy  announcement.py
Success: no issues found in 1 source file

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

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

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

Не все функции имеют оператор возврата. Когда вы создаете функцию без оператора возврата, она все равно возвращает значение None:

def announcement(language: str, version: float):
    print(f"{language} {version} has been released")


result = announcement("Python", 4.11)
print(result)  # None

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

Чтобы показать, что функция ничего не возвращает, мы можем аннотировать возвращаемое значение функции значением None:

def announcement(language: str, version: float) -> None:

    ...

Добавление подсказок о типе объединения в параметрах функции

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

Например, следующая функция принимает параметр, который может быть либо str, либо int:

def show_type(num):
    if(isinstance(num, str)):
        print("You entered a string")
    elif (isinstance(num, int)):
        print("You entered an integer")

show_type('hello') # You entered a string
show_type(3)       # You entered an integer

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

Для аннотации параметра мы будем использовать символ объединения |, который был введен в Python 3.10, для разделения типов следующим образом:

def show_type(num: str | int) -> None:
...

show_type('hello')
show_type(3)

Союз | теперь показывает, что параметр num является либо str, либо int.

Если вы используете Python ≤3.9, вам необходимо импортировать Union из модуля типизации. Параметр может быть аннотирован следующим образом:

from typing import Union

def show_type(num: Union[str, int]) -> None:
    ...

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

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

def format_name(name: str, title = None) -> str:
    if title:
        return f"Name: {title}. {name.title()}"
    else:
        return f"Name: {name.title()}"

format_name("john doe", "Mr")

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

Чтобы указать, что title является необязательным, мы воспользуемся типом Optional[str] из модуля typing:

from typing import Optional

def format_name(name: str, title: Optional[str] = None) -> str:
    ...

format_name("john doe", "Mr")

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

Конец первой части

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

Останні статті

Обучение Power BI – какие онлайн курсы аналитики выбрать

Сегодня мы поговорим о том, как выбрать лучшие курсы Power BI в Украине, особенно для…

13.01.2024

Work.ua назвал самые конкурентные вакансии в IТ за 2023 год

В 2023 году во всех крупнейших регионах конкуренция за вакансию выросла на 5–12%. Не исключением…

08.12.2023

Украинская IT-рекрутерка создала бесплатный трекер поиска работы

Unicorn Hunter/Talent Manager Лина Калиш создала бесплатный трекер поиска работы в Notion, систематизирующий все этапы…

07.12.2023

Mate academy отправит работников в 10-дневный оплачиваемый отпуск

Edtech-стартап Mate academy принял решение отправить своих работников в десятидневный отпуск – с 25 декабря…

07.12.2023

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

Служба безопасности Украины задержала в Киеве 46-летнего программиста, который за деньги устанавливал шпионские программы и…

07.12.2023

Как вырасти до сеньйора? Девелопер создал популярную подборку на Github

IT-специалист Джордан Катлер создал и выложил на Github подборку разнообразных ресурсов, которые помогут достичь уровня…

07.12.2023