Программисты любят, когда код говорит сам за себя. Представьте, что вы можете просмотреть структуру данных и мгновенно понять, что с ней делать — без сложных условий и вложенных проверок. Именно эту элегантность предлагает паттерн-матчинг, который появился в Python 3.10 и стал значительным шагом в эволюции языка.
Паттерн-матчинг — это способность программы анализировать структуру данных и выполнять действия в зависимости от её формы. По сути, это как если бы вы могли сказать: "Если данные выглядят так, сделай это; если иначе — сделай то". Звучит просто, но в этой простоте и кроется вся сила. В отличие от традиционных конструкций if-elif-else , которые проверяют значения, паттерн-матчинг фокусируется на форме и структуре. Это ключевое различие делает код не только более читаемым, но и значительно упрощает работу со сложными структурами данных, такими как списки и словари.
Python | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
| # Традиционный подход с условными операторами
def process_data(data):
if isinstance(data, list):
if len(data) > 0 and data[0] == "command":
if len(data) > 1:
# Обработка команды с аргументами
pass
else:
# Обработка команды без аргументов
pass
elif isinstance(data, dict):
if "type" in data and data["type"] == "message":
# Обработка сообщения
pass
# С использованием паттерн-матчинга
def process_data(data):
match data:
case ["command", *args] if args:
# Обработка команды с аргументами
pass
case ["command"]:
# Обработка команды без аргументов
pass
case {"type": "message"}:
# Обработка сообщения
pass |
|
Взглянув на пример выше, сразу видно, насколько более выразительным становится код с паттерн-матчингом. Он не только короче, но и проще для понимания — структура данных сразу видна в шаблоне.
История конструкции match-case в Python интересна тем, что язык долго шел к ней. Многие языки программирования, особенно из функционального семейства, давно имели такую возможность. Haskell и Scala известны своим мощным паттерн-матчингом, Rust сделал его центральным элементом работы с данными. Python с его философией читаемости кода не мог оставаться в стороне.
В Python 3.10, выпущенном в октябре 2021 года, паттерн-матчинг наконец стал реальностью. Это было результатом долгих обсуждений и разработки через PEP 634, 635 и 636 (Python Enhancement Proposals). Новая конструкция получилась не просто заимствованием из других языков, а самостоятельным решением, отражающим дух Python — простой, но мощный инструмент. Особенная красота паттерн-матчинга раскрывается при работе с данными, имеющими сложную структуру — многоуровневыми списками, вложенными словарями, комбинациями различных типов. Это та область, где традиционные условные выражения становятся громоздкими и трудночитаемыми, а новая конструкция раскрывает все свои преимущества.
Основы работы с паттерн-матчингом
Синтаксис паттерн-матчинга в Python элегантен и интуитивно понятен. Основная конструкция состоит из двух ключевых слов: match и case . Выражение match принимает объект для сопоставления, а блоки case определяют шаблоны и соответствующие им действия.
Python | 1
2
3
4
5
6
7
| match значение:
case шаблон_1:
# код, выполняемый при соответствии шаблону_1
case шаблон_2:
# код, выполняемый при соответствии шаблону_2
case _:
# код для всех остальных случаев (аналог default) |
|
При первом взгляде паттерн-матчинг может показаться просто аналогом конструкции switch-case из других языков, но это поверхностное сравнение. Фундаментальное отличие заключается в том, что switch-case обычно работает только с простыми значениями (числами, строками, перечислениями), тогда как паттерн-матчинг в Python способен анализировать структуру данных и их содержимое. Рассмотрим простой пример:
Python | 1
2
3
4
5
6
7
8
9
10
11
12
| def describe_value(x):
match x:
case 0:
return "Ноль"
case 1:
return "Один"
case _:
return f"Число {x}"
print(describe_value(0)) # Ноль
print(describe_value(1)) # Один
print(describe_value(5)) # Число 5 |
|
Здесь шаблоны достаточно просты — мы сопоставляем значения с конкретными числами. Но настоящее могущество паттерн-матчинга проявляется, когда мы начинаем использовать структурные шаблоны:
Python | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| def analyze_point(point):
match point:
case (0, 0):
return "Начало координат"
case (0, y):
return f"Точка на оси Y: {y}"
case (x, 0):
return f"Точка на оси X: {x}"
case (x, y) if x == y:
return f"Точка на диагонали: {x}, {y}"
case (x, y):
return f"Точка в пространстве: {x}, {y}"
print(analyze_point((0, 0))) # Начало координат
print(analyze_point((0, 5))) # Точка на оси Y: 5
print(analyze_point((3, 3))) # Точка на диагонали: 3, 3
print(analyze_point((2, 4))) # Точка в пространстве: 2, 4 |
|
В этом примере мы уже видим несколько важных концепций паттерн-матчинга:
1. Сопоставление с конкретными значениями — case (0, 0) проверяет, равна ли точка началу координат.
2. Захват значений — case (0, y) захватывает значение координаты Y, если X равен нулю.
3. Защитные выражения (guards) — case (x, y) if x == y добавляет дополнительное условие после основного шаблона.
Защитные выражения — это мощный инструмент, который позволяет уточнить шаблон с помощью произвольного логического условия. Они начинаются с ключевого слова if после шаблона и могут содержать любое выражение, возвращающее булево значение.
Python | 1
2
3
4
5
6
7
8
9
10
11
12
| def process_age(person):
match person:
case {"name": name, "age": age} if age < 18:
return f"{name} — несовершеннолетний"
case {"name": name, "age": age} if age >= 65:
return f"{name} — пенсионер"
case {"name": name, "age": age}:
return f"{name} — взрослый"
print(process_age({"name": "Алиса", "age": 15})) # Алиса — несовершеннолетний
print(process_age({"name": "Боб", "age": 35})) # Боб — взрослый
print(process_age({"name": "Карл", "age": 70})) # Карл — пенсионер |
|
Важно понимать порядок выполнения проверок в паттерн-матчинге. Python проверяет шаблоны последовательно, сверху вниз, и выполняет код для первого совпадения. Как только найдено соответствие, выполнение блока match завершается. Это значит, что порядок шаблонов имеет значение — более специфичные шаблоны должны идти перед более общими.
Python | 1
2
3
4
5
6
7
8
9
10
11
12
13
| # Правильный порядок — от специфичного к общему
match value:
case [1, 2]: # сначала специфичный шаблон
print("Список [1, 2]")
case [int(), int()]: # затем более общий
print("Список из двух целых чисел")
# Неправильный порядок — общий шаблон перехватит все совпадения
match value:
case [int(), int()]: # этот шаблон уже перехватит [1, 2]
print("Список из двух целых чисел")
case [1, 2]: # этот шаблон никогда не будет достигнут
print("Список [1, 2]") |
|
В отличие от конструкции switch-case во многих других языках, в Python не требуется явно прерывать выполнение с помощью break или подобных операторов. Это устраняет распространенный источник ошибок, когда программисты забывают добавить break и код "проваливается" в следующий блок.
Вместе с тем, паттерн-матчинг в Python предоставляет механизмы, которых нет в традиционных switch-case :
1. Сопоставление с подтипами — можно проверять, является ли объект экземпляром определённого класса:
Python | 1
2
3
4
5
6
7
8
| def process_shape(shape):
match shape:
case Circle(radius=r):
return f"Круг с радиусом {r}"
case Rectangle(width=w, height=h):
return f"Прямоугольник {w}×{h}"
case _:
return "Неизвестная фигура" |
|
2. OR-шаблоны — позволяют объединять несколько шаблонов:
Python | 1
2
3
4
5
| match command:
case "quit" | "exit" | "bye":
exit_program()
case "help" | "?":
show_help() |
|
3. Деструктуризация объектов — позволяет извлекать отдельные атрибуты объектов:
Python | 1
2
3
4
5
6
7
8
9
10
11
| class Point:
def __init__(self, x, y):
self.x = x
self.y = y
def analyze(point):
match point:
case Point(x=0, y=0):
return "В начале координат"
case Point(x=0, y=y):
return f"На оси Y: {y}" |
|
Важный аспект паттерн-матчинга в Python — его интеграция с системой типов. Хотя Python — динамически типизированный язык, паттерн-матчинг фактически вводит элементы структурной типизации, где важна не только конкретная реализация объекта, но и наличие определённых атрибутов и методов.
Python | 1
2
3
4
5
6
7
| # Пример использования структурной типизации в паттерн-матчинге
def describe_sequence(seq):
match seq:
case seq if hasattr(seq, "__len__"):
return f"Последовательность длиной {len(seq)}"
case _:
return "Не последовательность" |
|
Чтобы избежать ошибок при работе с паттерн-матчингом, полезно знать несколько особенностей:
1. Переменная с именем _ в шаблоне действует как подстановочный знак и не связывается ни с каким значением.
2. Имена переменных в разных частях одного шаблона должны иметь одинаковые значения в соответствующих местах проверяемого объекта.
3. Чтобы использовать переменную из внешней области видимости в шаблоне, её нужно тексутально заключить в кавычки.
Python | 1
2
3
4
| x = 1
match value:
case `x`: # проверит равенство с переменной x из внешней области
print("Совпадает с x") |
|
При обработке логических условий паттерн-матчинг может заменить сложные конструкции из вложенных условных выражений, делая код более линейным и понятным. Особенно это проявляется при анализе состояний в играх, парсинге командной строки или обработке событий в интерфейсе.
Списки и словари Python Здраствуйте, подскажите а можно ли как-то достать из списка в котором словарь с ключами, значения ключей выборочно и одним кодом? если у меня там... Python: списки, множества, словари (основы) . Напишите программу, на вход которой подается строка из целых чисел, разделенных пробелом. Необходимо вывести произведение введенных чисел, если... Pattern matching здравствуйте, как известно:
С релизом python 3.10 появился pattern matching, собственно и являющийся главной особенностью этого релиза. Возможно... Списки.Словари . На балансе предприятия находится десять зданий. Для каждого из
них известна начальная стоимость (руб) и величина износа (%). Определить ...
Матчинг списков
Списки — одна из самых фундаментальных структур данных в Python, и паттерн-матчинг раскрывает новые горизонты в работе с ними. Структурное сопоставление позволяет безошибочно декомпозировать список на составные элементы и обрабатывать его содержимое элегантным способом.
Самая базовая форма матчинга списков — это сопоставление с конкретным списком, содержащим определённые значения:
Python | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
| def process_command(cmd):
match cmd:
case ["help"]:
return "Показываю справку"
case ["exit"]:
return "Выхожу из программы"
case ["version"]:
return "Версия 1.0"
case _:
return "Неизвестная команда"
print(process_command(["help"])) # Показываю справку
print(process_command(["version"])) # Версия 1.0
print(process_command(["status"])) # Неизвестная команда |
|
В этом примере мы сопоставляем входную команду с конкретными шаблонами. Однако списки часто имеют более сложную структуру и переменную длину. Здесь на помощь приходят расширенные возможности паттерн-матчинга.
Оператор звёздочки (* ) позволяет захватывать произвольное количество элементов. Это невероятно удобно при работе с последовательностями неизвестной длины:
Python | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| def analyze_data(data):
match data:
case []:
return "Пустой список"
case [x]:
return f"Список с одним элементом: {x}"
case [x, y]:
return f"Пара элементов: {x}, {y}"
case [x, y, *rest]:
return f"Длинный список. Первые два: {x}, {y}. Остальных: {len(rest)}"
print(analyze_data([])) # Пустой список
print(analyze_data([42])) # Список с одним элементом: 42
print(analyze_data([1, 2])) # Пара элементов: 1, 2
print(analyze_data([1, 2, 3, 4, 5])) # Длинный список. Первые два: 1, 2. Остальных: 3 |
|
Оператор * может находиться не только в конце шаблона, но и в середине, что позволяет создавать более сложные шаблоны:
Python | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
| def process_command(args):
match args:
case ["count", *files, "--verbose"]:
return f"Подсчитываю файлы {files} с подробным выводом"
case ["count", *files]:
return f"Подсчитываю файлы {files}"
case [cmd, *_] if cmd not in ["count", "list", "delete"]:
return f"Неизвестная команда: {cmd}"
print(process_command(["count", "file1.txt", "file2.txt"]))
[H2]Подсчитываю файлы ['file1.txt', 'file2.txt'][/H2]
print(process_command(["count", "file1.txt", "--verbose"]))
# Подсчитываю файлы ['file1.txt'] с подробным выводом |
|
Важный аспект работы с оператором * — он собирает элементы в список, даже если совпадений нет. В последнем случае получается пустой список:
Python | 1
2
3
4
5
6
7
8
9
| match [1, 2, 3]:
case [first, *middle, last]:
print(f"Первый: {first}, середина: {middle}, последний: {last}")
[H2]Вывод: Первый: 1, середина: [2], последний: 3[/H2]
match [1, 2]:
case [first, *middle, last]:
print(f"Первый: {first}, середина: {middle}, последний: {last}")
# Вывод: Первый: 1, середина: [], последний: 2 |
|
При работе со списками можно комбинировать литеральные значения, переменные и сложные выражения в рамках одного шаблона. Это делает паттерн-матчинг гибким инструментом для обработки сложных структур:
Python | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| def process_command(cmd):
match cmd:
case ["set", name, value] if name.startswith("--"):
return f"Устанавливаю опцию {name[2:]} = {value}"
case ["set", name, value]:
return f"Устанавливаю переменную {name} = {value}"
case ["get", *variables] if variables:
return f"Получаю значения переменных: {', '.join(variables)}"
case ["get"]:
return "Ошибка: не указаны переменные для получения"
print(process_command(["set", "--verbose", "true"]))
[H2]Устанавливаю опцию verbose = true[/H2]
print(process_command(["get", "name", "age", "address"]))
# Получаю значения переменных: name, age, address |
|
Одна из самых мощных возможностей при работе с матчингом списков — рекурсивное сопоставление вложенных структур. Вы можете задавать шаблоны для элементов внутри списка, создавая сложные конструкции сопоставления:
Python | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| def process_nested_data(data):
match data:
case [subject, [grade1, grade2, *_]]:
return f"Предмет {subject} имеет оценки {grade1} и {grade2}"
case [subject, []]:
return f"Предмет {subject} не имеет оценок"
case [subject, grades] if isinstance(grades, list):
return f"Предмет {subject} имеет {len(grades)} оценок"
case [name, address, [phone, *alt_phones]]:
return f"Контакт {name}: {address}, тел. {phone}"
print(process_nested_data(["Математика", [5, 4, 3, 5]]))
[H2]Предмет Математика имеет оценки 5 и 4[/H2]
print(process_nested_data(["Иван Иванов", "ул. Пушкина, 10", ["123-45-67", "987-65-43"]]))
# Контакт Иван Иванов: ул. Пушкина, 10, тел. 123-45-67 |
|
Подстановочный знак (_ ) часто используется, когда нам нужно только проверить структуру, но не захватывать конкретные значения:
Python | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
| def validate_format(data):
match data:
case [_, _, _]:
return "Список из трёх элементов"
case [[_, _], [_, _]]:
return "Список из двух пар"
case [{"id": _}, {"id": _}]:
return "Два объекта с ID"
case _:
return "Неизвестный формат"
print(validate_format([1, 2, 3])) # Список из трёх элементов
print(validate_format([[1, 2], [3, 4]])) # Список из двух пар
print(validate_format([{"id": 1}, {"id": 2}])) # Два объекта с ID |
|
При работе со списками можно использовать OR-шаблоны (с помощью оператора | ) для обработки альтернативных вариантов:
Python | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
| def classify_command(cmd):
match cmd:
case ["add" | "create" | "new", name]:
return f"Создание: {name}"
case ["delete" | "remove", name]:
return f"Удаление: {name}"
case ["show" | "display" | "view", name]:
return f"Просмотр: {name}"
case _:
return "Неизвестная команда"
print(classify_command(["add", "user"])) # Создание: user
print(classify_command(["remove", "file"])) # Удаление: file
print(classify_command(["view", "log"])) # Просмотр: log |
|
В реальных приложениях часто встречаются сложные команды с множеством опций. Рассмотрим пример процессинга команды:
Python | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
| from enum import Enum
class FileType(Enum):
SOURCE_CODE = "src"
HTML = "html"
IMAGES = "img"
DOCUMENTS = "doc"
OTHERS = "other"
def process_command(args):
match args:
case ["count", (FileType.SOURCE_CODE.value | FileType.HTML.value | FileType.IMAGES.value | FileType.DOCUMENTS.value | FileType.OTHERS.value) as file_type]:
return f"Подсчёт файлов типа {file_type}"
case ["count", _, *_]:
return "Ошибка: команда 'count' требует точно один аргумент с типом файла"
case ["sha1", "verify"] | ["sha1", "verify", _, _, _, *_]:
return "Ошибка: команда 'verify-sha1' требует ровно два аргумента (путь к файлу и ожидаемый хеш)"
case ["sha1", "verify", file_path, expected_hash]:
return f"Проверка SHA1 для файла {file_path}"
case ["sha1", *file_paths] if file_paths:
return f"Расчёт SHA1 для файлов: {', '.join(file_paths)}"
case _:
return "Неизвестная команда" |
|
В этом примере мы видим несколько важных техник:
1. Использование перечислений Enum для проверки допустимых значений.
2. Захват значения, совпадающего с одним из вариантов, с помощью оператора as .
3. Проверка количества аргументов через шаблоны с подстановочными знаками.
4. Комбинирование шаблонов с помощью OR-оператора (| ).
5. Использование защитных условий для валидации списка.
Матчинг списков особенно полезен при обработке аргументов командной строки, анализе данных в формате JSON, и при работе с API, которые возвращают структурированные данные. Это удобный инструмент для программ, которые должны интерпретировать текстовые команды от пользователя или запросы от клиентов.
При работе с коллекциями данных мы можем комбинировать паттерн-матчинг с проверкой типов для более точного контроля над обработкой данных. Это особенно удобно, когда нам нужно различать списки разных составов:
Python | 1
2
3
4
5
6
7
8
9
10
| def process_data(data):
match data:
case [str(title), int(year)] if 1900 <= year <= 2023:
return f"Фильм: {title} ({year})"
case [str(name), float(price)]:
return f"Товар: {name}, цена: {price}"
case [int(x), int(y), int(z)]:
return f"3D координаты: ({x}, {y}, {z})"
case [*items] if all(isinstance(x, int) for x in items):
return f"Список целых чисел: {sum(items)}" |
|
Обратим внимание на последний шаблон — он показывает, как можно комбинировать паттерн-матчинг с классическими функциями Python для выполнения более сложных проверок.
Ещё одна сильная сторона матчинга списков — возможность захвата фиксированных позиций в комбинации со звёздочным оператором:
Python | 1
2
3
4
5
6
7
8
| def parse_log_entry(entry):
match entry:
case [timestamp, "ERROR", message, *details]:
return f"Критическая ошибка в {timestamp}: {message}"
case [timestamp, "WARNING", message, *_]:
return f"Предупреждение в {timestamp}: {message}"
case [timestamp, level, *_]:
return f"Запись уровня {level} в {timestamp}" |
|
При работе с вложенными структурами данных можно создавать очень детальные шаблоны:
Python | 1
2
3
4
5
6
7
8
9
10
| def analyze_student_data(data):
match data:
case [name, [grade1, grade2]] if grade1 > 90 and grade2 > 90:
return f"Отличник: {name}"
case [name, [_, _, *rest]] if len(rest) > 5:
return f"Много оценок у {name}"
case [name, grades] if sum(grades) / len(grades) >= 70:
return f"Хороший студент: {name}"
case [name, _]:
return f"Требует внимания: {name}" |
|
Особенно удобен матчинг списков при обработке результатов парсинга, где разные структуры данных могут представлять разные типы элементов:
Python | 1
2
3
4
5
6
7
8
9
10
11
12
13
| def render_html_element(element):
match element:
case ["img", {"src": src, "alt": alt, **attrs}]:
extra = " ".join(f'{k}="{v}"' for k, v in attrs.items())
return f'<img src="{src}" alt="{alt}" {extra}>'
case ["a", {"href": href}, *content]:
inner = "".join(render_html_element(item) if isinstance(item, list) else str(item) for item in content)
return f'<a href="{href}">{inner}</a>'
case ["div", *content]:
inner = "".join(render_html_element(item) if isinstance(item, list) else str(item) for item in content)
return f'<div>{inner}</div>'
case str(text):
return text |
|
Эти примеры иллюстрируют, как паттерн-матчинг списков позволяет создавать элегантный код для обработки сложных структур данных. Вместо вложенных условных операторов и проверок типов, мы получаем декларативный стиль программирования, где структура шаблонов отражает структуру обрабатываемых данных.
Не стоит забывать про кортежи — они ведут себя похоже на списки в контексте паттерн-матчинга, но с важным отличием: шаблоны для кортежей требуют, чтобы входные данные были именно кортежами, а не списками, даже если элементы идентичны.
Python | 1
2
3
4
5
6
7
8
9
| # Этот шаблон сработает только для кортежа
match value:
case (1, 2):
print("Кортеж (1, 2)")
# Этот шаблон сработает только для списка
match value:
case [1, 2]:
print("Список [1, 2]") |
|
Когда мы используем захват с помощью * в кортежных шаблонах, захваченные элементы также собираются в кортеж, а не в список. Это важно помнить при дальнейшей обработке данных.
Матчинг словарей
Словарь (dict) — одна из ключевых структур данных в Python, используемая для хранения пар "ключ-значение". Словари незаменимы при работе с JSON-данными, конфигурациями, человеко-читаемыми настройками и многими другими форматами. Паттерн-матчинг превращает обработку словарей из рутинного занятия в изящное и выразительное действо. В отличие от матчинга списков, где порядок элементов имеет значение, при сопоставлении с образцом словарей важны только ключи и их значения, но не порядок их определения. Это полностью соответствует природе словарей как неупорядоченных коллекций.
Базовый синтаксис для матчинга словарей выглядит следующим образом:
Python | 1
2
3
4
5
6
7
8
9
10
| def process_user_data(user_data):
match user_data:
case {"name": name, "age": age}:
return f"Пользователь {name}, возраст {age}"
case {"username": username}:
return f"Пользователь с именем {username}"
case {"email": email}:
return f"Контакт: {email}"
case _:
return "Неизвестный формат данных" |
|
Когда вы сопоставляете словарь с шаблоном, Python проверяет наличие указанных ключей и сопоставляет их значения. Есть важная особенность: шаблон словаря по умолчанию проверяет только наличие указанных ключей. Если в словаре присутствуют дополнительные ключи, они игнорируются:
Python | 1
2
3
4
5
6
| data = {"name": "Алексей", "age": 28, "city": "Москва", "job": "программист"}
match data:
case {"name": name, "age": age}:
print(f"{name}, {age} лет")
# Выведет: "Алексей, 28 лет", несмотря на наличие дополнительных ключей |
|
Это поведение называется "частичным сопоставлением" (partial matching) и делает матчинг словарей гибким инструментом при работе с разнородными данными. Вы можете сосредоточиться только на тех ключах, которые вам действительно нужны для обработки. Если вам нужно захватить весь словарь, соответствующий шаблону, используйте оператор as :
Python | 1
2
3
4
5
| match data:
case {"name": name, "age": age} as person:
print(f"Имя: {name}, возраст: {age}")
print(f"Полные данные: {person}")
# person будет содержать исходный словарь |
|
Паттерн-матчинг позволяет проверять вложенные словари, создавая сложные структурные шаблоны:
Python | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| def process_order(order):
match order:
case {"customer": {"name": name, "id": id}, "items": items}:
return f"Заказ от {name} (ID: {id}) содержит {len(items)} товаров"
case {"customer": {"company": company}, "total": total}:
return f"Корпоративный заказ от {company} на сумму {total}"
case {"status": "pending", "payment": payment}:
return f"Ожидается оплата: {payment}"
case {"status": status} if status in ["shipped", "delivered"]:
return f"Заказ в пути или доставлен, статус: {status}"
# Примеры вызовов
print(process_order({"customer": {"name": "Иван", "id": 12345}, "items": ["Книга", "Ручка"]}))
[H2]Заказ от Иван (ID: 12345) содержит 2 товаров[/H2]
print(process_order({"customer": {"company": "ООО Рога и Копыта"}, "total": 15000}))
# Корпоративный заказ от ООО Рога и Копыта на сумму 15000 |
|
В отличие от списков, в шаблонах словарей нельзя использовать оператор звёздочки (* ) для сбора неуказанных ключей. Однако, можно использовать оператор распаковки словаря (** ) для захвата дополнительных ключей:
Python | 1
2
3
4
5
6
7
8
| def analyze_config(config):
match config:
case {"host": host, "port": port, **rest}:
extra = ", ".join(f"{k}={v}" for k, v in rest.items())
return f"Соединение с {host}:{port}" + (f" (доп. параметры: {extra})" if rest else "")
print(analyze_config({"host": "localhost", "port": 8080, "debug": True, "timeout": 30}))
# Соединение с localhost:8080 (доп. параметры: debug=True, timeout=30) |
|
Особенно полезно сочетание матчинга словарей и защитных условий (guards), которые позволяют добавлять дополнительную логику проверки:
Python | 1
2
3
4
5
6
7
8
9
10
11
12
| def process_event(event):
match event:
case {"type": "click", "position": (x, y)} if 0 <= x < 100 and 0 <= y < 100:
return f"Клик в левом верхнем квадранте: ({x}, {y})"
case {"type": "click", "position": (x, y)}:
return f"Клик за пределами левого верхнего квадранта: ({x}, {y})"
case {"type": "hover", "duration": duration} if duration > 5:
return f"Длительное наведение: {duration} секунд"
case {"type": event_type, "timestamp": timestamp} if event_type not in ["click", "hover"]:
return f"Необычное событие {event_type} в {timestamp}"
case _:
return "Неизвестное событие" |
|
В этом примере мы не только проверяем наличие определённых ключей, но и анализируем значения с помощью дополнительных условий.
Интересная особенность матчинга словарей — возможность комбинировать проверки ключей и проверки типов значений:
Python | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
| def validate_user_input(data):
match data:
case {"age": int(age)} if age >= 18:
return "Совершеннолетний пользователь"
case {"age": int(age)}:
return f"Несовершеннолетний пользователь, возраст: {age}"
case {"age": value}:
return f"Некорректный формат возраста: {value}"
case {"name": str(name)} if len(name.strip()) > 2:
return f"Пользователь {name}"
case {"name": _}:
return "Некорректное имя"
case _:
return "Отсутствует обязательная информация" |
|
Для обработки API-ответов и JSON-структур, матчинг словарей предоставляет мощный инструментарий. Рассмотрим пример обработки ответа от REST API:
Python | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
| def handle_api_response(response):
match response:
case {"status": "success", "data": data}:
# Обработка успешного ответа с данными
return process_data(data)
case {"status": "success", "message": message}:
# Успешный ответ с информационным сообщением
return f"Операция выполнена успешно: {message}"
case {"status": "error", "error": {"code": code, "message": message}}:
# Обработка ошибки с кодом и сообщением
return f"Ошибка {code}: {message}"
case {"status": "error", "message": message}:
# Обработка простой ошибки
return f"Произошла ошибка: {message}"
case {"meta": meta, **data} if "status" not in data:
# Ответ без статуса, но с метаданными
return f"Получены данные с метаинформацией: {meta}"
case _:
# Необработанный формат ответа
return "Неизвестный формат ответа от API"
# Пример вызова
response = {
"status": "error",
"error": {
"code": 404,
"message": "Ресурс не найден"
},
"request_id": "a1b2c3d4"
}
print(handle_api_response(response))
# Ошибка 404: Ресурс не найден |
|
При работе с конфигурациями часто требуется установить значения по умолчанию для отсутствующих параметров. Паттерн-матчинг может упростить и эту задачу:
Python | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| def apply_config(config):
defaults = {
"debug": False,
"timeout": 30,
"retries": 3,
"log_level": "INFO"
}
match config:
case {"environment": "production", [B]settings}:
# В производственном окружении принудительно отключаем отладку
return {[/B]defaults, **settings, "debug": False, "log_level": "WARNING"}
case {"environment": "development", [B]settings}:
# В среде разработки включаем отладку и подробное логирование
return {[/B]defaults, **settings, "debug": True, "log_level": "DEBUG"}
case {"environment": env, **settings} if env in ["testing", "staging"]:
# Для тестовых сред
return {**defaults, **settings, "debug": settings.get("debug", True)}
case settings:
# Если окружение не указано, применяем настройки как есть
return {**defaults, **settings} |
|
Для валидации форматов данных, паттерн-матчинг предоставляет элегантное решение:
Python | 1
2
3
4
5
6
7
8
9
10
| def validate_address(address):
match address:
case {"country": "Россия", "postal_code": code} if len(code) == 6 and code.isdigit():
return True
case {"country": "США", "zip_code": code} if (len(code) == 5 or len(code) == 10) and "-" in code:
return True
case {"country": country, **_} if country not in ["Россия", "США"]:
return "Неподдерживаемая страна"
case _:
return False |
|
Паттерн-матчинг словарей особенно полезен при работе с данными, которые имеют различные форматы или версии. Допустим, вы обрабатываете события из разных источников, каждый из которых имеет свою структуру:
Python | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| def normalize_event(event):
match event:
case {"event_type": type, "timestamp": ts, "data": data}:
# Современный формат
return {"type": type, "time": ts, "payload": data}
case {"type": type, "time": ts, "payload": payload}:
# Уже нормализованный формат
return {"type": type, "time": ts, "payload": payload}
case {"action": action, "when": ts, **data}:
# Устаревший формат
return {"type": action, "time": ts, "payload": data}
case {"message": msg, "sent_at": ts}:
# Формат сообщений
return {"type": "message", "time": ts, "payload": {"text": msg}}
case _:
raise ValueError("Неизвестный формат события") |
|
Еще одним преимуществом матчинга словарей является возможность комбинировать его с другими видами паттернов. Это позволяет создавать гибкие системы обработки структурированных данных, например, при анализе результатов запросов к базам данных или обработке сообщений из очередей:
Python | 1
2
3
4
5
6
7
8
9
10
11
| def process_database_result(result):
match result:
case {"rows": [{"id": id, "name": name, **fields}], "count": 1}:
return f"Найдена одна запись: {name} (ID: {id})"
case {"rows": rows, "count": count} if count > 1:
names = [row.get("name", f"Запись {row['id']}") for row in rows if "id" in row]
return f"Найдено {count} записей: {', '.join(names)}"
case {"rows": [], "count": 0}:
return "Записи не найдены"
case {"error": {"message": message}}:
return f"Ошибка базы данных: {message}" |
|
При работе с вложенными структурами в JSON-данных, паттерн-матчинг может существенно упростить извлечение релевантной информации:
Python | 1
2
3
4
5
6
7
8
9
10
| def extract_weather_data(data):
match data:
case {"current": {"temp_c": temp, "condition": {"text": condition}},
"location": {"name": city, "country": country}}:
return f"В городе {city} ({country}) сейчас {temp}°C, {condition.lower()}"
case {"forecast": {"forecastday": [{"date": date, "day": {"maxtemp_c": max_temp,
"mintemp_c": min_temp}}]}}:
return f"Прогноз на {date}: от {min_temp}°C до {max_temp}°C"
case {"error": error}:
return f"Не удалось получить данные: {error.get('message', 'Неизвестная ошибка')}" |
|
Вместо традиционных глубоко вложенных проверок можно определить шаблоны, соответствующие конкретным схемам данных:
Python | 1
2
3
4
5
6
7
8
9
10
| def process_payment_info(payment):
match payment:
case {"status": "pending", "method": "card", "card": {"last4": digits, "expire": expire}}:
return f"Ожидание подтверждения карты *{digits} (до {expire})"
case {"status": "completed", "amount": amount, "currency": currency}:
return f"Оплата на сумму {amount} {currency} выполнена"
case {"status": "failed", "error": error, "retry_allowed": True}:
return f"Ошибка оплаты: {error}. Можно повторить."
case {"status": status} if status not in ["pending", "completed", "failed"]:
return f"Необычный статус платежа: {status}" |
|
Производительность паттерн-матчинга со словарями в Python оптимизирована и сравнима с традиционными подходами проверки ключей. В большей части случаев разница в производительности между использованием match-case и цепочки if-elif-else с проверкой ключей незначительна. Основное преимущество состоит в улучшении читаемости и уменьшении вероятности ошибок, особенно при работе со сложными структурами. Однако, стоит помнить о нескольких особенностях. Если в шаблоне вы указываете большое количество ключей (более 10-15), каждый из которых требует проверки, то это может привести к некоторому снижению производительности по сравнению с оптимизированным кодом, который проверяет только необходимые ключи. Но на практике такие случаи редки, а преимущества в удобочитаемости и сопровождаемости кода обычно превышают потенциальные незначительные потери в скорости.
При работе с API и внешними источниками данных полезно добавлять защитные условия для проверки типов данных:
Python | 1
2
3
4
5
6
7
8
9
10
| def validate_user(user_data):
match user_data:
case {"id": id, "email": email, "roles": roles} if isinstance(id, int) and isinstance(roles, list):
return f"Действительный пользователь: {email}"
case {"id": _, "email": email} if "@" not in email:
return f"Некорректный email: {email}"
case {"temp_access": True, "expires_at": expires} if isinstance(expires, (int, float)):
return f"Временный доступ до {expires}"
case _:
return "Неполные или некорректные данные пользователя" |
|
Сравнивая паттерн-матчинг словарей с традиционными подходами, можно увидеть существенное улучшение структуры кода:
Python | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
| # Традиционный подход
def process_response_old(response):
if "error" in response:
if "code" in response["error"]:
code = response["error"]["code"]
message = response["error"].get("message", "Неизвестная ошибка")
return f"Ошибка {code}: {message}"
else:
return f"Ошибка: {response['error']}"
elif "data" in response:
if "items" in response["data"]:
items = response["data"]["items"]
return f"Получено {len(items)} элементов"
else:
return "Данные без элементов"
else:
return "Неизвестный формат ответа"
# С использованием паттерн-матчинга
def process_response_new(response):
match response:
case {"error": {"code": code, "message": message}}:
return f"Ошибка {code}: {message}"
case {"error": error}:
return f"Ошибка: {error}"
case {"data": {"items": items}}:
return f"Получено {len(items)} элементов"
case {"data": {}}:
return "Данные без элементов"
case _:
return "Неизвестный формат ответа" |
|
Очевидно, что второй вариант значительно более читабельный, хорошо структурированный и меньше подвержен ошибкам.
При работе со сложными вложенными структурами данных, например, при обработке результатов работы REST API или GraphQL, паттерн-матчинг может существенно упростить выделение нужной информации:
Python | 1
2
3
4
5
6
7
8
9
10
11
12
13
| def extract_product_details(product_data):
match product_data:
case {"product": {"name": name, "price": {"current": price}, "availability": "in_stock"}}:
return f"{name} доступен по цене {price}"
case {"product": {"name": name, "price": {"current": price, "old": old_price}, "discount": discount}}:
savings = old_price - price
return f"{name} со скидкой {discount}%: {price} (экономия {savings})"
case {"product": {"name": name, "availability": "out_of_stock", "expected_date": date}}:
return f"{name} временно отсутствует, ожидается {date}"
case {"product": {"name": name, "availability": "out_of_stock"}}:
return f"{name} отсутствует в продаже"
case {"error": error}:
return f"Не удалось получить информацию о товаре: {error}" |
|
Для обработки конфигурационных файлов в форматах JSON или YAML, паттерн-матчинг обеспечивает декларативный способ проверки структуры и извлечения нужных параметров:
Python | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| def configure_app(config):
match config:
case {"app": {"name": name, "version": version}, "logging": {"level": level}}:
setup_app(name, version)
setup_logging(level)
return f"Приложение {name} v{version} настроено с уровнем логирования {level}"
case {"app": app_config, "database": db_config, "cache": cache_config}:
setup_app([B]app_config)
setup_database([/B]db_config)
setup_cache([B]cache_config)
return "Приложение настроено с базой данных и кешем"
case {"app": app_config}:
setup_app([/B]app_config)
return "Настроено только приложение, без дополнительных сервисов"
case _:
raise ValueError("Неверная конфигурация") |
|
Матчинг словарей предоставляет элегантное решение для обработки различных версий API или форматов данных, что особенно полезно при миграции с одной версии на другую:
Python | 1
2
3
4
5
6
7
8
9
10
11
12
13
| def process_api_data(data, version=None):
match data:
case {"api_version": "v1", "user": {"full_name": name, "email": email}}:
# Обработка данных для API v1
return create_user_v1(name, email)
case {"api_version": "v2", "user": {"first_name": first, "last_name": last, "contact": {"email": email}}}:
# Обработка данных для API v2
return create_user_v2(first, last, email)
case {"user": user_data} if version == "v1":
# Данные без указания версии, но версия передана в параметрах
return create_user_v1(user_data.get("full_name"), user_data.get("email"))
case _:
raise ValueError("Неподдерживаемый формат данных API") |
|
Практические примеры
Теория хороша, но практическое применение показывает истинную силу паттерн-матчинга. Рассмотрим несколько реальных сценариев, где эта возможность Python превращает сложный код в простой и понятный.
Обработка данных API
Современные приложения часто работают с данными, получаемыми от различных API в формате JSON. Обработка таких структур — идеальный случай для паттерн-матчинга:
Python | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
| def handle_weather_api_response(response):
match response:
case {"current": {"temperature": temp, "humidity": humidity, "wind_speed": wind},
"location": {"city": city, "country": country}}:
return f"В городе {city} температура {temp}°C, влажность {humidity}%, ветер {wind} м/с"
case {"current": {"temperature": temp}, "location": {"city": city}}:
return f"В городе {city} сейчас {temp}°C"
case {"error": {"code": code, "message": msg}}:
return f"Ошибка API {code}: {msg}"
case {"error": msg} if isinstance(msg, str):
return f"Ошибка: {msg}"
case _:
return "Неизвестный формат ответа"
# Примеры использования
response1 = {
"current": {"temperature": 25, "humidity": 70, "wind_speed": 5},
"location": {"city": "Москва", "country": "Россия"}
}
response2 = {"error": {"code": 401, "message": "Unauthorized access"}}
print(handle_weather_api_response(response1))
print(handle_weather_api_response(response2)) |
|
Без паттерн-матчинга мы бы писали многократные вложенные проверки вида if "current" in response and "temperature" in response["current"]... , что сделало бы код гораздо менее читаемым.
Парсинг командной строки
Обработка аргументов командной строки — ещё одна область, где паттерн-матчинг сияет:
Python | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
| def parse_command(args):
match args:
case ["search", *terms] if terms:
query = " ".join(terms)
return f"Поиск по запросу: '{query}'"
case ["filter", field, value, *options]:
options_str = ", ".join(f"{opt}" for opt in options) if options else "по умолчанию"
return f"Фильтрация по полю '{field}' со значением '{value}', опции: {options_str}"
case ["sort", field, ("asc" | "desc") as direction]:
return f"Сортировка по '{field}' в {'возрастающем' if direction == 'asc' else 'убывающем'} порядке"
case ["export", ("csv" | "json" | "xml") as format, filename]:
return f"Экспорт в формате {format.upper()} в файл '{filename}'"
case ["config", key, value]:
return f"Установка параметра конфигурации '{key}' в значение '{value}'"
case ["help" | "-h" | "--help"]:
return "Справка по использованию программы"
case [cmd, *_] if cmd not in ["search", "filter", "sort", "export", "config", "help"]:
return f"Неизвестная команда: {cmd}"
case []:
return "Не указана команда"
# Примеры использования
print(parse_command(["search", "Python", "tutorial"]))
print(parse_command(["sort", "date", "desc"]))
print(parse_command(["export", "json", "data.json"]))
print(parse_command(["unknown", "arg1", "arg2"])) |
|
Этот подход особенно удобен для создания CLI-инструментов, где требуется интерпретировать различные форматы команд.
Обработка вложенных структур данных
Обработка сложных вложенных структур — ещё одна сильная сторона паттерн-матчинга:
Python | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
| def analyze_shopping_cart(cart):
match cart:
case {"items": [], "total": 0}:
return "Корзина пуста"
case {"items": [{"product": product, "quantity": qty, "price": price}], "total": total} if qty == 1:
return f"В корзине один товар: {product} за {price}"
case {"items": items, "total": total, "discount": discount} if discount > 0:
return f"В корзине {len(items)} товаров на сумму {total} со скидкой {discount}%"
case {"items": items, "total": total}:
count = sum(item.get("quantity", 1) for item in items)
return f"В корзине {count} товаров на общую сумму {total}"
case {"error": msg}:
return f"Ошибка при загрузке корзины: {msg}"
# Примеры
cart1 = {"items": [{"product": "Ноутбук", "quantity": 1, "price": 50000}], "total": 50000}
cart2 = {
"items": [
{"product": "Клавиатура", "quantity": 1, "price": 2000},
{"product": "Мышь", "quantity": 1, "price": 1000},
{"product": "Наушники", "quantity": 2, "price": 1500}
],
"total": 6000,
"discount": 10
}
print(analyze_shopping_cart(cart1))
print(analyze_shopping_cart(cart2)) |
|
Такой подход позволяет легко анализировать данные электронной коммерции, содержимое баз данных и другие сложные структуры.
Упрощение парсинга JSON
При работе с JSON-данными часто возникает необходимость обрабатывать разные форматы или версии API:
Python | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
| import json
def parse_notification(notification_json):
try:
data = json.loads(notification_json)
match data:
case {"notification_type": "message", "sender": sender, "content": content, "timestamp": ts}:
return f"Сообщение от {sender} в {ts}: {content}"
case {"notification_type": "friend_request", "from": sender}:
return f"Запрос в друзья от {sender}"
case {"notification_type": "system", "message": msg, "level": "warning"}:
return f"Системное предупреждение: {msg}"
case {"notification_type": "system", "message": msg, "level": "error"}:
return f"Системная ошибка: {msg}"
case {"notification_type": type_, **details}:
return f"Уведомление типа {type_} с деталями: {details}"
case _:
return "Неизвестный формат уведомления"
except json.JSONDecodeError:
return "Некорректный JSON-формат"
# Примеры
notification1 = '{"notification_type": "message", "sender": "Иван", "content": "Привет!", "timestamp": "2023-11-29 15:30:22"}'
notification2 = '{"notification_type": "system", "message": "Сервер будет перезагружен", "level": "warning"}'
notification3 = '{"notification_type": "friend_request", "from": "Мария"}'
print(parse_notification(notification1))
print(parse_notification(notification2))
print(parse_notification(notification3)) |
|
Функциональные паттерны: сопоставление результатов вызовов функций
Паттерн-матчинг отлично сочетается с функциональным стилем программирования, особенно при обработке результатов функций:
Python | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
| def fetch_data(resource_id):
# Имитация запроса к API или базе данных
if resource_id == 404:
return None
elif resource_id == 500:
return {"error": "Internal server error"}
elif resource_id < 100:
return {"status": "pending", "id": resource_id}
else:
return {"data": {"id": resource_id, "name": f"Resource-{resource_id}"}, "status": "ok"}
def process_resource(resource_id):
result = fetch_data(resource_id)
match result:
case {"data": {"name": name}, "status": "ok"}:
return f"Получен ресурс: {name}"
case {"status": "pending", "id": id}:
return f"Ресурс {id} ещё обрабатывается"
case {"error": error_msg}:
return f"Ошибка получения ресурса: {error_msg}"
case None:
return f"Ресурс с ID {resource_id} не найден"
case unexpected:
return f"Неожиданный результат: {unexpected}"
# Примеры
print(process_resource(123))
print(process_resource(50))
print(process_resource(404))
print(process_resource(500)) |
|
Этот пример показывает, как паттерн-матчинг упрощает обработку различных типов возвращаемых значений. Особенно эта техника полезна при работе с функциями, которые могут возвращать разные структуры данных в зависимости от результата операции.
Примеры рефакторинга кода
Рассмотрим пример, как паттерн-матчинг может помочь упростить существующий код:
Python | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
| # Исходный код без паттерн-матчинга
def analyze_transaction_old(transaction):
if not isinstance(transaction, dict):
return "Неверный формат транзакции"
if "status" not in transaction:
return "Отсутствует статус транзакции"
status = transaction.get("status")
if status == "completed":
if "amount" in transaction and "currency" in transaction:
return f"Транзакция завершена: {transaction['amount']} {transaction['currency']}"
else:
return "Транзакция завершена, но детали отсутствуют"
elif status == "pending":
if "estimated_completion" in transaction:
return f"Транзакция в обработке, ожидается завершение {transaction['estimated_completion']}"
else:
return "Транзакция в обработке, время завершения неизвестно"
elif status == "failed":
if "error" in transaction:
error = transaction["error"]
if isinstance(error, dict) and "code" in error and "message" in error:
return f"Ошибка транзакции {error['code']}: {error['message']}"
else:
return f"Ошибка транзакции: {error}"
else:
return "Транзакция не выполнена, причина неизвестна"
else:
return f"Неизвестный статус транзакции: {status}"
# Рефакторинг с использованием паттерн-матчинга
def analyze_transaction_new(transaction):
match transaction:
case {"status": "completed", "amount": amount, "currency": currency}:
return f"Транзакция завершена: {amount} {currency}"
case {"status": "completed"}:
return "Транзакция завершена, но детали отсутствуют"
case {"status": "pending", "estimated_completion": date}:
return f"Транзакция в обработке, ожидается завершение {date}"
case {"status": "pending"}:
return "Транзакция в обработке, время завершения неизвестно"
case {"status": "failed", "error": {"code": code, "message": message}}:
return f"Ошибка транзакции {code}: {message}"
case {"status": "failed", "error": error}:
return f"Ошибка транзакции: {error}"
case {"status": "failed"}:
return "Транзакция не выполнена, причина неизвестна"
case {"status": status}:
return f"Неизвестный статус транзакции: {status}"
case _:
return "Неверный формат транзакции" |
|
Сравнивая эти две функции, мы видим, что версия с паттерн-матчингом:
1. Короче и читабельнее.
2. Не имеет глубокой вложенности условий.
3. Структурно отражает формат данных.
4. Меньше подвержена ошибкам.
Это демонстрирует, как паттерн-матчинг может сделать код более поддерживаемым, особенно при работе со сложными структурами данных.
Обработка иерархических структур данных
Паттерн-матчинг прекрасно подходит для работы с деревьями и другими иерархическими структурами:
Python | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
| def render_html_element(element):
match element:
case {"tag": "img", "attrs": {"src": src, "alt": alt, **rest}}:
attrs_str = " ".join(f'{k}="{v}"' for k, v in rest.items())
return f'<img src="{src}" alt="{alt}" {attrs_str}>'
case {"tag": "a", "attrs": {"href": href, **attrs}, "children": children}:
attrs_str = " ".join(f'{k}="{v}"' for k, v in attrs.items())
inner = "".join(render_html_element(child) for child in children)
return f'<a href="{href}" {attrs_str}>{inner}</a>'
case {"tag": tag, "children": children, "attrs": attrs}:
attrs_str = " ".join(f'{k}="{v}"' for k, v in attrs.items())
inner = "".join(render_html_element(child) for child in children)
return f'<{tag} {attrs_str}>{inner}</{tag}>'
case {"tag": tag, "children": children}:
inner = "".join(render_html_element(child) for child in children)
return f'<{tag}>{inner}</{tag}>'
case {"tag": tag, "text": text}:
return f'<{tag}>{text}</{tag}>'
case str(text):
return text
# Пример использования
document = {
"tag": "div",
"attrs": {"class": "container"},
"children": [
{"tag": "h1", "text": "Заголовок"},
{
"tag": "p",
"children": [
"Это параграф с ",
{"tag": "a", "attrs": {"href": "https://example.com"}, "children": ["ссылкой"]}
]
},
{"tag": "img", "attrs": {"src": "image.jpg", "alt": "Изображение", "width": "100"}}
]
}
print(render_html_element(document)) |
|
Такой подход позволяет элегантно обрабатывать сложные древовидные структуры, как в этом примере с HTML-разметкой.
Обработка событий в интерфейсах приложений
Паттерн-матчинг идеально подходит для обработки событий пользовательского интерфейса. В GUI или веб-приложениях часто нужно обрабатывать различные типы событий, каждое со своей структурой данных:
Python | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
| def handle_user_interaction(event):
match event:
case {"type": "click", "element": "button", "id": id, "position": (x, y)}:
return f"Нажатие на кнопку с ID {id} в позиции ({x}, {y})"
case {"type": "input", "element": "textfield", "id": id, "value": value}:
return f"Ввод текста '{value}' в поле с ID {id}"
case {"type": "drag", "element": element, "from": (x1, y1), "to": (x2, y2)}:
dx, dy = x2 - x1, y2 - y1
return f"Перетаскивание {element} на {dx}px вправо и {dy}px вниз"
case {"type": "keypress", "key": "Enter"}:
return "Нажата клавиша Enter"
case {"type": "keypress", "key": key, "modifiers": mods} if "Ctrl" in mods:
return f"Нажата комбинация Ctrl+{key}"
case {"type": event_type}:
return f"Необработанное событие типа {event_type}"
# Примеры использования
print(handle_user_interaction({"type": "click", "element": "button", "id": "submit-btn", "position": (120, 300)}))
print(handle_user_interaction({"type": "keypress", "key": "S", "modifiers": ["Ctrl", "Shift"]})) |
|
Обработка состояний конечного автомата
Многие приложения используют конечные автоматы для отслеживания состояний. Паттерн-матчинг предоставляет элегантный способ для обработки переходов между состояниями:
Python | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
| def process_state_transition(current_state, event):
match (current_state, event):
case ("idle", "start_task"):
return "running", "Задача запущена"
case ("running", "complete"):
return "completed", "Задача успешно завершена"
case ("running", "pause"):
return "paused", "Задача приостановлена"
case ("running", "error"):
return "failed", "Задача завершилась с ошибкой"
case ("paused", "resume"):
return "running", "Задача возобновлена"
case ("paused", "cancel") | ("running", "cancel"):
return "cancelled", "Задача отменена"
case (state, event_name) if event_name not in ["start_task", "complete", "pause", "resume", "error", "cancel"]:
return state, f"Неизвестное событие: {event_name}"
case (state, _):
return state, f"Недопустимый переход из состояния {state}"
# Пример использования
state = "idle"
events = ["start_task", "pause", "resume", "complete"]
for event in events:
state, message = process_state_transition(state, event)
print(f"Событие: {event}, Новое состояние: {state}, Сообщение: {message}") |
|
Этот паттерн особенно полезен в игровом программировании, управлении бизнес-процессами и других системах, основанных на состояниях.
Парсинг DSL (Domain-Specific Language)
Паттерн-матчинг может служить основой для создания простых интерпретаторов предметно-ориентированных языков:
Python | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
| def evaluate_simple_expression(expr):
match expr:
case ["add", a, b]:
return evaluate_simple_expression(a) + evaluate_simple_expression(b)
case ["subtract", a, b]:
return evaluate_simple_expression(a) - evaluate_simple_expression(b)
case ["multiply", a, b]:
return evaluate_simple_expression(a) * evaluate_simple_expression(b)
case ["divide", a, b]:
divisor = evaluate_simple_expression(b)
if divisor == 0:
raise ValueError("Деление на ноль")
return evaluate_simple_expression(a) / divisor
case ["if", condition, then_expr, else_expr]:
if evaluate_simple_expression(condition):
return evaluate_simple_expression(then_expr)
else:
return evaluate_simple_expression(else_expr)
case ["equals", a, b]:
return evaluate_simple_expression(a) == evaluate_simple_expression(b)
case ["greater", a, b]:
return evaluate_simple_expression(a) > evaluate_simple_expression(b)
case int() | float() as num:
return num
case _:
raise ValueError(f"Неизвестное выражение: {expr}")
# Пример использования
expression = ["if",
["greater", 10, 5],
["add", 1, 2],
["multiply", 3, 4]]
result = evaluate_simple_expression(expression)
print(f"Результат: {result}") # Результат: 3 |
|
Такой подход упрощает создание интерпретаторов и компиляторов, делая код более прямолинейным.
Обработка конфигураций и настроек
Паттерн-матчинг удобен для анализа и валидации различных конфигураций:
Python | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
| def validate_database_config(config):
match config:
case {"type": "sqlite", "file": file_path}:
return f"SQLite конфигурация. Файл базы данных: {file_path}"
case {"type": "postgresql", "host": host, "port": port, "database": db, "user": user}:
return f"PostgreSQL конфигурация. Соединение: {user}@{host}:{port}/{db}"
case {"type": "mysql", "host": host, "database": db, "user": user, "password": _}:
return f"MySQL конфигурация. База данных: {db} на {host}"
case {"type": db_type, **params}:
missing = set(["host", "database", "user"]) - set(params.keys())
if missing:
return f"Неполная конфигурация для {db_type}. Отсутствуют поля: {', '.join(missing)}"
return f"Конфигурация для {db_type} с параметрами: {params}"
case _:
return "Некорректная конфигурация базы данных"
# Примеры
print(validate_database_config({"type": "sqlite", "file": "/data/app.db"}))
print(validate_database_config({"type": "postgresql", "host": "localhost", "port": 5432, "database": "app_db", "user": "admin"}))
print(validate_database_config({"type": "mysql", "host": "db.example.com", "user": "app_user"})) |
|
Этот подход особенно полезен при работе с различными типами хранилищ данных, сервисов или внешних API, где каждый тип требует своего набора параметров.
Обработка сетевых протоколов
При разработке сетевых приложений паттерн-матчинг позволяет элегантно обрабатывать различные типы сообщений протокола:
Python | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
| def process_network_message(message):
match message:
case {"type": "connect", "client_id": client_id, "protocol_version": version} if version >= 2.0:
return f"Клиент {client_id} подключился по протоколу версии {version}"
case {"type": "connect", "client_id": _, "protocol_version": version}:
return f"Отказ в подключении: устаревшая версия протокола {version}"
case {"type": "data", "message_id": msg_id, "payload": payload, "checksum": checksum}:
calculated = calculate_checksum(payload) # предполагаемая функция
if calculated == checksum:
return f"Получено сообщение #{msg_id} с данными длиной {len(payload)} байт"
return f"Ошибка контрольной суммы в сообщении #{msg_id}"
case {"type": "heartbeat", "timestamp": ts}:
return f"Получен heartbeat в {ts}"
case {"type": msg_type}:
return f"Получено неизвестное сообщение типа {msg_type}"
case _:
return "Некорректный формат сообщения" |
|
Такой подход делает обработку сетевых сообщений более понятной и менее подверженной ошибкам, особенно при наличии сложных протоколов с множеством типов сообщений.
Ограничения и альтернативы
При всей мощи и элегантности паттерн-матчинга в Python у него существуют некоторые ограничения, о которых стоит знать. Понимание этих ограничений поможет избежать неожиданностей при использовании этой функциональности и принять обоснованные решения о том, когда её применять, а когда выбрать другие подходы.
Одним из основных ограничений является невозможность использовать переменные напрямую в качестве значений в шаблонах. Если вам нужно сравнить значение с переменной из внешней области видимости, придётся использовать защитные выражения (guards):
Python | 1
2
3
4
5
6
7
8
9
10
11
| value = 42
# Не сработает как ожидается:
match data:
case value: # Здесь 'value' — новая переменная, а не ссылка на внешнюю
print("Совпадение с 42")
# Правильный способ:
match data:
case x if x == value: # Использование guard condition
print("Совпадение с 42") |
|
Ещё одним ограничением при работе со словарями является отсутствие возможности использовать оператор звёздочки (* ) для захвата неуказанных ключей, как это можно делать со списками. В качестве альтернативы можно использовать оператор распаковки словаря (** ), но это не совсем то же самое с точки зрения синтаксиса.
Для словарей также существует ограничение на использование вычисляемых ключей. Ключи в шаблонах должны быть литералами (строками, числами и т.д.), но не могут быть выражениями или переменными:
Python | 1
2
3
4
5
6
| key_name = "username"
# Не сработает:
match data:
case {key_name: value}: # Ошибка: ключи должны быть литералами
print(f"Имя пользователя: {value}") |
|
Кроме этого, типы шаблонов должны соответствовать типам данных. Например, шаблон для списка не сработает с кортежем и наоборот, даже если структура идентична:
Python | 1
2
3
4
5
6
7
| data = (1, 2, 3)
match data:
case [1, 2, 3]: # Не сработает, т.к. data — кортеж, а не список
print("Список [1, 2, 3]")
case (1, 2, 3): # Сработает
print("Кортеж (1, 2, 3)") |
|
Еще одно ограничение связано с производительностью. При очень сложных шаблонах с множеством вложенных структур и защитных выражений производительность может быть ниже, чем у оптимизированного кода с традиционными условиями. В большинстве случаев это не критично, но для производительно-критичных участков кода стоит провести бенчмаркинг.
Работа с пользовательскими классами требует понимания, что при сопоставлении с экземплярами классов без каких-либо дополнительных действий будут проверяться все атрибуты объекта, а не только те, что указаны в шаблоне. Это может привести к неожиданным результатам.
Типичной ошибкой при использовании паттерн-матчинга является неправильный порядок шаблонов. Важно помнить, что шаблоны проверяются последовательно сверху вниз, и первый подходящий шаблон прерывает дальнейшую проверку. Размещение более общих шаблонов перед более конкретными может привести к тому, что конкретные шаблоны никогда не будут достигнуты:
Python | 1
2
3
4
5
6
| # Неправильно:
match value:
case [int(), int()]: # Этот шаблон перехватит все списки из двух целых чисел
print("Список из двух целых чисел")
case [1, 2]: # Этот шаблон никогда не сработает, т.к. предыдущий уже перехватил [1, 2]
print("Конкретный список [1, 2]") |
|
Альтернативой паттерн-матчингу в различных ситуациях могут служить:
1. Традиционные условные конструкции (if-elif-else) — подходят для простых случаев или когда логика сложнее, чем просто сопоставление структуры.
2. Словари функций или методов — когда нужно выполнить различные действия в зависимости от значения:
Python | 1
2
3
4
5
6
7
8
9
| def handle_add(a, b): return a + b
def handle_subtract(a, b): return a - b
operations = {
"add": handle_add,
"subtract": handle_subtract,
}
result = operations.get(operation, lambda a, b: None)(x, y) |
|
3. Полиморфизм и наследование — когда вы работаете с объектно-ориентированным кодом и имеете иерархию классов.
4. Функции высшего порядка и функциональные комбинаторы — для обработки данных в функциональном стиле.
5. Библиотеки для обработки данных, такие как Pandas — для анализа табличных данных.
Выбор между паттерн-матчингом и альтернативами зависит от конкретной задачи, стиля программирования команды и требований к производительности и поддерживаемости кода. Часто паттерн-матчинг упрощает код и делает его более читаемым, но не является серебряной пулей для всех сценариев. При переходе на Python 3.10 и использовании паттерн-матчинга важно учитывать совместимость с предыдущими версиями языка. Если ваш код должен работать в более ранних версиях Python, вам придётся использовать альтернативные подходы или добавить условную логику, проверяющую версию Python.
Словари и списки Немного не понимаю один момент. Есть словарь, в словаре к значению присвоен список. Знаю, что к элементу списка в словаре можно обратиться как dict.... ЗАДАНИЯ НА СЛОВАРИ И СПИСКИ Составьте список товаров, имеющихся в магазине, который можно редактировать с учетом завозов. Покупатель делает онлайн заказ, набирая нужные ему... Словари и списки - социальная сеть Неделю как занимаюсь с Питоном.
Задание: Используя списки и словари установить взаимосвязи пользователей некой соц.сети.
С помощью функции... Списки,Котежи,Словари, Множества! Проблема в том ,то что всё знать невозможно..вроде разобрался со списками и котрежами,начал вникать в словари и множества,забыл списки и кортежи,что... Лабораторная работа 3 Списки. Словари Всем привет,помогите пожалуйста решить задачку на питоне
В13. «Уровень жизни»
Имеются данные о цене на хлеб в течение 12 месяцев в шести... Нужно найти ошибку в программе (списки, словари) Здравствуйте, программа должна считать число слов, которые вводит пользователь( a aa abC aa ac abc bcd a) и выводить их в виде:
ac 1
a 2
abc 2 ... Где на практике в простом приложении можно использовать словари и списки? Кодер с меня мягко говоря плохой, но читаю питон, стараюсь вникнуть. Практикую прочитанное на простой текстовой RPG, с боевой системой в стиле удар... Словари в Python Здравствуйте. Помогите, пожалуйста, с задачей:
1. Надо реализовать фун-ию make_user, которая должна принимать два параметра - имя и... Словари в Python Помогите пожалуйста! Имена и адреса электронной почты. Напишите программу, которая сохраняет имена и адреса электронной почты в словаре в виде пар... Словари Python В список data вложено n словарей с двумя парами ключ-значение. Пример, где количество словарей n = 3:
Из этих этих словарей при помощи циклов... Python. Словари dict = {"name": , "time": }
Как можно увеличить ключ name и time не вручную, а программно ? И возможно ли это вообще ?
Допустим, чтобы... Сопоставление с образцом Столкнулась с проблемой: перебила программу из книги "Мир Лиспа" Э. Хювёнен, И. Сеппянен, а компилятор постоянно выдает одну и ту же ошибку " A...
|