Введение в аннотации типов Python. Продолжение

в 8:21, , рубрики: python, python3, typing, typing annotations, Блог компании Lamoda

Введение в аннотации типов Python. Продолжение - 1
Автор иллюстрации — Magdalena Tomczyk

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

Предварительное объявление

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

class LinkedList:
    data: Any
    next: LinkedList  # NameError: name 'LinkedList' is not defined

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

class LinkedList:
    data: Any
    next: 'LinkedList'

Так же вы можете обращаться к классам из других модулей (конечно, если модуль импортирован): some_variable: 'somemodule.SomeClass'

Замечание

Вообще говоря, в качестве аннотации можно использовать любое вычислимое выражение. Однако рекомендуется их делать максимально простыми, чтобы утилиты статического анализа могли их использовать. В частности, скорее всего ими не будут поняты динамически вычислимые типы. Подробнее про ограничения тут: PEP 484 — Type Hints # Acceptable type hints

Например, следующий код будет работать и даже аннотации будут доступны в рантайме, однако mypy на него выдаст ошибку

def get_next_type(arg=None):
    if arg:
        return LinkedList
    else:
        return Any

class LinkedList:
    data: Any
    next: 'get_next_type()'  # error: invalid type comment or annotation

UPD: В Python 4.0 планируется включить отложенное вычисление аннотаций типов (PEP 563), которое позволит избавиться от этого приема со строковыми литералами. с Python 3.7 можно включить новое поведение с помощью конструкции from __future__ import annotations

Функции и вызываемые объекты

Для ситуаций, когда необходимо передать функцию или другой вызываем объект (например, в качестве callback) нужно использовать аннотацию Callable[[ArgType1, ArgType2,...], ReturnType]
Например,

def help() -> None:
    print("This is help string")

def render_hundreds(num: int) -> str:
    return str(num // 100)

def app(helper: Callable[[], None], renderer: Callable[[int], str]):
    helper()
    num = 12345
    print(renderer(num))

app(help, render_hundreds)
app(help, help)  # error: Argument 2 to "app" has incompatible type "Callable[[], None]"; expected "Callable[[int], str]"

Допустимо указать только возвращаемый тип функции без указания её параметров. В этом случае используется многоточие: Callable[..., ReturnType]. Обратите внимание, что квадратные скобки вокруг многоточия отсутствуют.

На текущий момент невозможно описать сигнатуру функции с переменным числом параметров определенного типа или указать именованные аргументы.

Generic-типы

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

Такие типы как List или Callable, которые, мы видели раньше как раз используют механизм дженериков. Но кроме стандартных типов, вы можете создать свои дженерик-типы. Для этого надо, во-первых, завести TypeVar переменную, которая будет атрибутом дженерика, и, во-вторых, непосредственно объявить generic-тип:

T = TypeVar("T")

class LinkedList(Generic[T]):
    data: T
    next: "LinkedList[T]"

    def __init__(self, data: T):
        self.data = data

head_int: LinkedList[int] = LinkedList(1)
head_int.next = LinkedList(2)
head_int.next = 2  # error: Incompatible types in assignment (expression has type "int", variable has type "LinkedList[int]")
head_int.data += 1
head_int.data.replace("0", "1")  # error: "int" has no attribute "replace"

head_str: LinkedList[str] = LinkedList("1")
head_str.data.replace("0", "1")

head_str = LinkedList[str](1)  # error: Argument 1 to "LinkedList" has incompatible type "int"; expected "str"

Как вы можете заметить, для generic-типов работает автоматический вывод типа параметра.

Если требуется, дженерик может иметь любое количеством параметров: Generic[T1, T2, T3].

Также, при определении TypeVar вы можете ограничить допустимые типы:

T2 = TypeVar("T2", int, float)

class SomethingNumeric(Generic[T2]):
    pass

x = SomethingNumeric[str]()  # error: Value of type variable "T2" of "SomethingNumeric" cannot be "str"

Cast

Иногда анализатор статический анализатор не может корректно определить тип переменной, в этом случае можно использовать функцию cast. Её единственная задача — показать анализатору, что выражение имеет определённый тип. Например:

from typing import List, cast

def find_first_str(a: List[object]) -> str:
    index = next(i for i, x in enumerate(a) if isinstance(x, str))
    return cast(str, a[index])

Также это может быть полезно для декораторов:

MyCallable = TypeVar("MyCallable", bound=Callable)

def logged(func: MyCallable) -> MyCallable:
    @wraps(func)
    def wrapper(*args, **kwargs):
        print(func.__name__, args, kwargs)
        return func(*args, **kwargs)

    return cast(MyCallable, wrapper)

@logged
def mysum(a: int, b: int) -> int:
    return a + b

mysum(a=1)  # error: Missing positional argument "b" in call to "mysum"

Работа с аннотациями во время выполнения

Хотя интерпретатор и не использует аннотации самостоятельно, они доступны для вашего кода во время работы программы. Для этого предусмотрен атрибут объектов __annotations__, содержащий словарь с указаннами аннотациями. Для функций это — аннотации параметров и возвращаемого типа, для объекта — аннотации полей, для глобального scope — переменные и их аннотации.

def render_int(num: int) -> str:
    return str(num)

print(render_int.annotations)  # {'num': <class 'int'>, 'return': <class 'str'>}

Так же доступна get_type_hints — она возвращает аннотации для переданного ей объекта, во многих ситуациях это совпадает с содержимым __annotations__, но есть отличия: он также добавляет аннотации родительских объектов (в порядке обратном __mro__), а так же разрешает предварительные объявления типов указанные как строки.

T = TypeVar("T")

class LinkedList(Generic[T]):
    data: T
    next: "LinkedList[T]"

print(LinkedList.__annotations__)
# {'data': ~T, 'next': 'LinkedList[T]'}
print(get_type_hints(LinkedList))
# {'data': ~T, 'next': __main__.LinkedList[~T]}

Для generic-типов доступна информация о самом типе и его параметрах через атрибуты __origin__ и __args__, но это не является частью стандарта и поведение уже менялось между версиями 3.6 и 3.7

Автор: Tishka17

Источник


* - обязательные к заполнению поля