Pull to refresh

Структурированное сопоставление с шаблонами в Python 3.10

Reading time12 min
Views6.2K

Версия Python 3.10, работа над которой началась 25 мая 2020 года, запланирована к выпуску  4 октября 2021 года и будет содержать ряд интересных нововведений. Одним из многообещающих нововведений будет структурированное сопоставление с шаблонами (structured pattern matching). Для этого будет введена специальная инструкция сопоставления с шаблонами match. Функциональность сопоставления с шаблонами несомненно вызовет интерес, в особенности у программистов ФП, где она играет важную роль. Остальные новинки новой версии языка описаны здесь.

Python, при всей его мощи и популярности, долгое время не имел формы управления потоком, которая имеется в других языках, — способа брать значение и элегантно его сопоставлять с одним из множества возможных условий. В языках C и C++ для этого служит конструкция switch/case; в Rust и F# эта конструкция называется «сопоставлением с шаблонами».

Принятые для этого в Python традиционные способы не являются элегантными. Один из них состоит в написании цепочки выражений if/elif/else. Другой - в хранении значений, которые сопоставляются как ключи, в словаре, а затем в использовании значений по ключу для выполнения действия — например, хранить функцию в качестве значения и использовать ключ или какую-либо другую переменную на входе. Во многих случаях эти приемы работают хорошо, но громоздки в конструировании и техническом сопровождении.

После безуспешности многих предложений по добавлению в Python синтаксиса, похожего на switch/case, для Python 3.10 было принято недавнее предложение создателя языка Python Гвидо ван Россума и ряда других авторов: структурированное сопоставление с шаблонами. Структурированное сопоставление с шаблонами не только позволяет выполнять простые сопоставления в стиле switch/case, но и поддерживает более широкий спектр вариантов использования.

Содержание статьи

  • Введение в структурированное сопоставление с шаблонами на языке Python

  • Сопоставление с переменными с помощью структурированного сопоставления

  • Сопоставление с несколькими элементами с помощью структурированного сопоставления

  • Шаблоны в структурированном сопоставлении

  • Сопоставление с объектами с помощью структурированного сопоставления

  • Эффективное применение структурированного сопоставления

  • Классификация вариантов применения

Введение в структурированное сопоставление с шаблонами на языке Python

Структурированное сопоставление с шаблонами вводит в Python инструкцию match/case и шаблонный синтаксис. Инструкция match/case следует той же базовой схеме, что и switch/case. Она берет объект, проверяет его на соответствие одному или нескольким шаблонам сопоставления и выполняет действие, если находит совпадение.

match command:
    case "quit":
        quit()
    case "reset":
        reset()
    case unknown_command:
        print (f"Неизвестная команда '{unknown_command}')

За каждой инструкцией case следует шаблон, с которым выполняется сопоставление. В приведенном выше примере мы используем простые строковые литералы в качестве целей сопоставления но возможны и более сложные сопоставления.

Python выполняет сопоставления, просматривая список случаев сверху вниз. При первом совпадении Python выполняет инструкции в соответствующем блоке case, затем переходит к концу блока match и продолжает работу с остальной частью программы. Между инструкциями case нет «проскакивания», но можно разработать свою логику для манипулирования несколькими возможными случаями в одном блоке case (подробнее об этом позже).

Также можно захватывать все совпадение целиком или ее часть и использовать его повторно. В инструкции case для неизвестной команды, unknown_command, в приведенном выше примере значение «захватывается» в переменной unknown_command, поэтому мы можем использовать его повторно.

Сопоставление с переменными с помощью структурированного сопоставления

Здесь стоит упомянуть важное замечание. Если вы перечисляете имена переменных в инструкции case, то это не означает, что сопоставление будет делаться с содержимым именованной переменной. Переменные в case используются для захвата значения, с которым выполняется сопоставление.

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

from enum import Enum

class Command(Enum):
    QUIT = 0
    RESET = 1

match command:
    case Command.QUIT:
        quit()
    case Command.RESET:
        reset()

Не нужно обязательно использовать перечисление; подойдет любое имя свойства через точку. Тем не менее, перечисления, как правило, являются для этого наиболее знакомым и идиоматичным способом в Python.

Сопоставление с несколькими элементами с помощью структурированного сопоставления

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

Вот несколько более сложный пример. Здесь пользователь вводит команду, за которой обычно следует имя файла.

command = input()

match command.split():
    case ["выйти"]:
        quit()
    case ["загрузить", filename]:
        load_from(filename)
    case ["сохранить", filename]:
        save_to(filename)
    case _:
        print (f"Непонятная команда '{command}'")

Давайте проинспектируем эти инструкции case по порядку:

case ["выйти"]: тестирует то, с чем мы выполняем сопоставление, представляет собой список с одной единственной позицией "выйти", полученной в результате разбивки введенного значения.

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

case _: представляет собой сопоставление с подстановочным символом. Оно совпадает, если к этому моменту не совпал ни один другой шаблон. Обратите внимание, что переменная _ на самом деле ни к чему не привязывается; имя _ используется в качестве сигнала для команды match, что рассматривается подстановочный (универсальный) случай (именно по этой причине мы обращаемся переменной command в теле блока case; ничего не было захвачено.)

Шаблоны структурированного сопоставления

Шаблоны могут быть представлены простыми значениями, либо могут содержать более сложную логику сопоставления. Вот несколько примеров:

case "a": сопоставляет с одним единственным значением "a".

case ["a","b"]: сопоставляет с коллекцией ["a","b"].

case ["a", value1]: сопоставляет с коллекцией из двух значений, и поместить второе значение в переменную захвата value1.

case ["a", *values]: сопоставляет с коллекцией, по меньшей мере, из одного значения. Другие значения, если они есть, помещаются в значения. Обратите внимание, что в коллекцию может включаться только одна позиция со звездочкой (как это принято делать с аргументами со звездочкой в функции Python).

case ("a"|"b"|"c"): оператор ИЛИ (|) может использоваться, чтобы иметь возможность манипулировать несколькими случаями case в одном единственном блоке case. Здесь мы сопоставляем с "a", либо с "b", либо с "c".

case ("a"|"b"|"c") as letter: делает то же что и выше, за исключением того, что мы размещаем совпавшую позицию в переменную letter.

case ["a", value] if <выражение>: сопоставляет с захваченным элементом, только если выражение является истинным. В выражении могут использоваться переменные захвата. Например, если бы мы использовали значение if в коллекции valid_values, то вариант case был бы валидным, только если захваченное значение фактически находилось в указанной коллекции.

case ["z", _]: будет совпадать любая коллекция позиций, которая начинается с "z".

Сопоставление с объектами с помощью структурированного сопоставления

Наиболее продвинутой особенностью системы структурированного сопоставления с шаблонами в Python является способность выполнять сопоставление с объектами, обладающими специфическими свойствами. Возьмем приложение, в котором мы работаем с объектом под названием media_object и мы хотим его конвертировать в файл .jpg и вернуть из функции.

match media_object:
    case Image(type="jpg"):
        # Вернуть как есть
        return media_object
    case Image(type="png") | Image(type="gif"):
        return render_as(media_object, "jpg")
    case Video():
        raise ValueError("Не получается извлечь кадры из видео")
    case other_type:
        raise Exception(f"Обработать медиатип {media_object} не получается")

В каждом приведенном выше случае case мы ищем специфический вид объекта, иногда со специфическими атрибутами. Первая инструкция case совпадает с объектом Image, атрибут которого установлен равным "jpg". Вторая инструкция case совпадает в случае, если тип равен "png" либо "gif". Третья инструкция case совпадает с любым объектом, имеющим тип Video, не зависимо от его атрибутов. И заключительная инструкция case является нашим всеохватывающим случаем, если все другие не сработают.

Захватывать также можно с помощью объектных паросочетаний:

match media_object:
    case Image(type=media_type):
        print (f"Снимок имеет тип {media_type}")

Эффективное применение структурированного сопоставления

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

Следует иметь в виду еще одну важную вещь, и это порядок паросочетаний. То, какие паросочетания вы будет тестировать в первую очередь, повлияет на эффективность и точность вашего сопоставления в целом. Большинство людей, строивших длинные цепочки if/elif/else, это поймут, но сопоставление с шаблонами требует от вас еще более тщательного обдумывания порядка из-за потенциальной сложности. Сначала следует размещать самые конкретные паросочетания, а самые общие - последними.

Наконец, если ваша задача может быть решена с помощью простой цепочки if/elif/else или поиска по словарю — используйте их! Сопоставление с шаблонами является мощным, но не универсальным средством. Используйте его только тогда, когда оно имеет наибольший смысл для решения задачи.

Классификация вариантов применения

На данный момент можно выделить следующие сценарии применения функциональности структурированного сопоставления с шаблонами в будущем:

  • Обыкновенная инструкция switch/case

# ПРИМЕЧАНИЕ: 
# для выполнения приведенных ниже примеров кода 
# требуется Python версии 3.10.

# Обыкновенная инструкция switch/case
def match_errno(errno):
    match errno:
        case 0:
            pass
        case 1:
            pass
        case 42:
            print("42!")
        case _:    # совпадает с любым числом
            print("универсальное совпадение")
  • Сопоставление с использованием списка литералов

# Сопоставление с использованием списка литералов
def command_split(command):
    match command.split():
        case ["make"]:
            print("make по умолчанию")
        case ["make", cmd]:
            print(f"найдена команда make: {cmd}")
        case ["restart"]:
            print("выполняется перезапуск")
        case ["rm", *files]:
            print(f"удаляются файлы: {files}")
        case _:
            print("совпадений не найдено")
  • Сопоставление с использованием списка литералов и оператора ИЛИ (|)

# Сопоставление с использованием списка литералов и оператора ИЛИ (|)
def match_alternatives(command):
    match command.split():
        case ["север"] | ["переместиться на", "север"]:
            print("выполняется перемещение на север")
        case ["взять", obj] | ["поднять", "с пола", obj] | ["понять", obj, "с пола"]:
            print(f"выполняется поднятие: {obj}")
  • Групповое сопоставление с использованием кортежа альтернатив и оператора as

# Групповое сопоставление с использованием кортежа альтернатив и оператора as
def match_capture_subpattern(command):
    match command.split():
        case ["переместиться на", ("север" | "юг" | "восток" | "запад") as direction]:
            print(f"выполняется перемещение на {direction}")
  • Сопоставление с использованием списка литералов и ограничителя if

# Сопоставление с использованием списка литералов и ограничителя if
def match_guard(command, exits):
    match command.split():
        case ["переместиться на", direction] if direction in exits:
            print(f"выполняется перемещение на {direction}")
        case ["переместиться на", _]:
            print(f"переместиться в указанном направлении не получается")
  • Сопоставление с классом

# Сопоставление с классом
from dataclasses import dataclass

@dataclass
class Click:
    position: tuple[int, int]
    button: str

@dataclass
class KeyPress:
    key_name: str

@dataclass
class Quit:
    pass

def match_by_class(event):
    match event:
        case Click(position=(x,y), button="left"):
            print(f"обрабатывается левое нажатие в {x,y}")
        case Click(position=(x,y)):
            print(f"обрабатывается другое нажатие в {x,y}")
        case KeyPress("Q"|"q") | Quit():
            print("выход из программы")
        case KeyPress(key_name="up arrow"):
            print("перемещение вверх")
        case KeyPress():
            pass # игнорировать другие нажатия клавиш
        case other_event:
            raise ValueError(f'нераспознанное событие: {other_event}')

Сопоставление со словарем

# Сопоставление со словарем
def match_json_event(event):
    match event:
        case {"transport": "http"}:
            print("небезопасное insecure событие проигнорировано")
        case {"verb": "GET", "page": "articles", "pageno": n}:
            print(f"разрешено взять статью на странице {n}...")
        case {"verb": "POST", "page": "signup"}:
            print("обрабатывается вход в систему")

Код главной программы:

def main():
    # x, y = 1, 2
    command_split("make")
    command_split("make clean")
    command_split("restart")
    command_split("rm a b c")
    command_split("doesnt match")
    match_errno(42)
    match_alternatives("go north")
    match_alternatives("pick up sword")
    match_capture_subpattern("go north")
    match_capture_subpattern("go east")
    match_guard("go north", exits=["east", "south"])
    match_guard("go north", exits=["north"])
    match_by_class(Click(position=(0,0), button="left"))
    match_by_class(Quit())

    try:
        match_by_class("BADVALUE")
    except ValueError:
        pass

    match_json_event({"verb": "GET", "page": "articles", "pageno": 5, "info": "extra"})
    pass

if name == 'main':     
    main()

Пост написан по материалам статьи. Исходный код взят отсюда. Репо на Github по указанной ссылке содержит много других примеров кода, которые будут полезны интересующимся. Старый вариант «сопоставления» можно найти тут. Видео по теме поста можно найти тут. Данная статья может быть обновлена, когда появятся функционально-ориентированные примеры.

Only registered users can participate in poll. Log in, please.
Просим оценить полезность материала
69.23% Высокая36
23.08% Нейтральная12
7.69% Низкая4
52 users voted. 10 users abstained.
Tags:
Hubs:
If this publication inspired you and you want to support the author, do not hesitate to click on the button
Total votes 14: ↑14 and ↓0+14
Comments15

Articles