Модуль typing и аннотация типов в Python

Модуль typing, который добавлен в Python 3.5, предоставляет способ указания типов, что позволяет программам проверки статических типов и линтерам точно предсказывать ошибки.

Иногда разработчикам бывает сложно выяснить, что именно происходит в коде из-за того, что Python должен определять тип объектов во время выполнения.

Внешние средства проверки типов, например PyCharm IDE, не дают нужных результатов. Он правильно прогнозирует ошибки всего в 50% случаев, согласно доступной статистике.

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

Код Python становится читабельным и понятным для других пользователей!

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

Требования

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

Для этого используйте команду:

pip3 install mypy

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

mypy program.py

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

python program.py

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

Подсказки типа, или типовая аннотация

Типовая аннотация в функциях

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

def print_list(a: list) -> None:
    print(a)

Этот код сообщает средству проверки типов (у нас это mypy), что у нас есть функция print_list(), которая принимает list в качестве аргумента и возвращает None.

def print_list(a: list) -> None:
    print(a)

print_list([1, 2, 3])
print_list(1)

Сначала давайте запустим это в mypy:

vijay@JournalDev:~ $ mypy printlist.py 
printlist.py:5: error: Argument 1 to "print_list" has incompatible type "int"; expected "List[Any]"
Found 1 error in 1 file (checked 1 source file)

Поскольку строка № 5 имеет аргумент int, а не list, мы получаем ошибку.

Аннотация типов в переменных

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

# Annotates 'radius' to be a float
radius: float = 1.5

# We can annotate a variable without assigning a value!
sample: int

# Annotates 'area' to return a float
def area(r: float) -> float:
    return 3.1415 * r * r


print(area(radius))

# Print all annotations of the function using
# the '__annotations__' dictionary
print('Dictionary of Annotations for area():', area.__annotations__)

Вывод mypy:

vijay@JournalDev: ~ $ mypy find_area.py && python find_area.py
Success: no issues found in 1 source file
7.068375
Dictionary of Annotations for area(): {'r': <class 'float'>, 'return': <class 'float'>}

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

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

Модуль typing предоставляет нам псевдонимы типов. Чтобы определить такой псевдоним, присвойте его типу.

from typing import List

# Vector is a list of float values
Vector = List[float]

def scale(scalar: float, vector: Vector) -> Vector:
    return [scalar * num for num in vector]

В результате получится: 

vijay@JournalDev: ~ $ mypy vector_scale.py && python vector_scale.py
Success: no issues found in 1 source file
[2.0, 4.0, 6.0]

В фрагменте выше Vector — это псевдоним, обозначающий список значений с плавающей запятой. Приведенная выше программа может вывести подсказку для псевдонима.

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

Теперь давайте рассмотрим еще один пример, который проверяет каждую пару “ключ:значение” на соответствие формату name:email.

from typing import Dict
import re

# Create an alias called 'ContactDict'
ContactDict = Dict[str, str]

def check_if_valid(contacts: ContactDict) -> bool:
    for name, email in contacts.items():
        # Check if name and email are strings
        if (not isinstance(name, str)) or (not isinstance(email, str)):
            return False
        # Check for email xxx@yyy.zzz
        if not re.match(r"[a-zA-Z0-9\._\+-]+@[a-zA-Z0-9\._-]+\.[a-zA-Z]+$", email):
            return False
    return True


print(check_if_valid({'vijay': 'vijay@sample.com'}))
print(check_if_valid({'vijay': 'vijay@sample.com', 123: 'wrong@name.com'}))

Вывод mypy будет выглядеть так:

vijay@JournalDev:~ $ mypy validcontacts.py 
validcontacts.py:19: error: Dict entry 1 has incompatible type "int": "str"; expected "str": "str"
Found 1 error in 1 file (checked 1 source file)

Поскольку параметр name в нашем втором словаре является целым числом (123), то здесь мы получаем статическую ошибку времени компиляции в mypy. Таким образом, псевдонимы — это еще один способ обеспечить точную проверку типов из mypy.

Создание пользовательских типов данных с помощью NewType()

Для создания новых пользовательских типов мы можем использовать функцию NewType().

from typing import NewType

# Create a new user type called 'StudentID' that consists of
# an integer
StudentID = NewType('StudentID', int)
sample_id = StudentID(100)

Средство проверки типов будет рассматривать новый тип, как если бы он был подклассом исходного. Это помогает находить логические ошибки.

from typing import NewType

# Create a new user type called 'StudentID'
StudentID = NewType('StudentID', int)

def get_student_name(stud_id: StudentID) -> str:
    return str(input(f'Enter username for ID #{stud_id}:\n'))

stud_a = get_student_name(StudentID(100))
print(stud_a)

# This is incorrect!!
stud_b = get_student_name(-1)
print(stud_b)

Вывод mypy будет иметь следующий вид:

vijay@JournalDev:~ $ mypy studentnames.py  
studentnames.py:13: error: Argument 1 to "get_student_name" has incompatible type "int"; expected "StudentID"
Found 1 error in 1 file (checked 1 source file)

Тип Any

Any – это особый тип, который сообщает другим инструментам проверки типов (в нашем случае mypy), что каждый тип совместим с этим ключевым словом.

Рассмотрим нашу старую функцию print_list(), которая теперь принимает аргументы любого типа.

from typing import Any

def print_list(a: Any) -> None:
    print(a)

print_list([1, 2, 3])
print_list(1)

Теперь при запуске mypy ошибок не будет.

vijay@JournalDev:~ $ mypy printlist.py && python printlist.py
Success: no issues found in 1 source file
[1, 2, 3]
1

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

def foo(bar):
    return bar

# A static type checker will treat the above
# as having the same signature as:
def foo(bar: Any) -> Any:
    return bar

То есть, тип Any можно использовать, чтобы смешивать статически и динамически типизированный код.

Заключение

В этой статье мы разобрались с модулем typing в Python, который очень полезен в контексте проверки типов. Он позволяет внешним средствам проверки типов, таким как mypy, точно сообщать обо всех ошибках.

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

Читайте также: Основы Python: полный список типов данных

Tags:

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