Типизация и аннотации типов данных в Python
easy

Типизация и аннотации типов данных в Python

Можно без неё, но с ней лучше

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

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

Как используется типизация в разных языках

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

Компилируемые языки требуют, чтобы весь код программы был сначала переведён в машинный язык с помощью компилятора. Компилятор включается при запуске кода и тщательно проверяет его, а затем оптимизирует и переводит в двоичный язык, который понятен процессору и оперативной памяти. После этого код на машинном языке загружается в оперативную память, а процессор его выполняет. Так работают языки C, C++, Rust, Go и Mojo.

Типизация и аннотации типов данных в Python

При проверке кода компилятор анализирует типы данных и решает, сколько памяти нужно будет выделить для выполнения программы. Разным типам данных соответствует свой объём памяти. Например, переменной типа int нужно 4 байта оперативной памяти, а переменной double — 8. Если тип данных не указан, а язык не поддерживает автоматическое определение типов, мы получим ошибку компиляции, потому что компьютер не знает, сколько памяти нужно выделить этой переменной.

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

Интерпретируемые языки не требуют предварительной компиляции. При запуске кода подключается интерпретатор, который работает построчно: читает каждую строку и сразу её выполняет. Так работают Python, JavaScript, Ruby и PHP. В разных языках интерпретаторы работают немного по-разному. Мы сейчас говорим о Python, поэтому рассмотрим работу его самого распространённого интерпретатора CPython.

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

Типизация и аннотации типов данных в Python

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

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

В чём отличия типизации от аннотаций типов

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

# тип данных для переменной x — целое число
# присваиваем переменной значение 1000
# :int — аннотация типа 
x: int = 1000

# тип данных для переменной y — строка
# присваиваем переменной значение 'Привет, КОД'
# :str — аннотация типа
y: str = 'Привет, КОД!'

# тип данных для переменной z — словарь
# объявляем переменную и пока не присваиваем ей значение
# :dict — аннотация типа
z: dict

Аннотации типов ни к чему не обязывают ни компьютер, ни программиста. Мы можем переприсвоить значения любой переменной и положить в неё не то, что указано, а любой тип данных. Например, указать для переменной тип int (число), а потом присвоить ей значение типа str (строку). Программа всё равно будет работать.

Какие типы данных можно указывать в Python

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

  • int — целые числа;
  • float — числа с запятой;
  • str — строки;
  • bool — булевы значения True и False;
  • list — список;
  • dict — словарь;
  • set — множество;
  • tuple — кортеж;
  • callable — объекты, которые могут быть вызваны как функции;
  • type — аннотация для объектов, представляющих классы;
  • значение None.

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

my_list: list[int] = [1, 2, 3]

Типы значений для множеств тоже указываются в квадратных скобках:

# объявляем переменную, указываем для неё ожидаемый тип
# «множество» и добавляем аннотацию, что множество
# my_set задумано как множество целых чисел
my_set: set[int] = (1, 2, 3)

Для словаря указываем в квадратных скобках две подсказки: тип ключей и тип значений.

# объявляем переменную, указываем для неё ожидаемый тип
# «словарь» и добавляем аннотацию, что ключи
# предполагаются типа «строка», а значение — типа «целое число»
my_dict: dict[str, int] = {'Номер ячейки': 42}

У кортежей нужно указать тип каждого элемента:

# объявляем переменную, указываем для неё ожидаемый тип
# «кортеж» и добавляем аннотацию для каждого типа
my_tuple: tuple[int, str, int] = (33, 'is', 33)

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

# объявляем функцию и пишем подсказки для каждого аргумента
def func(x: int, y: str, z: dict):
   z = {'КОД': 'Типизация и аннотации типов'}

Во-вторых, после аргументов объявляется тип возвращаемого значения. Если функция ничего не возвращает, это тоже указывается, через тип None:

# объявляем функцию и пишем подсказки для каждого аргумента
# объявляем, что функция ничего не возвращает
def func(x: int, y: str, z: dict) -> None:
   z = {'КОД': 'Типизация и аннотации типов'}

Дополнительные типы для аннотаций

Если встроенных типов не хватает, в Python есть дополнительные. Для их использования понадобится модуль typing. Чтобы импортировать нужные типы, их нужно указать через запятую командой:

# импортируем 4 типа для аннотаций: Union, Any, Optional и Callable
from typing import Union, Any, Optional, Callable

В typing есть четыре типа аннотаций, которые применяются чаще всего.

Union позволяет указать два допустимых типа вместо одного:

# тип данных для переменной x — целое число или строка
x: Union[int, str]
x = 10
x = 'Можно положить в переменную число или строку'

# объявляем переменную, указываем для неё ожидаемый тип
# типа «словарь» и добавляем аннотацию, что ключи и значения
# предполагаются типа «строка» или «целое число»
my_dict: dict[Union[int, str], Union[int, str]]
my_dict = {'Номер ячейки': 'Сорок два'}
my_dict = {42: 84}

Any разрешает указывать вообще любой тип:

# тип данных для переменной y — любой
y: Any = 'Привет, КОД!'
y = 333
y = [12, 'строка', 98]
Y = {'Номер сроки кода': 71}

# объявляем переменную, указываем для неё ожидаемый тип «кортеж»
# добавляем аннотацию, что элементы могут быть любого типа
my_tuple: tuple[Any, Any, Any] = (33, 'is', 33)

Optional показывает, что объект может принимать значение None. Эти две записи равнозначны:

# тип данных для переменной z — словарь
# объявляем переменную и говорим, что её значение может быть None
z: Optional[dict]
z: Union[dict, None]

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

# функция func принимает в качестве аргумента функцию another_func
# при вызове внутри функции func в another_func должны быть переданы
# два аргумента: целое число и строка, а возвращаемое значение должно быть словарём
def func(another_func: Callable[[int, str], dict]):
   another_func(11, 'строка')

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

  • вся аннотация пишется в квадратных скобках;
  • аргументы указаны квадратных в скобках через запятую;
  • возвращаемое значение — через запятую после квадратных скобок с аргументами.

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

def call_func(inner_func: Callable[..., float]) -> float:

Как написать свой тип

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

# кортеж с 3 типами значений
my_tuple: tuple[int, str, int]

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

# объявляем переменную, которая будет хранить тип сложного значения
# названия таких переменных принято писать с заглавной буквы
Custom_type = tuple[int, str, int]

Теперь Custom_type может использоваться для аннотаций:

# объявляем переменную, которая принимает аргумент типа 
# Custom_type и возвращает значение такого же типа
def new_func(x: Custom_type) -> Custom_type:
    return x

Как проверить программу на правильность типов

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

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

Типизация и аннотации типов данных в Python

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

Сначала расширение нужно установить командой в терминале pip install mypy. Это можно сделать в командной строке, если работаете в текстовых редакторах. Предварительно нужно зайти в папку проекта:

Типизация и аннотации типов данных в Python

Ещё установить модуль можно в терминале среды разработки IDE:

Типизация и аннотации типов данных в Python

После этого в терминале пишем название скрипта и ставим перед ним mypy, например mypy test_mypy.py. Программа проверит код, выделит ошибки и напишет в терминале результат проверки:

Типизация и аннотации типов данных в Python

Что дают аннотации типов

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

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

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

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

Редактор:

Инна Долога

Обложка:

Алексей Сухов

Корректор:

Ирина Михеева

Вёрстка:

Маша Климентьева

Соцсети:

Юлия Зубарева

Получите ИТ-профессию
В «Яндекс Практикуме» можно стать разработчиком, тестировщиком, аналитиком и менеджером цифровых продуктов. Первая часть обучения всегда бесплатная, чтобы попробовать и найти то, что вам по душе. Дальше — программы трудоустройства.
Получите ИТ-профессию Получите ИТ-профессию Получите ИТ-профессию Получите ИТ-профессию
Еще по теме
easy