Сегодня ночью вышел 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__
не сделали
Ссылки
Комментарии (29)
trapwalker
15.10.2019 11:25+1Интересно, когда в популярных IDE начнут появляться режимы сокрытия типизации в коде?
Вжух! — и код снова чист и прозрачен, но при этом мы продолжаем получать ворнинги в случае подозрительных махинаций с типами.Cykooz
15.10.2019 11:45+1Вроде как давно уже поддерживается описание тайп-хинтов в отдельном от кода файле с расширением .pyi. Это даже применимо для кода на Python2.
Правда поддерживать актуальность этих файлов несколько сложнее, т.к. надо не забывать вносить в них правки при изменении кода.trapwalker
15.10.2019 11:56Да это вообще антипаттерн какой-то. Мы для кого жизнь упростить хотим? Для роботов или человеков? Пусть вкалывают!
Делов-то — держать привязанное к коду синтаксическое дерево и прятать лишние лексемы...
MechanicZelenyy
15.10.2019 19:00Такое есть в IDEA+Kotlin. Kotlin умеет автоматически выводить типы, и они явно в коде не прописываются, а IDEA показывает подсказки по типам.
MooNDeaR
15.10.2019 11:36Literal какая-то невероятно странная хрень. Это типа попытка замутить арифметику в условном Compile Time, как в плюсах? Иначе мне не очень понятно, чем Literal лучше константы внутри метода?
Tishka17 Автор
15.10.2019 11:44Нет, ни в коем случае это не арифметика. Это скорее "енумы" + дополнительная возможность параметризовать шаблонные функции.
Например, взяв
Literal
+overload
мы можем сделать функцию, которая в завимости от значения параметра возвращает разный тип: функцияopen
возвращает класс работающий с байтами если режим содержит букву 'b' или со строками, в противном случае (пример из PEP):
# Note: this is a simplification of the true type signature. _PathType = Union[str, bytes, int] @overload def open(path: _PathType, mode: Literal["r", "w", "a", "x", "r+", "w+", "a+", "x+"], ) -> IO[Text]: ... @overload def open(path: _PathType, mode: Literal["rb", "wb", "ab", "xb", "r+b", "w+b", "a+b", "x+b"], ) -> IO[bytes]: ... # Fallback overload for when the user isn't using literal types @overload def open(path: _PathType, mode: str) -> IO[Any]: ...
sobolevn
15.10.2019 12:06Самая главная ссылка про типизацию в питоне: github.com/typeddjango/awesome-python-typing
YuriM1983
15.10.2019 14:24Final
Так, глядишь, и настоящих констант дождёмся.LinearLeopard
15.10.2019 17:17Так, глядишь, и настоящих констант дождёмся.
Ну джавы в python уже почти дождались.Gordio
17.10.2019 17:01А я бы не отказался от (я не про синтаксис):
@throw(ValueError) def foo(arg: int) -> str: pass
Tishka17 Автор
17.10.2019 17:04Можете попробовать глянуть dry-python/returns. Но лично мне оно не зашло в том виде, в котором предлагается
artemisia_borealis
16.10.2019 01:58+1А про самое новое новшество что-то ни слова, про Assignment Expressions, PEP 572 . Об этом, конечно, давно известно, но всё-таки это — первый релиз.
# Handle a matched regex if (match := pattern.search(data)) is not None: # Do something with match # Reuse a value that's expensive to compute [y := f(x), y**2, y**3] # Share a subexpression between a comprehension filter clause and its output filt_data = [y for x in data if (y := f(x)) is not None]
unabl4
16.10.2019 11:00Может оно всё и полезно, но из простого и понятного языка, который декларировал ясность и понятность мы получаем какой-то малопонятный комбайн. Очень жаль
DollaR84
16.10.2019 13:12Я такого же мнения. Как по мне, так последняя норм версия 3.5. А потом пошло все в такие дебри… Когда мне нужны все эти штуки с аннотациями и прочим — я беру C++. А делать из python плюсы — ну как-то не очень, я считаю.
Tishka17 Автор
16.10.2019 13:24Подскажите, а как вы раньше пользователям вашего модуля объясняли какие данные надо передать в метод или что он возвращает?
DollaR84
16.10.2019 13:41+1PEP 257 никто не отменял. И хоть это только рекомендации, так ведь аннотации — тоже рекомендации. Так зачем из простого и понятного кода делать Франкенштейна.
Sly_tom_cat
И где же былая простота и ясность Python кода?
Не, ну понятно — вещи то в принципе полезные, но читаемость кода, которой питон славился, уходит в небытие. А жаль.
trapwalker
Мне кажется это не так критично из-за необязательности тайпхинтинга. По-прежнему в учебных целях можно показывать новичкам простой лаконичный прозрачный код, а те, кого уже не испугать расширенным синтаксисом, могут насладиться большей надёжностью и уверенностью, что где-то не закралась ошибочка.
Новичкам труднее читать чужой код? Да, но, мне кажется, это не большая цена за спокойствие.
JTG
Да уж, с «There should be one — and preferably only one — obvious way to do it» тоже дела в последнее время не очень.
Tishka17 Автор
Зато норм с