В Python 3.10 введена новая конструкция match/case
, которая называется Structural pattern matching (соответствие структуре шаблона). Оператор match
был введен для того, чтобы быть больше чем просто похожим на оператор switch
, который присутствует во многих других языках программирования. PEP 634, 635 и 636 содержат много информации о том, что приносит конструкция match/case
в Python 3.10, а также обоснование его добавления.
Во многих случаях конструкция match/case
, может упростить и повысить читабельность кода Python. Этот материал акцентирует внимание на практическом использовании конструкции match/case
, чтобы писать короткий и красивый код.
Шаблоны в операторах case
, конструкции match/case
состоят из последовательностей, словарей, примитивных типов данных (int
, float
,str
и т.д.), а также экземпляров классов. Сопоставление с образцом позволяет программам извлекать информацию из сложных типов данных, переходить к структуре данных и применять определенные действия на основе различных форм данных.
Общий синтаксис конструкции match/case
:
match subject: case <pattern_1>: <action_1> case <pattern_2>: <action_2> case <pattern_3>: <action_3> case _: <action_wildcard>
Оператор match
принимает выражение subject
и сравнивает его значение с последовательными шаблонами, заданными как один или несколько блоков case
. В частности, сопоставление с образцом работает следующим образом:
subject
);subject
в заявлении match
;subject
с каждым шаблоном в заявлении case
сверху вниз, пока совпадение не будет подтверждено.action
, связанного с шаблоном подтвержденного совпадения;case
c подстановочным знаком '_'
, если он указан. Если точное совпадение не подтверждено и case _:
- не существует, то весь блок match
не выполняется.Обратите внимание, что большинство литералов сравниваются по равенству. Однако синглтоны: True
, False
и None
сравниваются по идентичности.
match/case
;match/case
+ распаковка последовательности или словаря;match/case
;match/case
.match/case
.Для примера простого использования match/case
реализуем функцию факториала. Пример взят из материала "Рекурсия в Python":
def factorial(n): if n == 0 or n == 1: return 1 else: return n * factorial(n-1) factorial(5) # 120
Теперь вместо конструкции if/else
будем использовать сопоставление match/case
:
def factorial(n): match n: case 0 | 1: return 1 case _: return n * factorial(n - 1) factorial(5)
Обратите внимание на пару моментов: конструкция начинается с выражения match n
, это означает, что можно делать разные вещи в зависимости от того, какое значение передается в n
. В конструкции также присутствуют операторы case
, которые можно рассматривать в различных возможных сценариях, которые необходимо обработать. Каждый оператор case
должен сопровождаться шаблоном, с которым оператор match
будет пытаться сопоставить значение переменной n
.
Шаблоны также могут содержать альтернативы, которые можно перечислить через '|'
(или). В примере выше, паттерн case 0 | 1:
означает: значение n
равно 0 или 1. Второй шаблон примера, case _:
, является значением по умолчанию, т. е. если не один шаблон не подошел (не сработал), то выбирается case _:
(работает как оператор else
без условия).
Иногда может потребоваться сопоставление с более структурированным шаблоном, указанным в case
def go(direction): match direction: case "North" | "East" | "South" | "West": return "Хорошо, я пошел!" case _: return "Неизвестное направление..." print(go("North")) # Хорошо, я пошел! print(go("asfasdf")) # Неизвестное направление...
Теперь представьте, что логика обработки переменной “direction” вложена в нечто более сложное:
def act(command): match command.split(): case "Cook", "breakfast": return "Я люблю завтракать." case "Cook", *wtv: return "Что то готовится..." case "Go", "North" | "East" | "South" | "West": return "Хорошо, я пошел!" case "Go", *wtv: return "Неизвестное направление..." case _: return "Я не могу этого сделать..." print(act("Go North")) # Хорошо, я пошел! print(act("Go asdfasdf")) # Неизвестное направление... print(act("Cook breakfast")) # Я люблю завтракать. print(act("Drive")) # Я не могу этого сделать...
Можно зафиксировать совпадение, оставив опции открытыми, а затем записать результат совпадения в переменную:
def act(command): match command.split(): case "Cook", "breakfast": return "Я люблю завтракать." case "Cook", *wtv: return f"Готовится {wtv}..." # результат совпадения записывается в переменную `direction` case "Go", "North" | "East" | "South" | "West" as direction: return f"Хорошо, я пошел на {direction}!" case "Go", *wtv: return f"{wtv} - неизвестное направление." case _: return "Я не могу этого сделать..." print(act("Go North")) # Хорошо, я пошел на North! print(act("Go asdfasdf")) # asdfasdf - неизвестное направление.
Хотя оператор соответствия match
может использоваться как простой оператор if/else
, как показано выше, он действительно эффективен, когда нужно сопоставить структурированные данные:
def normalise_colour_info(colour): """Нормализация структуры цвета, как (name, (r, g, b, alpha)).""" match colour: case (r, g, b): name = "" a = 0 case (r, g, b, a): name = "" case (name, (r, g, b)): a = 0 case (name, (r, g, b, a)): pass case _: raise ValueError("Unknown colour info.") return (name, (r, g, b, a)) >>> print(normalise_colour_info((240, 248, 255))) # ('', (240, 248, 255, 0)) >>> print(normalise_colour_info((240, 248, 255, 0))) # ('', (240, 248, 255, 0)) >>> print(normalise_colour_info(("AliceBlue", (240, 248, 255)))) # ('AliceBlue', (240, 248, 255, 0)) >>> print(normalise_colour_info(("AliceBlue", (240, 248, 255, 0.3)))) # ('AliceBlue', (240, 248, 255, 0.3))
Обратите внимание, что каждый case
содержит выражение, подобное левой части присвоения с распаковкой, и когда структура цвета совпадает со структурой, указанной в определенном case
, то тогда значения присваиваются переменным, указанным в case
.
Это большое улучшение по сравнению с эквивалентным кодом с операторами if/else
:
def normalise_colour_info(colour): """Нормализация структуры цвета, как (name, (r, g, b, alpha)).""" if not isinstance(colour, (list, tuple)): raise ValueError("Unknown colour info.") if len(colour) == 3: r, g, b = colour name = "" a = 0 elif len(colour) == 4: r, g, b, a = colour name = "" elif len(colour) != 2: raise ValueError("Unknown colour info.") else: name, values = colour if not isinstance(values, (list, tuple)) or len(values) not in [3, 4]: raise ValueError("Unknown colour info.") elif len(values) == 3: r, g, b = values a = 0 else: r, g, b, a = values return (name, (r, g, b, a))
Версия функции нормализации цвета становится еще лучше, если добавить проверку типов, запрашивая конкретные значения:
def normalise_colour_info2(colour): match colour: case (int(r), int(g), int(b)): name = "" a = 0 case (int(r), int(g), int(b), int(a)): name = "" case (str(name), (int(r), int(g), int(b))): a = 0 case (str(name), (int(r), int(g), int(b), int(a))): pass case _: raise ValueError("Unknown colour info.") return (name, (r, g, b, a))) print(normalise_colour_info(("AliceBlue", (240, 248, 255)))) # ('AliceBlue', (240, 248, 255, 0)) print(normalise_colour_info2(("Red", (255, 0, "0")))) # ValueError: Unknown colour info.
Конструкция match/case
также может использоваться для сопоставления структуры экземпляров класса. Создадим класс Point()
, который будет представлять точки в двумерном пространстве:
class Point: x: int y: int
Теперь надо написать небольшую функцию, которая берет Point()
и записывает небольшое описание того, где находится точка. Можно использовать сопоставление с образцом match/case
для захвата значений атрибутов x
и y
объекта Point()
, более того, можно использовать однострочные операторы if/else
, чтобы помочь сузить тип совпадений!
Смотрим пример:
def describe_point(point): """Удобочитаемое описание положения точки.""" match point: case Point(x=0, y=0): desc = "в начале координат" case Point(x=0, y=y): desc = f"на вертикальной оси, при y = {y}" case Point(x=x, y=0): desc = f"на горизонтальной оси, при x = {x}" # использование однострочного оператора if/else case Point(x=x, y=y) if x == y: desc = f"вдоль линии x = y, где x = y = {x}" case Point(x=x, y=y) if x == -y: desc = f"вдоль линии x = -y, где x = {x} и y = {y}" case Point(x=x, y=y): desc = f"в позиции {point}" return "Точка находится " + desc print(describe_point(Point(0, 0))) # Точка находится в начале координат print(describe_point(Point(3, 0))) # Точка находится на горизонтальной оси, при x = 3 print(describe_point(Point(3, -3))) # Точка находится вдоль линии x = -y, где x = 3 и y = -3 print(describe_point(Point(1, 2))) # Точка находится в позиции (1, 2)
Обратите внимание, что для создания нового шаблона экземпляра Point()
, приходится указывать, какой аргумент был x
, а какой y
(Point(x=x, y=y)
). Можно использовать позиционные параметры с некоторыми встроенными классами, которые обеспечивают упорядочение их атрибутов (например, dataclasses
). Также можно определить конкретную позицию для атрибутов в шаблонах, установив специальный атрибут __match_args__
в классах.
Вот более короткая версия приведенного выше примера, в котором используется __match_args__
при определении класса Point()
:
class Point: __match_args__ = ["x", "y"] x: int y: int def describe_point(point): """Удобочитаемое описание положения точки.""" match point: case Point(0, 0): desc = "в начале координат" case Point(0, y): desc = f"на вертикальной оси, при y = {y}" case Point(x, 0): desc = f"на горизонтальной оси, при x = {x}" case Point(x, y): desc = f"в позиции {point}" return "Точка находится " + desc print(describe_point(Point(0, 0))) # Точка находится в начале координат print(describe_point(Point(3, 0))) # Точка находится на горизонтальной оси, при x = 3 print(describe_point(Point(1, 2))) # The point is at (1, 2)
Паттерны могут быть произвольно вложенными. Например, если данные представляют собой короткий список точек, они могут быть сопоставлены следующим образом:
match points: case []: print("Нет точек для сопоставления.") case [Point(0, 0)]: print("Единственная точка в начале координат.") case [Point(x, y)]: print(f"Единственная точка с координатами {x}, {y}.") case [Point(0, y1), Point(0, y2)]: print(f"Две точки на оси Y в точке {y1}, {y2}.") case _: print("В списке есть еще кое-что.")
match/case
+ распаковка последовательности или словаря.Еще одна классная вещь, которую можно сделать в конструкции сопоставления match/case
- это использовать эффект распаковки последовательностей (одна звездочка *
) и словарей (две звездочки **
).
match/case
.Смотрим на следующий код, который распаковывает последовательность:
>>> head, *body, tail = range(10) >>> print(head, body, tail) # 0 [1, 2, 3, 4, 5, 6, 7, 8] 9
Здесь *body
сообщает Python, что нужно вставлять в переменную body
все, что не входит в head
или tail
. В конструкции match/case
можно использовать распаковку *
со списками и кортежами. Другими словами шаблоны последовательностей поддерживают подстановочные знаки: [x, y, *rest]
и (x, y, *rest)
работают аналогично подстановочным знакам при распаковке. После *
также может быть имя _
, следовательно (x, y, *_)
соответствует последовательности по крайней мере из двух элементов без привязки остальных элементов.
Например нужно создать последовательность, где каждое число получается из предыдущего, сопоставляя первые цифры. Например, если в начале есть три одинаковые цифры подряд, например 222
, то они переписываются их как 32
, далее, оставшееся часть последовательности, снова оценивается в цикле, пока совпадения не закончатся.
С оператором match
код становится намного чище:
def rule_substitution(seq): new_seq = [] while seq: match seq: case [x, y, z, *tail] if x == y == z: new_seq.extend(["3", x]) case [x, y, *tail] if x == y: new_seq.extend(["2", x]) case [x, *tail]: new_seq.extend(["1", x]) seq = tail return new_seq seq = ["1"] for _ in range(10): seq = rule_substitution(seq) print("".join(seq)) # 11 # 21 # 1211 # 111221 # 312211 # 13112221 # 1113213211 # 31131211131221 # 13211311123113112211 # 11131221133112132113212221
match/case
.Точно так же мы можем использовать **
для сопоставления остатка словаря. Но сначала посмотрим, каково поведение match/case
при сопоставлении словарей:
d = {0: "oi", 1: "uno"} match d: case {0: "oi"}: return True case _: return False # True
Если словарь d
имеет пару ключ/значение 0: "oi"
и этот ключ/значение указан в единственном операторе case {0: "oi"}
, то происходит совпадение. При сопоставлении со словарями необходимо заботится только о совпадении структуры, которая была явно упомянута в case
. В отличие от шаблонов последовательности, любые другие дополнительные ключи, которые есть в исходном словаре, игнорируются.
Также поддерживается знак распаковки словарей **
- запишется остаток словаря от сопоставления. (Но **_
был бы лишним, поэтому его использование недопустимо.)
Например ситуация, в которой необходимо получить исходный словарь, за исключением явно указанного в case
ключа/значения. В этом случае можно использовать знак распаковки словаря **
.
d = {0: "oi", 1: "uno"} match d: case {0: "oi", **remainder}: print(remainder) # {1: 'uno'}
Такое поведение оператора match
со словарями, можно использовать в своих интересах:
d = {0: "oi", 1: "uno"} match d: case {0: "oi", **remainder} if not remainder: print("Словарь состоит одной пары ключ/значение") case {0: "oi", **remainder}: print(f"Словарь включает 0: 'oi' и имеет дополнительно {remainder}.") # Словарь включает 0: 'oi' и имеет дополнительно {1: "uno"}.
Кроме того можно использовать переменные для сопоставления значений заданных ключей:
d = {0: "oi", 1: "uno"} match d: case {0: zero, 1: one}: print(f"Ключ 0 сопоставлено с `{zero}` и ключ 1 с `{one}`") # Ключ 0 сопоставлен с `oi` и ключ 1 с `uno`
match/case
.В шаблонах могут использоваться именованные константы (перечисления). Эти именованные константы должны иметь имена, разделенные точками, чтобы константа не интерпретировалась как переменная захвата:
from enum import Enum class Color(Enum): RED = 0 GREEN = 1 BLUE = 2 match color: case Color.RED: print("Это красный") case Color.GREEN: print("Трава зеленая") case Color.BLUE: print("Небо синее")
match/case
.pattern
в операторе case
) имеют одно и то же значение и фактически соответствуют произвольным последовательностям. Технически, subject
(в операторе match
) должен быть последовательностью. Поэтому важным исключением является то, что шаблоны pattern
не соответствуют итераторам. Кроме того, во избежание распространенной ошибки, шаблоны последовательностей, используемые в pattern
не соответствуют строкам.pattern
в операторе case
) поддерживают подстановочные знаки: [x, y, *rest]
и (x, y, *rest)
, работают аналогично подстановочным знакам при распаковке. Имя после *
также может быть _
, поэтому (x, y, *_)
соответствует последовательности по крайней мере из двух элементов без привязки остальных элементов.pattern
в операторе case
), например: case {"bandwidth": b, "latency": l}
будут сравниваться (фиксироваться) ключи "bandwidth"
и "latency"
. В отличие от шаблонов с последовательностями, дополнительные ключи игнорируются. Также поддерживается подстановочный знак **
. (Но **_
было бы излишним, поэтому не допускается.)case (Point(x1, y1), Point(x2, y2) as p2): ...
. Это связывает x1
, y1
, x2
, y2
, как и следовало ожидать без предложения as
, и p2
со всем вторым элементом subject
.==
. Однако синглтоны True
, False
и None
сравниваются по идентичности.Именованные константы могут использоваться в шаблонах pattern
. Эти именованные константы должны иметь имена, разделенные точками, чтобы константа не интерпретировалась как переменная захвата:
from enum import Enum class Color(Enum): RED = 0 GREEN = 1 BLUE = 2 match color: case Color.RED: print("I see red!") case Color.GREEN: print("Grass is green") case Color.BLUE: print("I'm feeling the blues :(")