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


  1. Sly_tom_cat
    15.10.2019 10:33
    -1

    И где же былая простота и ясность Python кода?

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


    1. trapwalker
      15.10.2019 11:21

      Мне кажется это не так критично из-за необязательности тайпхинтинга. По-прежнему в учебных целях можно показывать новичкам простой лаконичный прозрачный код, а те, кого уже не испугать расширенным синтаксисом, могут насладиться большей надёжностью и уверенностью, что где-то не закралась ошибочка.
      Новичкам труднее читать чужой код? Да, но, мне кажется, это не большая цена за спокойствие.


    1. JTG
      15.10.2019 16:32

      Да уж, с «There should be one — and preferably only one — obvious way to do it» тоже дела в последнее время не очень.


      1. Tishka17 Автор
        15.10.2019 17:15

        Зато норм с


        Explicit is better than implicit.


  1. Barnaby
    15.10.2019 11:01

    Python++


    1. swelf
      15.10.2019 11:04

      Так вот что чувствовали люди пишущие на C, когда увидели C++?


  1. trapwalker
    15.10.2019 11:25
    +1

    Интересно, когда в популярных IDE начнут появляться режимы сокрытия типизации в коде?
    Вжух! — и код снова чист и прозрачен, но при этом мы продолжаем получать ворнинги в случае подозрительных махинаций с типами.


    1. Cykooz
      15.10.2019 11:45
      +1

      Вроде как давно уже поддерживается описание тайп-хинтов в отдельном от кода файле с расширением .pyi. Это даже применимо для кода на Python2.
      Правда поддерживать актуальность этих файлов несколько сложнее, т.к. надо не забывать вносить в них правки при изменении кода.


      1. trapwalker
        15.10.2019 11:56

        Да это вообще антипаттерн какой-то. Мы для кого жизнь упростить хотим? Для роботов или человеков? Пусть вкалывают!
        Делов-то — держать привязанное к коду синтаксическое дерево и прятать лишние лексемы...


    1. MechanicZelenyy
      15.10.2019 19:00

      Такое есть в IDEA+Kotlin. Kotlin умеет автоматически выводить типы, и они явно в коде не прописываются, а IDEA показывает подсказки по типам.


  1. MooNDeaR
    15.10.2019 11:36

    Literal какая-то невероятно странная хрень. Это типа попытка замутить арифметику в условном Compile Time, как в плюсах? Иначе мне не очень понятно, чем Literal лучше константы внутри метода?


    1. 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]: ...


      1. MooNDeaR
        15.10.2019 11:56

        Ок, понял, спасибо.


  1. sobolevn
    15.10.2019 12:06

    Самая главная ссылка про типизацию в питоне: github.com/typeddjango/awesome-python-typing


  1. YuriM1983
    15.10.2019 14:24

    Final

    Так, глядишь, и настоящих констант дождёмся.


    1. LinearLeopard
      15.10.2019 17:17

      Так, глядишь, и настоящих констант дождёмся.

      Ну джавы в python уже почти дождались.


      1. sentyaev
        15.10.2019 23:07

        Любой популярный ЯП рано или поздно превращается в Java.


        1. ixamilion
          16.10.2019 12:57

          Просветите неофита, это хорошо или плохо?


          1. sentyaev
            16.10.2019 16:11

            Это зависит от того, нравится ли вам Java или нет.


      1. YuriM1983
        16.10.2019 15:38

        Что вы имеете ввиду?


      1. Gordio
        17.10.2019 17:01

        А я бы не отказался от (я не про синтаксис):


        @throw(ValueError)
        def foo(arg:  int) -> str:
            pass


        1. Tishka17 Автор
          17.10.2019 17:04

          Можете попробовать глянуть dry-python/returns. Но лично мне оно не зашло в том виде, в котором предлагается


  1. 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]
    


    1. Tishka17 Автор
      16.10.2019 08:18

      Почему же ни слова? Я в начале статьи упомянул моржей :)


    1. prefrontalCortex
      16.10.2019 14:30

      Фичи Lisp'ов продолжают диффундировать в Python, прикольно.


  1. unabl4
    16.10.2019 11:00

    Может оно всё и полезно, но из простого и понятного языка, который декларировал ясность и понятность мы получаем какой-то малопонятный комбайн. Очень жаль


    1. DollaR84
      16.10.2019 13:12

      Я такого же мнения. Как по мне, так последняя норм версия 3.5. А потом пошло все в такие дебри… Когда мне нужны все эти штуки с аннотациями и прочим — я беру C++. А делать из python плюсы — ну как-то не очень, я считаю.


      1. Tishka17 Автор
        16.10.2019 13:24

        Подскажите, а как вы раньше пользователям вашего модуля объясняли какие данные надо передать в метод или что он возвращает?


        1. DollaR84
          16.10.2019 13:41
          +1

          PEP 257 никто не отменял. И хоть это только рекомендации, так ведь аннотации — тоже рекомендации. Так зачем из простого и понятного кода делать Франкенштейна.