Аннотации типов 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 может быть только целым числом и т. д. С динамически типизированными языками можно только догадываться, каким является или должно быть значение переменной.

python logo

Лаборатория Django-разработки

За 3 месяца отработай навыки Django-разработки до профессионального уровня на серьезном проекте под руководством наставника.

×

Мы можем использовать ожидаемый тип переменной при написании и вызове функций, чтобы обеспечить правильную передачу и использование параметров. Если мы передадим 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[<тип 1>, <тип 2>, ...]

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татье я показал лишь верхушку айсберга, но, надеюсь, вдохновил вас на написание более чистого кода.

Перевод статьи Using Python’s Type Annotations.