Функціональність pattern matching (зіставлення шаблонів) – була додана у Python починаючи з версії 3.10.
Pattern matching реалізується за допомогою конструкції match…case, котра дає змогу зіставити вираз з шаблоном:
match expression:
case pattern_1:
action_1
case pattern_2:
action_2
................
case pattern_N:
action_N
case _:
default_actionPython обчислює вираз, після чого перебирає всі варіанти шаблонів зверху донизу, поки не знайде шаблон що відповідає виразу (в такому разі буде виконано відповідну дію). При цьому, виконується перший збіг, або варіант за замовчуванням, якщо вираз не збігається з жодним шаблоном.
Базовий приклад використання:
def http_error(status):
match status:
case 400:
return 'Bad request'
case 401 | 403:
return 'Not allowed'
case 404:
return 'Not found'
case _:
return 'Something\'s wrong with the internet'
print(http_error(404)) # Not foundЄ можливість порівняти вираз з кількома варіантами значень в одному рядку, за допомогою оператора <|>. Приклад:
def http_error(status):
match status:
case 400:
return 'Bad request'
case 401 | 403:
return 'Not allowed'
print(http_error(403)) # Not allowedСамі шаблони поділяються на групи, деякі з котрих буде розглянуто далі.
Зміст:
- Literal Patterns
- Wildcard Patterns
- Capture Patterns
- Sequence Patterns
- Mapping Patterns
- Class Patterns
- Matching з об’єктами Enum
Literal Patterns
Літеральні шаблони відповідають числам (int, float, complex), рядкам, логічним (True, False) і None.
Приклад функції котра повертає назву дня тижня:
def weekday_name(weekday_num):
match weekday_num:
case 1:
return 'Monday'
case 2:
return 'Tuesday'
case 3:
return 'Wednesday'
case 4:
return 'Thursday'
case 5:
return 'Friday'
case 6:
return 'Saturday'
case 7:
return 'Sunday'
print(weekday_name(5)) # FridayWildcard Patterns
Коли потрібно вказати дію за замовчуванням, на випадок якщо не знайдено жодного збігу, можна використати шаблон підстановки, що записується у вигляді символу нижнього підкреслення _ :
def http_error(status):
match status:
case 400:
return 'Bad request'
case 401 | 403:
return 'Not allowed'
case 404:
return 'Not found'
case _:
return 'Something\'s wrong with the internet'
print(http_error(405)) # Something's wrong with the internetВикористання wildcard _ не є обов’язковим, в такому випадку просто нічого не відбудеться, якщо не буде знайдено збігів.
Capture Patterns
Шаблон захоплення дає змогу прив’язати змінну до заданого у шаблоні ім’я, та використовувати її всередині локальної області видимості:
def print_name(name):
match name.split():
case [first_name]:
print(f'First name: {first_name}')
case [first_name, last_name]:
print(f'First name: {first_name}, Last name: {last_name}')
print_name('Stuart Maxwell') # First name: Stuart, Last name: Maxwell
print_name('Rory') # First name: RoryПрив’язка значень до змінних, подібна до аргументів та їх значень у функціях.
Sequence Patterns
Дозволяють зіставляти списки, кортежі та будь-які інші об’єкти від collections.abc.Sequence, крім str, bytes, bytearray. Приклад зіставлення списків:
def process_list(value):
match value:
case []:
return 'Empty list'
case [x]:
return f'Single element list: {x}'
case [x, y]:
return f'Two elements list: {x} and {y}'
case [x, y, *_]:
return 'Longer list'
print(process_list([])) # Empty list
print(process_list([1])) # Single element list: 1
print(process_list([1, 2])) # Two elements list: 1 and 2
print(process_list([1, 2, 3])) # Longer listСимвол * використовується в шаблоні для позначення довільної довжини. Тобто в прикладі вище, шаблон [x, y, *_], буде відповідати списку, котрий містить більше ніж два елементи, незалежно від його довжини.
Mapping Patterns
Подібно до об’єктів Sequence, є можливість зіставляти словники (точніше об’єкти collections.abc.Mapping). Але на відміну від шаблонів послідовностей, зайві ключі ігноруються (тобто відбувається співставлення саме тих ключів словника, що визначені в операторі case):
def print_config(config):
match config:
case {'host': host, 'port': port}:
print(f'Host: {host}, Port: {port}')
case _:
print('No match')
config = {'host': 'localhost', 'port': 8080, 'token': 'TOKEN'}
print_config(config)
# OUTPUT: Host: localhost, Port: 8080В цьому прикладі, спрацював шаблон {‘host’: host, ‘port’: port} (рядок 3), незважаючи на відсутність в ньому ключа token.
Class Patterns
Python також дає змогу використовувати pattern matching, для зіставлення з об’єктом класу. Приклад:
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
def print_point(point):
match point:
case Point(x=0, y=0):
print('Default Point')
case Point(x=x, y=0):
print(f'Point with: x={x}')
case Point(x=0, y=y):
print(f'Point with: y={y}')
case Point(x=x, y=y):
print(f'Point with: x={x}, y={y}')
print_point(Point(0, 0)) # Default Point
print_point(Point(10, 0)) # Point with: x=10
print_point(Point(0, 10)) # Point with: y=10
print_point(Point(10, 10)) # Point with: x=10, y=10Розберемо кожний шаблон з прикладу вище окремо:
- case Point(x=0, y=0): цей шаблон строго визначає значення обох атрибутів, тобто відповідає об’єкту класу Point, котрий має атрибути x та y, що дорівнюють 0.
- case Point(x=x, y=0): суворо задає значення лише для атрибуту y. Для відповідності цьому шаблону, атрибут y повинен дорівнювати 0, атрибут x може мати довільне значення.
- case Point(x=0, y=y): суворо задає значення лише для атрибуту x, атрибут y може мати довільне значення.
- case Point(x=x, y=y): обидва атрибути можуть мати довільне значення.
Варто додати, що шаблон не є викликом конструктора Point, шаблон лише встановлює як порівнювати атрибути та їх значення.
В прикладі вище, для визначення атрибутів в шаблоні, використовується їх ім’я (наприклад case Point(x=0, y=0)). Але Python також дозволяє, використовувати позиційні аргументи в конструкції match…case. Приклад вище можна переписати наступним чином, використовуючи позиційні аргументи:
class Point:
__match_args__ = ('x', 'y')
def __init__(self, x, y):
self.x = x
self.y = y
def print_point(point):
match point:
case Point(0, 0):
print('Default Point')
case Point(x, 0):
print(f'Point with: x={x}')
case Point(0, y):
print(f'Point with: y={y}')
case Point(x, y):
print(f'Point with: x={x}, y={y}')
print_point(Point(10, 10)) # Point with: x=10, y=10Слід звернути увагу, що для використання позиційних аргументів в шаблонах, потрібно вказати їх порядок за допомогою атрибута класу __match_args__, (рядок 2). В іншому випадку буде отримано помилку:
TypeError: Point() accepts 0 positional sub-patterns (2 given)Якщо замість звичайних класів, використовуються дата-класи (про Data Classes в Python), потреби додавання атрибуту __match_args__ немає (для dataclasses він генерується автоматично). Приклад:
from dataclasses import dataclass
@dataclass
class Point:
x: int
y: int
def print_point(point):
match point:
case Point(0, 0):
print('Default Point')
case Point(x, y):
print(f'Point with: x={x}, y={y}')
print_point(Point(0, 0)) # Default Point
print_point(Point(10, 10)) # Point with: x=10, y=10Також шаблон можна поєднувати з if (при цьому якщо умова хибна, pattern matching продовжиться). Cлід додати, що спершу відбувається зіставлення з шаблоном, і тільки після цього перевіряється умова. Приклад:
from dataclasses import dataclass
@dataclass
class Point:
x: int
y: int
def print_point(point):
match point:
case Point(0, 0):
print('Default Point')
case Point(x, y) if x > 0 and y > 0:
print(f'Positive Сoordinate: x={x}, y={y}')
case _:
print('Another Сoordinate')
print_point(Point(10, 10)) # Positive Сoordinate: x=10, y=10
print_point(Point(-5, 10)) # Another Сoordinate
Matching з об’єктами Enum
Також в pattern matching Python, можливе співставлення з об’єктами Enum:
from enum import Enum
class Command(Enum):
QUIT = 0
RESET = 1
def execute(command):
match command:
case Command.QUIT:
print('Quit')
case Command.RESET:
print('Reset')
execute(Command.RESET) # Reset