ru:https://highload.today/blogs/structural-pattern-matching-chto-python-3-10-nam-gotovit-chast-ii/ ua:https://highload.today/uk/blogs/structural-pattern-matching-chto-python-3-10-nam-gotovit-chast-ii/
logo
Теория      17/03/2021

Structural pattern matching: что Python 3.10 нам готовит (часть II)

Павел Дмитриев BLOG

full-stack iOS developer компании Postindustria

В 6-ю альфа-версию Python 3.10, вышедшую в марте, включено одно из самых больших нововведений в истории языка: новая синтаксическая конструкция structural pattern matching (SPM).

Это продолжение рассказа о SPM, начатого этим текстом.

Словари и встроенные типы данных

Отдел маркетинга решил что строчка “Web scale” будет отлично смотреться на сайте нашего KVS, и попросил добавить в него HTTP API. Продакт, ненадолго отвлекшись от экспериментов с новой кофеваркой, постановил: “Отправлять и принимать строки — это какой-то прошлый век”. Согласовав вопрос с CTO, он потребовал сделать альтернативный API, принимающий на вход JSON.

Сперва — традиционно добавляем метод для работы с новым форматом команд.

def api_command(self, command: Dict):
    print(f"< {dumps(command)}")
    match command:

Как вы уже понимаете, Python умеет матчить не только простые типы данных и списки, но и словари. Начинаем добавлять команды. Сохранение и получение значений удалось скинуть на джуна, строго-настрого наказав ему не забыть про тесты; нам же достался более сложный фрагмент бизнес-логики — удаление.

case {"action": "delete", "id": int(id)} if not self._protected:
        print(f"> delete {id}")

При сопоставлении словарей проверяются только те ключи, которые указаны в case, остальные игнорируются. Как и для других значений, можно потребовать строгого соответствия одному из нескольких вариантов, как мы делаем с параметром action,  либо же “захватить” значение ключа в локальную переменную, как сделано для id. Тут показана еще одна продвинутая возможность match: мы можем проверять шаблоны на соответствие базовым типам Python; в данном случае ветка будет работать только если id без ошибок приведется к целому числу. Да, пока мы были в отгуле, сервер успели переписать для использования целых чисел в роли ключей.

Конечно же, мы не забыли добавить проверку на включение защиты, но во время демонстрации текущей сборки потенциальным инвесторам CTO забыл об этом и не смог удалить значение. Вернувшись, он потребовал “срочно добавить power users необходимые возможности”. Выдернув из медитации продакта, вы совместно решили, что это означает поддержку удаления с параметром force.

К счастью, с SPM сделать это не сложно:

case {"action": "delete", "id": int(id), "force": True}:
    print(f"> force delete {id}")

Смотрим, как это работает:

server.string_command("protection off")
# > Protection state is False

server.api_command({"action": "delete", "id": "oldkey"})
# > Invalid command

server.api_command({"action": "delete", "id": 1})
# > delete 1

server.string_command("protection on")
# > force delete 1

server.api_command({"action": "delete", "id": 1, "force": True})
# > Protection state is True

Как видите, все действует так, как мы ожидаем. Команда с нечисловым id не распознается, а дополнительный ключ включает именно “силовое” удаление. Не забудьте, что “force”-версия — более частный случай и должна идти раньше “обычного” удаления.

Онлайн-курс "Архітектура високих навантажень" від robot_dreams.
Досвід та інсайти від інженера, який 12 років створює програмне забезпечення для Google.
Програма курсу і реєстрація

Как быть, если нам нужно “словить” какой-то параметр, не укладывающийся в рамки базовых типов данных? Вот один из вариантов решения:

case {"kill_nodes": args} if self._is_int_list(args):
    print(f"> killing {args!r}")

Метод, используемый в защитном условии выглядит как-то так:

@staticmethod
def _is_int_list(param):
        return isinstance(param, list) and all(isinstance(item, int) for item in param)

Дописав этот код, мы поскорее пушаем его и закрываем тикет, пока руководству не пришло в голову сделать какой-то флаг для защиты от удаления с force.

Лирическое отступление: на самом деле, наличие защитного блока в сочетании с матчингом простых типов позволяет довольно сильно “абюзить” новый оператор, создавая код вроде такого:

match "bar str":
    case str(s) if s.startswith("foo"):
        print(f"pass1 {s}")
    case str(s) if s.startswith("bar"):
        print(f"pass2 {s}")

Особенно заманчиво это начинает выглядеть, например, с регулярными выражениями. Но увлекаться этим не стоит. Несмотря на правильную работу, читается подобный “инвертированный” код не особо хорошо.

Классы

Пока вы обдумывали, на кого бы свалить написание Flask-кода для сетевого слоя API, позвонил продакт и, как бы невзначай поигрывая новым умным браслетом с оксиметром, сказал что надо сделать интеграцию с библиотекой Gata, используемой для популярной облачной платформы Baboon Wide Services. Поэтому наш сервер должен уметь обрабатывать команды в Gata-вском формате. Радует в этом лишь то, что команды Gata — это простые dataclass, а SPM умеет с этим работать.

Начнем с интеграции самой базовой команды библиотеки, ее класс выглядит так:

@dataclass
class APICommand:
    action: str
    node: int
    comment: str
    force: bool = False

    def __str__(self):
        return f"{self.action}({self.node}, force={self.force})"

Знакомая уже заглушка нового метода:

def thirdparty_command(self, command: ServerCommands):
    print(f"< {command!s}")

    match command:
        case _:
            print("> Invalid command")

По традиции, можно начать с команд удаления:

Курс UX/UI дизайнер сайтів і застосунків з Alice K.
Курс від практикуючої UI/UX дизайнерки, після якого ви знатимете все про UI/UX дизайн .
Реєстрація на курс
case APICommand(("delete" | "rm"), node, force=True):
    print(f"> force delete {node}")
case APICommand(("delete" | "rm"), node) if not self._protected:
    print(f"> delete {node}")

Синтаксис выглядит достаточно очевидно, датакласс “матчится”, если совпадают его поля. При этом, как и раньше, можно использовать | и захватывать переменные. Поля, которые опущены, игнорируются.

В первом варианте case мы принудительно указали имя поля force, если бы этого не было сделано, с True сравнивалось бы поле comment, поскольку оно идет в APICommand третьим. В качестве альтернативной меры можно пропускать “ненужное” поле с помощью _,тогда строка шаблона выглядела бы так: case APICommand(("delete" | "rm"), node, _, True).

Но на самом деле комментарии нам совсем не нужны, и было бы здорово вообще игнорировать это поле при сопоставлении с шаблоном. Это не сложно сделать, указав список полей, которые участвуют в сопоставлении с помощью нового атрибута (разработчики Gata пообещали добавить его в свой класс).

__match_args__ = ["action", "node", "force"]

Обратите внимание, что этот атрибут задает не только, какие именно поля участвуют в сопоставлении, но и их порядок.

Теперь добавлять новые паттерны стало проще.

case APICommand("erase", node, True):
    print(f"> erasing {node}")

Последнее значение True как раз соответствует полю force.

Разумеется, один match-блок может обрабатывать разные датаклассы. Нам это пригодится, потому что в Gata нашлись дополнительные классы команд: административные и “суперпользовательские”.  Последние просто оборачивают обычные команды, повышая их привилегии:

@dataclass
class AdminCommand:
    action: str
    password: str


@dataclass
class RootCommand:
    command: APICommand

Добавляем поддержку пары команд:

case AdminCommand("shutdown", self._password):
    print("> shutting down")
case RootCommand(APICommand(("delete" | "rm"), node)):
    print(f"> root delete {node}")

Как видите, оператор match прекрасно справляется с разными классами и даже умеет сопоставлять вложенные датаклассы.

Онлайн-курс "Computer Vision" від robot_dreams.
Застосовуйте Machine Learning / Deep Learning та вчіть нейронні мережі розпізнавати об’єкти на відео. Отримайте необхідні компетенції Computer Vision Engineer.
Дізнатись більше про курс

Посмотрим, как это работает:

server.thirdparty_command(APICommand("delete", 1, "Do it?"))
# > Invalid command

server.thirdparty_command(APICommand("delete", 1, "Do it!", force=True))
# > force delete 1

server.thirdparty_command(APICommand("erase", 12, "Not needed", force=True))
# > erasing 12

server.thirdparty_command(AdminCommand("shutdown", "test123"))
# > shutting down

server.thirdparty_command(RootCommand(APICommand("delete", 1, "sudo!")))
# > root delete 1

На этом моменте CTO вернулся с совещания с инвесторами и сказал, что финансирования не будет и проект свернут.

Заключение

Надеюсь, я сумел вас убедить в том что structural pattern matching — это одно из самых больших нововведений в истории Python. Инструмент получился настолько мощным, что были даже призывы не допустить его добавления, аргументированные тем, что он сильно усложняет язык, снижая “фирменную” простоту. Впрочем, консенсус большинства (включая Гвидо ван Россума) был прост: у тех, для кого эта возможность слишком сложна, остается прекрасная возможность — ее не использовать. Стоит отметить, что обсуждение на этот раз вышло короче, чем при добавлении :=, — видимо, желание получить в свои руки новую игрушку все же победило.

Несколько векторов потенциального расширения SPM видны уже сейчас, например, было бы здорово иметь какой-то оператор вроде continue, заставляющий интерпретатор продолжить сопоставление, даже если он уже нашел подходящую ветку. Еще можно было бы сделать возможность “кастомного матчинга” классов, добавив в них служебный метод-предикат, возвращающий булевый флаг. Впрочем, требовать этого от первого релиза действительно было бы “слишком жирно”.

Теперь нам остается ждать повсеместного внедрения новой версии Python, чтоб писать более эффектный код. А скрасить ожидание поможет чтение обсуждения одного из следующих “больших изменений”: сокращенного синтаксиса объявления анонимных функций.

If you have found a spelling error, please, notify us by selecting that text and pressing Ctrl+Enter.

Кіноклуб "Забагато драми" від Skvot.
10 лекцій та 10 практикумів, щоб зрозуміти мистецтво кіномови.Сформуй власний смак та бібліотеку фільмів і навчись писати рецензії.
Програма кіноклубу

Этот материал – не редакционный, это – личное мнение его автора. Редакция может не разделять это мнение.

Топ-5 самых популярных блогеров марта

PHP Developer в ScrumLaunch
Всего просмотровВсего просмотров
2434
#1
Всего просмотровВсего просмотров
2434
Founder at Shallwe, Python Software Engineer (Django/React)
Всего просмотровВсего просмотров
113
#2
Всего просмотровВсего просмотров
113
Career Consultant в GoIT
Всего просмотровВсего просмотров
95
#3
Всего просмотровВсего просмотров
95
CEO & Founder в Trustee
Всего просмотровВсего просмотров
94
#4
Всего просмотровВсего просмотров
94
Рейтинг блогеров

Ваша жалоба отправлена модератору

Сообщить об опечатке

Текст, который будет отправлен нашим редакторам: