Сегодня ночью вышел 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)
Автор: Tishka17