Structural Pattern Matching#

Python 3.10 wprowadza nową instrukcję match inspirowaną jezykami funkcyjnymi (Scala, Erlang).

Instrukcja match porównuje wartość (subject) do kilku różnych wzorców (patterns) wymienionych po etykietach case, aż znalezione zostanie dopasowanie. Każdy wzorzec (pattern) opisuje typ i strukturę akceptowanych wartości. Wzorzec może zawierać też zmienne, do których są wiązane pasujące wartości (binding).

Składnia:

match <subject_expression>:
    case <pattern_1> [<if guard>]:
        <block to execute if pattern_1 matches>
    case <pattern_n> [<if guard>]:
        <block to execute if pattern_n matches>

Wzorzec - Pattern#

Wzorzec (pattern) jest nowym elementem składni języka, który wygląda jak część wyrażenia służącego do konstrukcji obiektu, np:

  • [first, second, *rest]

  • Point2D(x, 0)

  • {id: 665, name: "John}

  • 665

Podobieństwo ze składnią służącą do konstrukcji jest zamierzone, ale dla wzorca oznacza proces odwrotny, nazywany dekonstrukcją. Dekonstrukcja umożliwia ekstrakcję elementów obiektu na podstawie wzorca.

Proces dopasowania wzorca#

Instrukcja match stara się dopasować obiekt (subject) do każdego wzorca podanego po etykiecie case. Dla pierwszego pasującego wzorca:

  • wiązane są wartości do zmiennych występujące w wzorcu

  • wykonywany jest odpowiedający etykiecie blok instrukcji

from collections import namedtuple

Point2D = namedtuple('Point2D', 'x y')
Point3D = namedtuple('Point3D', 'x y z')

def make_point_3d(pt):
    match pt:
        case (x, y):
            return Point3D(x, y, 0)
        case (x, y, z):
            return Point3D(x, y, z)
        case Point2D(x, y):
            return Point3D(x, y, 0)
        case Point3D(_, _, _):
            return pt
        case _:
            raise TypeError('a type cannot be converted to Point3D')
make_point_3d((1, 2, 3))
Point3D(x=1, y=2, z=3)
make_point_3d(Point2D(99, 45))
Point3D(x=99, y=45, z=0)

Rodzaje wzorców#

Literały#

number = 42

match number:
    case 0:
        print("Nothing")
    case 1:
        print("Just one")
    case 2:
        print("A couple")
    case -1:
        print("One less than nothing")
    case 1-1j:
        print("Good luck with that...")

Wzorce przechwyceń#

Wprowadzenie nazwy zmiennej we wzorcu pozwala przypisać tej zmiennej odpowiednią wartość (w przypadku dopasowania):

greeting = "John"

match greeting:
    case "":
        print("Hello stranger!")
    case name:
        print(f"Hello {name}")
Hello John

W danym wzorcu określona nazwa może wystąpić tylko raz!

data = [1, 4]

match data:
    case [x, x]:
        print(x)
  Cell In[6], line 4
    case [x, x]:
             ^
SyntaxError: multiple assignments to name 'x' in pattern

Symbol zastępczy#

Symbol _ jest specjalnym znakiem oznaczającym wzorzec, który zawsze pasuje ale nie powoduje wiązania z wartością:

data = [42, 665]

match data:
    case [_]:
        print("A list with just one element")
    case [_, _]:
        print("A list with two elements")
A list with two elements

Stałe i wyliczenia#

from enum import Enum

class Guitar(str, Enum):
    STRATOCASTER = "Stratocaster"
    TELECASTER = "Telecaster"
    LES_PAUL = "Les-Paul"

my_guitar = Guitar.STRATOCASTER

match my_guitar:
    case Guitar.LES_PAUL: # compares my_guitar == Guitar.LES_PAUL
        print("I have guitar with humbuckers")
    case fender:
        print(f"I have a {fender} guitar")
I have a Stratocaster guitar

Wzorce sekwencji#

Wzorce sekwencji mają tą samą semantykę co rozpakowanie przypisania (działają zarówno dla krotek jak i list).

collection = [1, 2, [3, 4, 5]]

match collection:
    case 1, x, [y, *others]:
        print(f"Got 1 , {x} , [{y} , {others}]")
Got 1 , 2 , [3 , [4, 5]]

Symbol zastępczy _ może być użyty w połączeniu z * w celu określenia zmiennej długości:

  • [*_] - pasuje do sekwencji o dowolnej długości

  • (_, _, *_) - pasuje do sekwencji o długości równej dwa lub większej

  • ['a', *_, 'z'] - pasuje do sekwencji dowolnej długości zaczynającej się od 'a' i kończącej się na 'z'

Wzorce słownikowe#

Dopasowywana wartość (subject) musi być instancją typu collections.abc.Mapping. Dodatkowe klucze są pomijane nawet gdy we wzorcu nie został użyty symbol **rest.

config = { 'url': "http://localhost", 'port': 8080, 'timeout': 60 }

match config:
    case {'url': url, 'port': port}:
        print(f"Connecting to {url}:{port}")
    case {}:
        print("Connection not configured...")
Connecting to http://localhost:8080

Wzorce klas#

Umożliwiają dopasowanie na podstawie typu (odpowiednik isinstance()) i destrukturyzację obiektów. Dostępne są dwie opcje dopasowań:

  • z wykorzystaniem pozycji np. Point(1, 2) - dla danej klasy wymagany jest atrybut __match_args__

  • z wykorzystaniem nazw np. Point(x=1, y=2)

from dataclasses import dataclass
from typing import Tuple

@dataclass
class Shape:
    coord: Tuple[int, int]

@dataclass
class Circle(Shape):
    radius: int
    
class Rectangle(Shape):
    __match_args__ = ('coord', 'width', 'height') # required for positional pattern matching

    def __init__(self, coord, width, height):
        super().__init__(coord)
        self.height = height
        self.width  = width

shp = Rectangle((0, 0), 90, 665)

match shp:
    case Circle(coord, r):
        print(f"Drawing circle with radius={r} at {coord}")
    case Rectangle(_, w, h):
        print(f"Drawing rectangle with width={w} and height={h}")
    case Shape(_):
        print(f"Drawing a shape!")
Drawing rectangle with width=90 and height=665

Łączenie wielu wzorców (wzorce z OR)#

Alternatywne wzorce mogą być połączone w jeden za pomocą |. Takie połączenie oznacza, że cały wzorzec zostaje dopasowany, jeśli przynajmniej jedna z alternatyw pasuje.

Alternatywne wzorce są dopasowywane od lewej do prawej i mają właściwość short-circuit.

something = "something"

match something:
    case 0 | 1 | 2:
        print("small number")
    case [] | [_]:
        print("a short sequence")
    case str() | bytes():
        print("something string-like")
    case _:
        print("something else")
something string-like

Wzorce z warunkiem#

Każdy z wzorców umieszczonych na początku instrukcji match może zawierać warunek (guard) w postaci wyrażenia if.

MAX_SIZE = 100

coord = (88, 88)

match coord:
    case x, y if x > MAX_SIZE and y > MAX_SIZE:
        print("Both coordinates out of bounds")
    case x, y if x > MAX_SIZE or y > MAX_SIZE:
        print("one coordinate out of bounds")
    case x, y if x == y:
        print("Pixel with x coordinate the same as y")
    case _:
        print(f"Pixel at {coord}")
Pixel with x coordinate the same as y