Модуль типизации Python – эффективное использование проверяющих типы

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

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

Даже внешние проверяющие типы, такие как PyCharm IDE, не всегда демонстрируют лучшие результаты; в среднем правильно предсказывают ошибки всего лишь примерно в 50% случаев, согласно этому ответу на StackOverflow.

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

Это делает код на Python более читаемым и надежным для других читателей!

ПРИМЕЧАНИЕ: Это не выполняет фактическую проверку типов во время компиляции. Если фактический возвращаемый объект не был того же типа, что подсказан, не будет никакой ошибки компиляции. Поэтому мы используем внешние проверяющие типов, такие как mypy, чтобы выявить любые ошибки типов.


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

Мы уже рассмотрели основы проверки типов в Python. Вы можете сначала прочитать эту статью.

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

pip3 install mypy

Вы можете запустить mypy для любого файла Python, чтобы проверить, соответствуют ли типы. Это похоже на ‘компиляцию’ кода 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 мы также можем аннотировать типы переменных, указывая тип. Но это не обязательно, если вы хотите, чтобы тип переменной изменялся перед возвращением функцией.

# Аннотирует 'radius' как float
radius: float = 1.5

# Мы можем аннотировать переменную, не присваивая ей значение!
sample: int

# Аннотирует 'area' как float
def area(r: float) -> float:
    return 3.1415 * r * r


print(area(radius))

# Вывести все аннотации функции с помощью
# словаря '__annotations__'
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

# Вектор - это список значений типа float
Vector = List[float]

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

a = scale(scalar=2.0, vector=[1.0, 2.0, 3.0])
print(a)

Вывод

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 – это псевдоним, который означает список значений с плавающей запятой. Мы можем использовать подсказку типа для псевдонима, что и делает вышеуказанная программа.

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

Давайте рассмотрим еще один пример, который проверяет каждую пару ключ:значение в словаре и проверяет, соответствует ли она формату имя:электронная_почта.

from typing import Dict
import re

# Создание псевдонима 'ContactDict'
ContactDict = Dict[str, str]

def check_if_valid(contacts: ContactDict) -> bool:
    for name, email in contacts.items():
        # Проверка, являются ли имя и электронная почта строками
        if (not isinstance(name, str)) or (not isinstance(email, str)):
            return False
        # Проверка электронной почты на соответствие формату [email protected]
        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': '[email protected]'}))
print(check_if_valid({'vijay': '[email protected]', 123: '[email protected]'}))

Вывод от 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)

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


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

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

from typing import NewType

# Создайте новый пользовательский тип под названием 'StudentID', который состоит из
# целого числа
StudentID = NewType('StudentID', int)
sample_id = StudentID(100)

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

from typing import NewType

# Создайте новый пользовательский тип под названием '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)

# Это неверно!!
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

Это специальный тип, информирующий статический анализатор типов (в моем случае 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

# Статический типовый проверчик будет обрабатывать вышеуказанное
# как имеющее ту же сигнатуру, что и:
def foo(bar: Any) -> Any:
    return bar

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


Вывод

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

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


Ссылки


Source:
https://www.digitalocean.com/community/tutorials/python-typing-module