TypedDict, произвольная литеральная строка и многое другое

25 апреля 2022 года Python опубликовал последнюю альфа-версию — 3.11.0a7, которая представляет собой последний выпуск альфа-фазы разработки 3.11. В начале мая он перейдет на этап бета-тестирования, а окончательная официальная версия 3.11, как ожидается, будет выпущена в октябре 2022 года.

Я знаю, что адаптировать ваши приложения к последней версии Python — это большой труд, и в большинстве случаев вам это не нужно, потому что ваши приложения, по-видимому, нормально работают с более старой версией. Однако, если вы любите пробовать что-то новое, давайте начнем с Python 3.11!

Обратите внимание, что вы не хотите изменять свои текущие приложения, потому что версия 3.11 все еще находится в альфа-фазе, но это не мешает нам попробовать ее с образом докера — отдельным виртуальным контейнером, независимым от ваших приложений.

Среди новых возможностей в этой статье я хочу сосредоточиться на аннотациях типов. Python — это язык с динамической типизацией, и аннотации типов используются многими средствами проверки типов, которые могут обеспечивать анализ кода в реальном времени. Многие ошибки в коде, такие как несоответствие типов, можно выявить еще во время написания кода. Таким образом, улучшение аннотаций типов в Python 3.11 поможет нам писать код без ошибок.

Обратите внимание, что мы тестируем альфа-версию, и эти функции могут быть изменены.

Предпосылки

Установка Python 3.11 с помощью Docker

Если на вашем компьютере не установлен Docker, вы можете зайти на docker.com, чтобы узнать инструкцию по установке. Идея докеров заключается в создании воспроизводимого контейнера, который упрощает разработку приложений.

После установки докера вы можете запустить следующую команду в своем инструменте командной строки. В моем случае я использую приложение «Терминал» на своем компьютере Mac.

docker run -t -d python:3.11-rc-bullseye

Эта команда извлекает изображение с тегом python:3.11-rc-bullseye. На всякий случай вам может быть интересно, что означает этот тег. 3.11 представляет версию Python, rc означает релиз-кандидат, а bullseye — релиз Debian яблочко, это изображение основано на этом релизе Debian.

Если вы успешно установили этот образ, вы должны увидеть нечто, как указано выше, изображение в списке контейнеров/приложений. Вы можете навести курсор мыши на это изображение и щелкнуть значок CLI, который запускает приложение «Терминал».

Вы можете просто ввести python, что активирует консоль Python в Терминале, как показано ниже.

# python
Python 3.11.0a7 (main, Apr 20 2022, 17:55:51) [GCC 10.2.1 20210110] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>>

Из приглашения мы видим, что это Python 3.11.0.a7 — седьмой альфа-релиз для 3.11. Мы также можем проверить это, запустив следующий код:

>>> import sys
>>> sys.version
'3.11.0a7 (main, Apr 20 2022, 17:55:51) [GCC 10.2.1 20210110]'

Использование контейнера в коде Visual Studio (VSC)

Использование интерфейса командной строки прекрасно подходит для изучения некоторых основных функций. Однако это не самое приятное с точки зрения написания кода. Таким образом, мы можем захотеть использовать редактор кода, подобный IDE, например Visual Studio Code (VSC). Хотя существуют и другие альтернативные IDE для Python, в этом руководстве мы будем придерживаться VSC.

Предполагая, что вы установили VSC на свой компьютер, чтобы ваш VSC работал с образом докера, вам необходимо установить расширение Remote Containers. После установки вы должны увидеть вкладку на боковой панели, как показано на рисунке ниже.

Нажав на вкладку, вы увидите, что VSC заполняет доступные контейнеры, в частности, контейнер Python 3.11. Щелкните контейнер правой кнопкой мыши, и вы сможете присоединить VSC к контейнеру. При подключении появится новое окно с использованием контейнера.

Создание файла Python для оценки

Вы можете открыть папку как наш проект. Для простоты мы просто используем корневую папку по умолчанию, и вы можете найти скриншот ниже.

В этой папке для оценки мы создаем файл Python с именем test11.py. Вы можете запустить файл, вызвав python -m test11.py в своем терминале (возможно, вам придется использовать python3 -m test11.py, если у вас установлен python 3).

4 новых функции набора текста

1. Self — тип класса

Когда мы определяем методы в классе, нам часто нужно возвращать экземпляр того же класса. Во многих случаях возвращаемое значение является экземпляром объекта, с которым мы вызываем этот метод. Для начала рассмотрим следующий пример.

class Box:
    def paint_color(self, color):
        self.color = color
        return self

В приведенном выше коде не используются подсказки типов, и он может быть непонятным. Таким образом, мы можем добавить более конкретную информацию о типе. Некоторые люди могут подумать о следующем решении.

class Box:
    def paint_color(self, color: str) -> Box:
        self.color = color
        return self

Однако вы не можете заставить его работать, потому что в теле класса Box вы не можете использовать Box, так как он еще не определен! Таким образом, существующий обходной путь заключается в использовании варианта типа, который служит прокси для определяемого типа, как показано ниже:

from typing import TypeVar
TBox = TypeVar("TBox", bound="Box")
class Box:
    def paint_color(self, color: str) -> TBox:
        self.color = color
        return self

Одним из неудобств этого подхода является то, что когда вы говорите, что хотите изменить имя класса, скажем, мы хотим переименовать его в Container, нам, возможно, придется внести следующие изменения: имя класса, определение TBox и тип для возвращаемое значение в методе paint_color, как показано ниже:

from typing import TypeVar
TContainer = TypeVar("TContainer", bound="Container")
class Container:
    def paint_color(self, color: str) -> TContainer:
        self.color = color
        return self

Эти изменения могут быть нетривиальными, если в вашем проекте много подобных применений. С введением типа Self в Python 3.11 все стало проще.

from typing import Self
class Box:
    def paint_color(self, color: str) -> Self:
        self.color = color
        return self

Вы должны быть знакомы с аргументом self в методах экземпляра, который является объектом экземпляра. В качестве аналога Self представляет класс, в котором он используется. В приведенном выше примере мы используем Self, чтобы указать, что возвращаемое значение является объектом типа «Я», который интерпретируется как класс Box.

Помимо преимуществ устранения необходимости создавать варианты шрифта, есть и другие преимущества, обсуждаемые в официальном PEP 673, включая использование в подклассах.

2. Произвольная литеральная строка

Вторая особенность также относится к аннотации типа. Давайте пересмотрим статус-кво. Когда мы определяем функцию, которая принимает строковые литералы, мы можем использовать тип Literal, как показано ниже:

from typing import Literal
def paint_color(color: Literal["red", "green", "blue", "yellow"]):
    pass

Как видите, когда мы хотим, чтобы функция принимала строковый литерал, вы должны указать совместимые строковые литералы. Однако из-за ограниченных возможностей текущий анализ кода считает, что это ошибка (предупреждение), как показано ниже:

Чтобы устранить это ограничение, в Python 3.11 представлен новый общий тип LiteralString, который позволяет пользователям вводить любые строковые литералы, как показано ниже:

from typing import LiteralString


def paint_color(color: LiteralString):
    pass


paint_color("cyan")
paint_color("blue")

Тип LiteralString обеспечивает гибкость использования любых строковых литералов вместо определенных строковых литералов при использовании типа Literal. Для более конкретных случаев использования, где применимо LiteralString, таких как построение буквальных строк запроса SQL, вы можете обратиться к официальному PEP 675.

3. Вариативные дженерики

Мы можем использовать TypeVar для создания дженериков с одним типом, как мы делали ранее для Box. Когда мы выполняем числовые вычисления, такие как операции с массивами в NumPy и TensorFlow, мы используем массивы различных размеров и форм.

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

from typing import Generic, TypeVar
Dim1 = TypeVar('Dim1')
Dim2 = TypeVar('Dim2')
Dim3 = TypeVar('Dim3')
class Shape1(Generic[Dim1]):
    pass
class Shape2(Generic[Dim1, Dim2]):
    pass
class Shape3(Generic[Dim1, Dim2, Dim3]):
    pass

Как показано выше, для трех измерений нам придется определить три типа и соответствующие им классы, что не является чистым и представляет собой высокий уровень повторения, с которым мы должны быть осторожны. Python 3.11 представляет TypeVarTuple, который позволяет создавать дженерики с использованием нескольких типов. Используя эту функцию, мы можем реорганизовать наш код в предыдущем фрагменте и получить что-то вроде следующего:

from typing import Generic, TypeVarTuple
Dim = TypeVarTuple('Dim')
class Shape(Generic[*Dim]):
    pass

Поскольку это объект tuple, вы можете использовать выражение со звездочкой для распаковки содержащихся в нем объектов, и в нашем случае это переменное число типов. Приведенный выше класс Shape может иметь любую форму, что значительно повышает гибкость и устраняет необходимость создания отдельных классов для разных форм.

Для получения более подробной информации об использовании TypeVarTuple обратитесь к официальному PEP 646.

4. TypedDict — гибкие ключевые требования

В Python словари — это мощный тип данных, который сохраняет данные в виде пар ключ-значение. Ключи являются произвольными, и вы можете использовать любые применимые ключи для хранения данных. Однако иногда требуется структурированный словарь с определенными ключами и значениями определенного типа. В этом случае вы можете использовать тип TypedDict.

from typing import TypedDict
class Name(TypedDict):
    first_name: str
    last_name: str

Как показано выше, мы определяем Name, который имеет first_name и last_name в качестве необходимых ключей, а их значения должны быть строками.

Мы знаем, что у некоторых людей могут быть отчества (скажем, соответствующий ключ — middle_name), а у некоторых — нет. То есть класс Name, как TypedDict, должен допускать отсутствие middle_class. Нет прямых аннотаций, чтобы сделать ключ необязательным, и текущий обходной путь — создание суперкласса, который использует все необходимые ключи, в то время как подкласс включает необязательные ключи, как показано ниже:

from typing import TypedDict
class _Name(TypedDict):
    first_name: str
    last_name: str
class Name(_Name, total=False):
    middle_name: str

Как показано выше, у класса _Name есть first_name и last_name, а у класса Name есть middle_name. Примечательно, что нам нужно указать total как False, а это означает, что ключ middle_name можно опустить в классе Name.

Создание суперкласса для удовлетворения этой потребности бизнеса неудобно, и у нас должно быть лучшее решение — Python 3.11 вводит NotRequired в качестве квалификатора типа, чтобы указать, что ключ может быть потенциально отсутствующим для TypedDict. Использование очень простое, как показано ниже:

from typing import TypedDict, NotRequired
class Name(TypedDict):
    first_name: str
    middle_name: NotRequired[str]
    last_name: str

Как показано выше, нам больше не нужна структура суперкласса/подкласса. Вместо этого мы просто указываем, что middle_name в этом TypedDict не требуется — не так ли лаконичнее? Обратите внимание: возможно, вы заметили, что NotRequired невозможно импортировать, и я предполагаю, что это может быть проблема с изображением.

Если у вас слишком много необязательных ключей, вы можете указать те ключи, которые необходимы, используя Required, вместо того, чтобы указывать необязательные необязательные. Таким образом, альтернативное эквивалентное решение вышеуказанной проблемы показано ниже:

from typing import TypedDict, Required
class Name(TypedDict, total=False):
    first_name: Required[str]
    middle_name: str
    last_name: Required[str]

Обратите внимание, что во фрагменте кода мы указываем total как False, что делает все ключи необязательными. А пока мы помечаем эти обязательные ключи как Required, что означает, что другие ключи потенциально отсутствуют.

Для получения дополнительной информации об этой функции обратитесь к официальному PEP 655.

Выводы

В этой статье мы рассмотрели четыре новые функции в Python 3.11, касающиеся аннотаций типов. Эти новые функции позволяют писать более чистый код, используя более мощные подсказки типов.

Напоминаем, что окончательный официальный выпуск Python 3.11 — октябрь 2022 года. Вы готовы к этому?