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