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

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.

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


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

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


Протоколы


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


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


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


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


Протокол описывается как обычный класс, наследующийся от 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,...]


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


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", {"title": str, "author": str})  # 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__ не сделали


Ссылки


  • What’s New In Python 3.8
  • PEP 586 — Literal Types
  • PEP 591 — Adding a final qualifier to typing
  • PEP 589 — TypedDict: Type Hints for Dictionaries with a Fixed Set of Keys
  • PEP 544 — Protocols: Structural subtyping (static duck typing)
Источник: https://habr.com/ru/post/470774/


Интересные статьи

Интересные статьи

Привет, Хабр! У нас возможен предзаказ долгожданного второго издания книги "Простой Python". Перевод первого издания вышел в 2016 году и по сей день остается в числе бестселл...
Привет, Хабр! Эта статья описывает процесс апгрейда самоходной платформы на базе МК esp8266 с micropython, до простейшего робота, оснащённого сканирующим ультразвуковым датчиком препятствий, м...
Всем привет. Видел несколько дашбордов по COVID-19, но не нашёл пока главного — прогноза времени спада эпидемии. Поэтому написал небольшой скрипт на Python. Он забирает данные из таблиц ВОЗ...
Go на данный момент является монополистом среди языков программирования, которые люди выбирают для написания операторов для Kubernetes. Тому есть такие объективные причины, как: Существ...
Тема статьи навеяна результатами наблюдений за методикой создания шаблонов различными разработчиками, чьи проекты попадали мне на поддержку. Порой разобраться в, казалось бы, такой простой сущности ка...