Функціональність 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_action

Python обчислює вираз, після чого перебирає всі варіанти шаблонів зверху донизу, поки не знайде шаблон що відповідає виразу (в такому разі буде виконано відповідну дію). При цьому, виконується перший збіг, або варіант за замовчуванням, якщо вираз не збігається з жодним шаблоном.

Базовий приклад використання:

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

Самі шаблони поділяються на групи, деякі з котрих буде розглянуто далі.

Зміст:

  1. Literal Patterns
  2. Wildcard Patterns
  3. Capture Patterns
  4. Sequence Patterns
  5. Mapping Patterns
  6. Class Patterns
  7. 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))  # Friday

Wildcard 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


Мітки: