Хабрахабр

Новинки аннотаций типов в Python 3.8 (Protocol, Final, TypedDict, Literal)

8 и аннотации типов получили новые возможности: Сегодня ночью вышел Python 3.

  • Протоколы
  • Типизированные словари
  • Final-спецификатор
  • Соответствие фиксированному значению

Если вы ещё не знакомы с аннотациями типов, рекомендую обратить внимание на мои предыдущие статьи (начало, продолжение)
И пока все переживают о моржах, я хочу кратко рассказать о новинках в модуле typing

Протоколы

8 мы не могли выразить необходимые требования к объекту с помощью аннотаций типов.
PEP 544 призван решить эту проблему. В Python используется утиная типизация и от классов не требуется наследование от некоего интерфейса, как в некоторых других языках.
К сожалению, до версии 3.

Такие термины как "протокол итератора" или "протокол дескрипторов" уже привычны и используются давно.
Теперь можно описывать протоколы в виде кода и проверять их соответствие на этапе статического анализа.

6 в модуль typing уже входят несколько стандартных протоколов.
Например, SupportsInt (требующего наличие метода __int__), SupportsBytes (требует __bytes__) и некоторых других. Стоит отметить, что начиная с Python 3.

Описание протокола

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

from abc import abstractmethod
from typing import Protocol, Iterable class SupportsRoar(Protocol): @abstractmethod def roar(self) -> None: raise NotImplementedError class Lion(SupportsRoar): def roar(self) -> None: print("roar") class Tiger: def roar(self) -> None: print("roar") class Cat: def meow(self) -> None: print("meow") def roar_all(bigcats: Iterable[SupportsRoar]) -> None: for t in bigcats: t.roar() roar_all([Lion(), Tiger()]) # ok
roar_all([Cat()]) # error: List item 0 has incompatible type "Cat"; expected "SupportsRoar"

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

class BigCatProtocol(SupportsRoar, Protocol): def purr(self) -> None: print("purr")

Дженерики, self-typed, callable

Вместо указания в качестве родителей Protocol и Generic[T, S,...] можно просто указать Protocol[T, S,...] Протоколы как и обычные классы могут быть Дженериками.

PEP 484). Ещё один важный тип протоколов — self-typed (см. Например,

C = TypeVar('C', bound='Copyable')
class Copyable(Protocol): def copy(self: C) -> C: class One: def copy(self) -> 'One': ...

Кроме того, протоколы могут использоваться в тех случаях, когда синтаксиса Callable аннотации недостаточно.
Просто опишите протокол с __call__ методом нужной сигнатуры

Проверки в рантайме

Хотя протоколы и рассчитаны в первую очередь на использование статическими анализаторами, иногда бывает нужно проверить принадлежность класса нужному протоколу.
Чтобы это было возможно, примените к протоколу декоратор @runtime_checkable и isinstance/issubclass проверки начнут проверять соответствие протоколу

В частности, не поддерживаются дженерики Однако такая возможность имеет ряд ограничений на использование.

Типизированные словари

Для представления структурированных данных обычно используются классы (в частности, дата-классы) или именованные кортежи.
но иногда, например, в случае описания json-структуры бывает полезно иметь словарь с определенным ключами.
PEP 589 вводит понятие TypedDict, который ранее уже был доступен в расширениях от mypy

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

class Book(TypedDict): title: str author: str AlsoBook = TypedDict("AlsoBook", ) # same as Book book: Book = {"title": "Fareneheit 481", "author": "Bradbury"} # ok
other_book: Book = {"title": "Highway to Hell", "artist": "AC/DC"} # error: Extra key 'artist' for TypedDict "Book"
another_book: Book = {"title": "Fareneheit 481"} # error: Key 'author' missing for TypedDict "Book"

Типизированные словари поддерживают наследование:

class BookWithDesc(Book): desc: str

По умолчанию все ключи словаря обязательны, но можно это отключить передав total=False при создании класса.
Это распространяется только на ключи, описанные в текущем кассе и не затрагивает наследованные

class SimpleBook(TypedDict, total=False): title: str author: str simple_book: SimpleBook = {"title": "Fareneheit 481"} # ok

В частности: Использование TypedDict имеет ряд ограничений.

  • не поддерживаются проверки в рантайме через isinstance
  • ключи должны быть литералами или final значениями

Кроме того, с таким словарем запрещены такие "небезопасные" операции как .clear или del.
Работа по ключу, который не является литералом, так же может быть запрещена, так как в этом случае невозможно определить ожидаемый тип значения

Модификатор Final

PEP 591 вводит модификатор final (в виде декоратора и аннотации) для нескольких целей

  • Обозначение класса, от которого нельзя наследоваться:

from typing import final @final
class Childfree: ... class Baby(Childfree): # error: Cannot inherit from final class "Childfree" ...

  • Обозначение метода, который запрещено переопределять:

from typing import final class Base: @final def foo(self) -> None: ... class Derived(Base): def foo(self) -> None: # error: Cannot override final attribute "foo" (previously declared in base class "Base") ...

  • Обозначение переменной (параметра функции. поля класса), которую запрещено переприсваивать.

ID: Final[float] = 1
ID = 2 # error: Cannot assign to final name "ID" SOME_STR: Final = "Hello"
SOME_STR = "oops" # error: Cannot assign to final name "SOME_STR" letters: Final = ['a', 'b']
letters.append('c') # ok class ImmutablePoint: x: Final[int] y: Final[int] # error: Final name must be initialized with a value def __init__(self) -> None: self.x = 1 # ok ImmutablePoint().x = 2 # error: Cannot assign to final attribute "x"

При этом допустим код вида self.id: Final = 123, но только в __init__ методе

Literal

Literal-тип, определенный в PEP 586 используется когда нужно проверить на конкретным значениям буквально (literally)

Например, Literal[42] означает, что ожидается в качестве значения ожидается только 42.
Важно, что проверяется не только равенство значения, но и его тип (например, нельзя будет использовать False, если ожидается 0).

def give_me_five(x: Literal[5]) -> None: pass give_me_five(5) # ok
give_me_five(5.0) # error: Argument 1 to "give_me_five" has incompatible type "float"; expected "Literal[5]"
give_me_five(42) # error: Argument 1 to "give_me_five" has incompatible type "Literal[42]"; expected "Literal[5]"

В скобках при этом можно передать несколько значений, что эквивалентно использованию Union (типы значений при этом могут не совпадать).

В качестве значения нельзя использоваться выражения (например, Literal[1+2]) или значения мутабельных типов.

В качестве одного из полезных примеров использование Literal — функция open(), которая ожидает конкретные значения mode.

Обработка типов в рантайме

Если вы хотите во время работы программы обрабатывать различную информацию о типах (как я),
теперь доступны функции get_origin и get_args.

Так, для типа вида X[Y, Z,...] в качестве origin будет возвращён тип X, а в качестве аргументов — (Y, Z, ...)
Стоит отметить, что если X является алиасом для встроенного типа или типа из модуля collections, то он будет заменен на оригинал.

assert get_origin(Dict[str, int]) is dict
assert get_args(Dict[int, str]) == (int, str) assert get_origin(Union[int, str]) is Union
assert get_args(Union[int, str]) == (int, str)

К сожалению, функцию для __parameters__ не сделали

Ссылки

Теги
Показать больше

Похожие статьи

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

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»
Закрыть