Obsah
1. Nejdůležitější novinka v Pythonu 3.10: strukturální pattern matching
2. SNOBOL – jazyk založený na práci se vzorky
3. Strukturální pattern matching v programovacím jazyku ML
4. Strukturální pattern matching v Rustu
5. Strukturální pattern jazyk v jazyku Coconut: první krok k Pythonu
6. Strukturální pattern matching v programovacím jazyku Python verze 3.10
7. Překlad a instalace Pythonu 3.10
8. Nejjednodušší příklad – rozeskok
9. Větší množství hodnot v každé větvi
10. Zachycení hodnoty proměnné v rozhodovací větvi
11. Podmínka v rozhodovací větvi, větev „else“
12. Test obsahu n-tic a seznamů
13. n-tice a podmínky pro hodnoty prvků
14. Zpracování příkazů či strukturovaných textových souborů
15. Proměnné části víceslovních příkazů
16. Vnořené řídicí struktury match
17. Strukturální pattern matching a objekty
18. Příloha: část gramatiky Pythonu s pattern matchingem
19. Repositář s demonstračními příklady
1. Nejdůležitější novinka v Pythonu 3.10: strukturální pattern matching
V relativně nedávno vydaném Pythonu 3.10 se objevila dlouho očekávaná novinka – takzvaný strukturální pattern matching. Jedná se o novou programovou konstrukci, která vzdáleně připomíná konstrukci switch-case z jazyka C (odkud byla převzata do dalších programovacích jazyků, včetně C++ či Javy). Ovšem strukturální pattern matching nabízí programátorům mnohem více možností než původní jednoduchá konstrukce switch-case. Některé z těchto možností si ukážeme v navazujícím textu a pro úplnost si řekneme, jak a kde se pattern matching vlastně začal v informatice používat. Poměrně zajímavá by pro programátory mohla být i zmínka o jazyce Coconut, v němž se pattern matching objevil již před relativně dlouhou dobou a navíc je tento v mnoha ohledech zajímavý programovací jazyk transpilován do Pythonu.
2. SNOBOL – jazyk založený na práci se vzorky
Prvním významným jazykem s pattern matchingem (kde je dokonce pattern matching ústřední prvek jazyka) je jazyk SNOBOL (StriNg Oriented and symBOlic Language). Bellovy laboratoře jsou v oblasti elektroniky a samozřejmě taktéž informatiky velmi známou institucí. Právě v Bellových laboratořích byl zkonstruován první tranzistor (William Shockley, Gerald Pearson, John Bardeen, Walter Brattain), vytvořena první verze dodnes široce používaného programovacího jazyka C (Dennis Ritchie) a v neposlední řadě taktéž funkční prototyp operačního systému UNIX (Ken Thompson, Dennis Ritchie, Brian Kernighan, Douglas McIlroy, Joe Ossanna a další). Kromě těchto známých projektů, z nichž například vynález tranzistoru byl odměněn Nobelovou cenou, však v Bellových laboratořích vzniklo i poměrně velké množství dalších více či méně úspěšných projektů. Jedním z těchto dnes již poněkud méně známých projektů je i programovací jazyk SNOBOL, který byl v Bellových laboratořích vyvíjen v průběhu let 1962 až 1967. Jeho autory jsou David J. Farber, Ralph E. Griswold a Ivan P. Polonsky.
Obrázek 1: Učebnice SNOBOLu, kterou napsali mj. i dva z autorů tohoto z dnešního pohledu poněkud zvláštního jazyka.
Syntaxe programovacího jazyka SNOBOL je velmi jednoduchá, protože tento jazyk ve své základní podobě neobsahuje žádné podmíněné příkazy, programové smyčky ani jiné jazykové konstrukce známé z jiných (strukturovaných) jazyků. Namísto toho se struktura programů nejvíce podobá zápisu stavů a přechodů konečného automatu, což je ostatně při zpracovávání řetězců (či jiných dat) jeden ze základních nástrojů prakticky každého programátora. Nejprve si popišme základní syntaxi programovacího jazyka SNOBOL. Na každém programovém řádku se může nacházet až pět bloků, z nichž ovšem žádný není povinný:
- návěští (label)
- subjekt (subject)
- vzor (pattern)
- objekt (object)
- odkaz(y) na jiné příkazy (goto)
V případě, že se neprovádí vyhledávání řetězců podle zadaného vzoru, ani náhrada části řetězců za jiné řetězce, obsahuje programový řádek pouze trojici bloků:
- návěští (label)
- příkaz (ostatement)
- odkaz(y) na jiné příkazy (goto)
Nepovinné návěští (to je použito jako cíl skoku nebo skoků) je od příkazu odděleno mezerou, podmíněné či nepodmíněné odkazy jsou od příkazu odděleny dvojtečkou. V programech je možné používat běžné aritmetické výrazy (jejich části se od sebe musí oddělovat mezerou), proměnné (s automatickými konverzemi mezi celočíselným typem, reálným číslem a řetězcem) i některé speciální proměnné, jejichž použití ve skutečnosti vede k zavolání nějaké funkce operačního systému. Mezi speciální proměnné patří OUTPUT (standardní výstup) a INPUT (standardní vstup). Přiřazení nějaké hodnoty do proměnné OUTPUT způsobí její tisk na standardní výstup, naopak čtení z proměnné INPUT odpovídá načtení řetězce ze standardního vstupu (pojmy „standardní vstup“ a „standardní výstup“ nemusí odpovídat tomu, co si pod těmito souslovími představujeme v Unixu, neboť SNOBOL se používal a dokonce někdy dodnes používá na velkém množství různých platforem, které mají vstupně/výstupní část řešenou odlišným způsobem).
Příklad zápisu programu ve SNOBOLu:
BEGIN LINE = INPUT &ANCHOR = 1 NUM = SPAN("0123456789") TERM = ANY("XYZ") OP = ANY("+-*/") EXPR = TERM | *EXPR OP TERM LINE EXPR "=" NUM :S(END) OUTPUT = LINE :(BEGIN) END
Obrázek 2: Další učebnice SNOBOLu, opět napsaná jedním z autorů tohoto jazyka.
3. Strukturální pattern matching v programovacím jazyku ML
Koncept pattern matchingu, který byl představen v šedesátých letech ve SNOBOLu, by dále rozvinut v programovacím jazyku ML, odkud se dále rozšířil do dalších programovacích jazyků: na přímé linii především do jazyka Caml a OCamp (a vlastně i do F#, který je v přímé linii ML) a na nepřímé linii do Rustu, Pythonu 3.10 atd. Podívejme se nyní ve stručnosti na praktické použití této programovací techniky v ML.
Obecný zápis funkce s pattern matchingem vypadá následovně:
fun <jméno> <vzorek> = <tělo/výraz>
Většinou se však v praxi používá větší množství vzorků, které jsou spojeny znakem „|“ neboli „or“. V takovém případě jsou vzorky postupně procházeny a pokud budou vyhovovat předaným datům, bude příslušná větev funkce vykonána:
fun <jméno> <vzorek> = <tělo/výraz> | <jméno> <vzorek> = <tělo/výraz> | <jméno> <vzorek> = <tělo/výraz> | <jméno> <vzorek> = <tělo/výraz>
Naproti tomu příkladem běžné funkce bez pattern matchingu je funkce length v této podobě (pro seznamy):
fun length(x) = if null(x) then 0 else 1 + length(tl(x));
V této funkci se provede vždy jedna z větví na základě zapsané podmínky (case analysis). Ovšem díky existenci pattern matchingu lze stejnou funkci zapsat odlišně – a to vyjmenováním invariantů:
fun length([]) = 0 | length(a::x) = 1 + length(x)
Tímto zápisem vlastně popisujeme, že pro prázdný seznam je délka rovna nule a pro seznam skládající se z hlavy a těla je délka tohoto seznamu rovna délce těla + 1 (což je elegantní rekurzivní vyjádření nějaké vlastnosti, s níž se velmi často setkáme v matematice).
Podobným způsobem lze zapsat výpočet Fibonacciho řady, tentokrát se třemi větvemi:
fun fib 0 = 0 | fib 1 = 1 | fib n = fib (n - 1) + fib (n - 2); fib 0; fib 1; fib 10;
Prakticky stejným způsobem lze zapsat výpočet v jazyku F#, který je, jak již ostatně víme, potomkem ML:
let rec fib n = match n with | 0 -> 0 | 1 -> 1 | n -> fib(n-1) + fib(n-2) printfn "Fib 0=%i" (fib 0) printfn "Fib 1=%i" (fib 1) printfn "Fib 10=%i" (fib 10)
Vraťme se k jazyku ML. Existují i zajímavější vzorky. Například funkci car vracející hlavu seznamu (první prvek) lze zapsat opět vyjmenováním invarianty:
fun car(x::y) = x;
V tomto případě ML rozezná, že nejsou pokryty všechny možné varianty a vypíše varování:
> val car = fn: ∀ 'a . 'a list → 'a; WARN: Pattern matching is not exhaustive.
To, zda tato funkce pracuje podle očekávání, si můžeme snadno ověřit:
car([1,2,3]); > val it = 1: int; car(["foo", "bar"]); > val it = "foo": string; car(["first"]); > val it = "first": string; car([]); Uncaught SML exception: Match
Obrázek 3: Otestování výše uvedené funkce ve webovém prostředí.
Na posledním příkladu je patrné, že ML měl pravdu – kód funkce nepokrývá všechny okrajové podmínky. Náprava je snadná, protože stačí přidat vzorek pro prázdný seznam na vstupu:
fun car([]) = 0 | car(x::y) = x; > val car = fn: int list → int; car([1,2,3]); > val it = 1: int; car([]); > val it = 0: int;
Nebo ještě lépe – vyhodíme výjimku:
fun car nil = raise Empty | car(x::y) = x; > val car = fn: ∀ 'a . 'a list → 'a; car([1,2,3]); > val it = 1: int; car([]); Uncaught SML exception: Empty
4. Strukturální pattern matching v Rustu
Programovací jazyk Rust sice nepodporuje (ostatně stejně jako Python a vlastně i programovací jazyk Go) klasickou céčkovskou rozhodovací konstrukci typu switch-case (kterou převzala a jen mírně rozšířila Java atd.), což však ve skutečnosti není žádná škoda, protože namísto toho nabízí mnohem robustnější konstrukci match, kterou lze taktéž použít pro volbu některé větve kódu, jenž se má provést. Nejprve se bez dlouhých popisů podívejme na způsob zápisu této konstrukce v její nejjednodušší podobě, tedy při klasickém rozvětvení:
fn main() { let x:i32 = 1; match x { 0 => println!("zero"), 1 => println!("one"), 2 => println!("two"), 3 => println!("three"), _ => println!("something else"), } }
Vidíme, že v konstrukci match je několik větví, přičemž každá z těchto větví začíná porovnávanou hodnotou a výraz či příkaz je uveden za šipkou tvořenou znaky =>. Poslední řádek obsahuje symbol _, který vlastně nahrazuje větev default. Ve skutečnosti je však sémantika tohoto symbolu poněkud odlišná, protože před šipkou ⇒ je uveden takzvaný vzorek (pattern) a nikoli pouhopouhá konstanta, což si ostatně ukážeme na dalších příkladech.
V době překladu zdrojového kódu překladač Rustu kontroluje, zda je _ použit, protože je vyžadováno, aby větve v konstrukci match pokryly všechny možné hodnoty testovaného výrazu. Zkusme přeložit následující příklad:
fn main() { let x:i32 = 1; match x { 0 => println!("zero"), 1 => println!("one"), 2 => println!("two"), 3 => println!("three"), } }
Podle očekávání skončí pokus o překlad tohoto zdrojového kódu s chybou:
Compiling playground v0.0.1 (/playground) error[E0004]: non-exhaustive patterns: `i32::MIN..=-1_i32` and `4_i32..=i32::MAX` not covered --> src/main.rs:4:11 | 4 | match x { | ^ patterns `i32::MIN..=-1_i32` and `4_i32..=i32::MAX` not covered | = note: the matched value is of type `i32` help: ensure that all possible cases are being handled by adding a match arm with a wildcard pattern, a match arm with multiple or-patterns as shown, or multiple match arms | 8 ~ 3 => println!("three") 9 ~ i32::MIN..=-1_i32 | 4_i32..=i32::MAX => todo!(), | For more information about this error, try `rustc --explain E0004`. error: could not compile `playground` due to previous error
Podobně, jako tomu bylo u programové konstrukce if-else či u programového bloku {}, je i konstrukce match považována za výraz, nikoli za příkaz. To znamená, že celou konstrukci match je možné použít v nějakém výpočtu, uvnitř jiného výrazu atd. V dalším demonstračním příkladu je deklarována funkce nazvaná classify, které se předá celočíselná hodnota a funkce vrátí konstantní řetězec, který tuto hodnotu popisuje. Tato funkce obsahuje ve svém bloku jediný výraz a tím je match (proto ostatně za složenou závorkou není středník):
fn classify(x:i32) -> &'static str { match x { 0 => "zero", 1 => "one", 2 => "two", 3 => "three", _ => "something else", } } fn main() { for x in 0..10 { println!("{}:{}", x, classify(x)) } }
Po překladu a spuštění dostaneme na standardním výstupu tyto zprávy:
0:zero 1:one 2:two 3:three 4:something else 5:something else 6:something else 7:something else 8:something else 9:something else
Fibonacciho řadu lze vypočítat rekurzivně, taktéž s matchingem:
fn fib(n: u32) -> u32 { match n { 0 | 1 => 1, _ => fib(n - 1) + fib(n - 2), } } fn main() { for x in 0..10 { println!("{}:{}", x, fib(x)) } }
Použití rozsahu:
match number { 1 => println!("One!"), 2 | 3 | 5 | 7 | 11 => println!("This is a prime"), 13..=19 => println!("A teen"), _ => println!("Ain't special"), }
5. Strukturální pattern jazyk v jazyku Coconut: první krok k Pythonu
Funkcionální jazyk Coconut, s nímž jsme se seznámili v článcích [1] a [2] (a jenž je transpřekládán právě do Pythonu), taktéž podporuje pattern matching, jenž je ostatně poměrně běžný i v dalších funkcionálních jazycích. Pattern matching je v Coconutu realizován jazykovou konstrukcí používající nová klíčová slova case a match. V nejjednodušším případě dokážeme pomocí těchto slov nahradit poměrně špatně čitelnou konstrukci if-elif-elif…-else:
def factorial_variant_A(n): case n: match 0: return 1 match 1: return 1 match x: return x * factorial_variant_A(x-1) else: raise TypeError("expecting integer >= 0")
V tomto případě se bude interpret (resp. přesněji řečeno transpřekladač) snažit najít ten vzor (pattern), který nejlépe a jednoznačně odpovídá vstupu, tj. hodnotě proměnné n. Postupně se testují vzorky 0, 1 a jakákoli jiná hodnota.
Povšimněte si toho, že se na třetím řádku match x objevuje x, což je proměnná platná jen v rámci dané větve a to (samozřejmě) pouze v tom případě, že se tato větev bude provádět (do této proměnné se zachytí konkrétní hodnota n).
Ve skutečnosti se však mohou v části match použít i další klauzule, zejména klauzule s podmínkou. Předchozí příklad nepracuje korektně pro záporná čísla, takže program opravíme – doplníme příslušnou podmínku:
def factorial_variant_B(n): case n: match 0: return 1 match 1: return 1 match x if x > 1: return x * factorial_variant_B(x-1) else: raise TypeError("expecting integer >= 0")
Zde klademe na třetí vzorek další podmínku, takže mu nebude odpovídat úplně jakákoli hodnota proměnné n, ale pouze hodnota větší než 1.
Ani tato úprava však nezajistí zcela korektní chování. Ostatně si sami vyzkoušejte, co se stane při volání s hodnotou 1.5. Přidáme tedy další klauzuli na kontrolu typu s využitím operátoru is:
def factorial_variant_C(n): case n: match 0: return 1 match 1: return 1 match x is int if x > 1: return x * factorial_variant_C(x-1) else: raise TypeError("expecting integer >= 0")
Ukažme si nyní některé poněkud složitější příklady použití pattern matchingu v programovacím jazyku Coconut. Například funkci, která rozhodne o tom, jak velký seznam je jí předán:
def type(x): case x: match [a, b]: return "list, 2 items" match [a]: return "list, 1 item" match []: return "empty list"
Otestování vzájemného vztahu dvou hodnot ve dvojici (seznamu, n-tici):
def pair(p): case p: match [x,x]: return "same values!" match [x,y] if x>y: return "1st value is greater" match [x,y]: return "2nd value is greater" else: return "other"
V případě, že se další funkci předá řetězec začínající na „My name is “, vezme se z něj další text a přiřadí do proměnné name:
def say_hello(s): case s: match "My name is " + name: return "Hi " + name
Specifikovat je možné začátek i konec řetěze (ovšem ne regulárním výrazem):
def get_name(s): case s: match name + "@root.cz": return name def say_hello2(s): case s: match "My name is " + name + ".": return "Hi " + name
6. Strukturální pattern matching v programovacím jazyku Python verze 3.10
Určitá – a nutno říci, že dosti zjednodušená – varianta výše uvedeného strukturálního pattern matchingu byla přidána i do Pythonu 3.10. V té nejjednodušší variantě se můžeme na strukturální pattern matching dívat jako na variantu programové konstrukce switch, která doposud v Pythonu chyběla. Ovšem již na následujícím příkladu je patrné, že možnosti mohou být poněkud větší (a to se jedná a triviální příklad použití):
def fib(value): match value: case 0: return 0 case 1: return 1 case n if n>1: return fib(n-1) + fib(n-2) case _ as wrong: raise ValueError("Wrong input", wrong) for n in range(0, 11): print(n, fib(n)) fib(-1)
7. Překlad a instalace Pythonu 3.10
Vzhledem k tomu, že Python 3.10 prozatím nebývá nabízen standardními distribucemi Linuxu, provedeme si jeho lokální překlad. Nejedná se o nic složitého (otestováno na Fedoře a Mintu).
Nejdříve je nutné nainstalovat tooling jazyka C a některé použité knihovny:
$ sudo dnf install wget yum-utils make gcc openssl-devel bzip2-devel libffi-devel zlib-devel
Dále si stáhněte zdrojové kódy Pythonu 3.10 (zde konkrétně se jedná o 3.10.6):
$ wget https://www.python.org/ftp/python/3.10.6/Python-3.10.6.tgz
Rozbalte stažený tarball:
$ tar xzf Python-3.10.6.tgz
Následuje přesun do adresáře se zdrojovými kódy a konfigurace na základě možností poskytovaných operačním systémem:
$ cd Python-3.10.6 $ ./configure --with-system-ffi --with-computed-gotos --enable-loadable-sqlite-extensions
Překlad je proveden příkazem make, kterému je vhodné předat počet paralelně běžících úloh. Ten by měl odpovídat počtu procesorových jader (například 8):
$ make -j ${nproc}
Tedy konkrétně pro osm jader:
$ make -j 8
Výsledkem je spustitelný soubor s interpretrem Pythonu, který má velikost přibližně 24MB (ovšem v závislosti na použité architektuře):
Python-3.10.6$ ls -l -h python -rwxrwxr-x 1 ptisnovs ptisnovs 24M Aug 25 12:55 python
Tato velikost je až směšně velká (a to i na dnešní poměry), ovšem lze ji zmenšit do poněkud rozumnější podoby:
Python-3.10.6$ strip python Python-3.10.6$ ls -l -h python -rwxrwxr-x 1 ptisnovs ptisnovs 3,9M Aug 25 12:57 python
Nově přeložený interpret si můžeme ihned spustit:
Python-3.10.6$ ./python Python 3.10.6 (main, Aug 25 2022, 12:55:31) [GCC 9.4.0] on linux Type "help", "copyright", "credits" or "license" for more information. >>>
Popř. si můžete právě přeložený interpret Python 3.10 nainstalovat do systému příkazem:
$ sudo make altinstall
Potom je spuštění mnohem jednodušší:
$ python3.10 Python 3.10.6 (main, May 20 2022, 04:51:23) [GCC 11.3.1 20220421 (Red Hat 11.3.1-2)] on linux Type "help", "copyright", "credits" or "license" for more information. >>>
8. Nejjednodušší příklad – rozeskok
Vraťme se nyní k problematice pattern matchingu v Pythonu. Pokusíme se nahradit následující konstrukci (mimochodem se slavnou otázkou, kterou jsem vlastně nikdy nepochopil :-) za rozumnější řešení:
print("Not ready reading drive A") def abort_retry_fail(): response = input("Abort, Retry, Fail? ") if response == "a": return "Abort" elif response == "r": return "Retry" elif response == "f": return "Fail" else: return "Wrong response" print(abort_retry_fail())
V tomto případě je možné sekvenci if-elif-elif-else nahradit mapou:
print("Not ready reading drive A") def abort_retry_fail(): response = input("Abort, Retry, Fail? ") commands = { "a": "Abort", "r": "Retry", "f": "Fail" } return commands.get(response, "Wrong response") print(abort_retry_fail())
Nás ovšem bude zajímat především nová konstrukce match. Tu lze použít následovně:
print("Not ready reading drive A") def abort_retry_fail(): response = input("Abort, Retry, Fail? ") match response: case "a": return "Abort" case "r": return "Retry" case "f": return "Fail" case _: return "Wrong response" print(abort_retry_fail())
print("Not ready reading drive A") def abort_retry_fail(): response = input("Abort, Retry, Fail? ") match response: case "a": return "Abort" case "r": return "Retry" case "f": return "Fail" print(abort_retry_fail())
V případě neočekávaného vstupu se v tomto případě vrátí hodnota (nebo raději symbol představující neexistující hodnotu) None.
9. Větší množství hodnot v každé větvi
Nyní se pokusme předchozí demonstrační příklady rozšířit takovým způsobem, aby skript reagoval nejenom na odpovědi „a“, „r“ a „f“, ale například i na zápis odpovědí velkými písmeny, na zápis celých slov atd. Ve „starém“ Pythonu spočívá jedno možné řešení ve využití množinové operace in použité ve větvích in a elif:
print("Not ready reading drive A") def abort_retry_fail(): response = input("Abort, Retry, Fail? ") if response in {"a", "A", "Abort", "abort"}: return "Abort" elif response in {"r", "R"}: return "Retry" elif response in {"f", "F"}: return "Fail" else: return "Wrong response" print(abort_retry_fail())
Nová programová konstrukce match-case umožňuje v každé větvi case použít operátor „or“, který je zapisovaný znakem „|“. Zápis je tak relativně stručný, což je ostatně patrné i z následující ukázky:
print("Not ready reading drive A") def abort_retry_fail(): response = input("Abort, Retry, Fail? ") match response: case "a" | "A": return "Abort" case "r" | "R": return "Retry" case "f" | "F": return "Fail" case _: return "Wrong response" print(abort_retry_fail())
10. Zachycení hodnoty proměnné v rozhodovací větvi
Následující příklad je prozatím poněkud umělý. Je v něm ukázáno, jak lze v zápisu vzorku ve větvi case zachytit hodnotu (nebo hodnoty). Jedná se o poslední větev typu „else“, kde vstupní hodnotu zachytíme do lokální proměnné x, kterou dále v této větvi použijeme:
print("Not ready reading drive A") def abort_retry_fail(): response = input("Abort, Retry, Fail? ") match response: case "a" | "A": return "Abort" case "r" | "R": return "Retry" case "f" | "F": return "Fail" case _ as x: return f"Wrong response {x}" print(abort_retry_fail())
Výše uvedený příklad je umělý z toho důvodu, že v tomto konkrétním případě můžeme přímo použít proměnnou response. Ovšem můžeme ho snadno upravit do podoby, v němž žádná taková proměnná nebude existovat:
print("Not ready reading drive A") def abort_retry_fail(): match input("Abort, Retry, Fail? "): case "a" | "A": return "Abort" case "r" | "R": return "Retry" case "f" | "F": return "Fail" case _ as x: return f"Wrong response {x}" print(abort_retry_fail())
11. Podmínka v rozhodovací větvi, větev „else“
Vyzkoušejme si nyní zapsat rekurzivní výpočet faktoriálu s využitím pattern matchingu. První varianta může vypadat následovně – až na nepatrně odlišnou rekurzi se jedná o stejný kód, jako tomu bylo v příkladu s Fibonacciho posloupností (povšimněte si, že x odpovídá libovolné hodnotě, přičemž hodnoty 0 a 1 se zde již nebudou vyskytovat):
def factorial(n): match n: case 0: return 1 case 1: return 1 case x: return x * factorial(x-1) for i in range(0, 10): print(i, factorial(i))
Ve skutečnosti ovšem výpočet nebude korektní pro záporné vstupy. Proto do struktury match dopíšeme podmínku omezující hodnotu zachycené proměnné x. To Python umožňuje – viz podtrženou část zdrojového kódu – a výsledkem je tento skript:
def factorial(n): match n: case 0: return 1 case 1: return 1 case x if x>1: return x * factorial(x-1) case _: raise TypeError("expecting integer >= 0") for i in range(-1, 10): try: print(i, factorial(i)) except Exception as e: print(e)
Můžeme dokonce deklarovat (prozatím nepříliš elegantně), jakého typu má hodnota být. Později si ukážeme mnohem lepší řešení:
def factorial(n): match n: case 0: return 1 case 1: return 1 case x if isinstance(x, int) and x>1: return x * factorial(x-1) case _: raise TypeError("expecting integer >= 0") for i in range(-1, 10): try: print(i, factorial(i)) except Exception as e: print(e) try: print(factorial(3.14)) except Exception as e: print(e) try: print(factorial("hello")) except Exception as e: print(e)
Nakonec spojíme první dvě větve, protože se vrací stejná hodnota:
def factorial(n): match n: case 0 | 1: return 1 case x if isinstance(x, int) and x>1: return x * factorial(x-1) case _: raise TypeError("expecting integer >= 0") for i in range(-1, 10): try: print(i, factorial(i)) except Exception as e: print(e) try: print(factorial(3.14)) except Exception as e: print(e) try: print(factorial("hello")) except Exception as e: print(e)
12. Test obsahu n-tic a seznamů
V dalším demonstračním příkladu je ukázáno, jak lze v konstrukci match testovat nikoli pouze jedinou hodnotu, ale dvojici hodnot předaných v n-tici (tedy zde konkrétně pochopitelně ve dvojici). Automat, který je schován za pattern matcherem, správně rozpozná všechny čtyři možné situace, které mohou nastat:
def test_number(value): match value: case (0, 0): print("Zero") case (real, 0): print(f"Real number {real}") case (0, imag): print(f"Imaginary number {imag}") case (real, imag): print(f"Complex number {real}+i{imag}") case _: raise ValueError("Not a complex number") test_number((0,0)) test_number((1,0)) test_number((0,1)) test_number((1,1))
Vše si můžeme otestovat:
Zero Real number 1 Imaginary number 1 Complex number 1+i1
Úprava pro seznamy je triviální:
def test_number(value): match value: case [0, 0]: print("Zero") case [real, 0]: print(f"Real number {real}") case [0, imag]: print(f"Imaginary number {imag}") case [real, imag]: print(f"Complex number {real}+i{imag}") case _: raise ValueError("Not a complex number") test_number([0,0]) test_number([1,0]) test_number([0,1]) test_number([1,1])
13. n-tice a podmínky pro hodnoty prvků
Výše uvedený demonstrační příklad můžeme rozšířit o stanovení podmínek, které musí splňovat prvek nebo prvky n-tice. Například můžeme rozlišit mezi kladným a záporným reálným číslem či kladným a záporným imaginárním číslem. Tyto podmínky se zapíšou následovně:
def test_number(value): match value: case (0, 0): print("Zero") case (real, 0) if real>0: print(f"Positive real number {real}") case (real, 0): print(f"Negative real number {real}") case (0, imag) if imag<0: print(f"Negative imaginary number {imag}") case (0, imag): print(f"Negative imaginary number {imag}") case (real, imag): print(f"Complex number {real}+i{imag}") case _: raise ValueError("Not a complex numbe") test_number((0,0)) test_number((1,0)) test_number((-1,0)) test_number((0,1)) test_number((0,-1)) test_number((1,1))
S výsledky:
Zero Positive real number 1 Negative real number -1 Negative imaginary number 1 Negative imaginary number -1 Complex number 1+i1
Pro úplnost si ukažme úpravu příkladu pro prvky seznamů a nikoli jen n-tic:
def test_number(value): match value: case [0, 0]: print("Zero") case [real, 0] if real>0: print(f"Positive real number {real}") case [real, 0]: print(f"Negative real number {real}") case [0, imag] if imag<0: print(f"Negative imaginary number {imag}") case [0, imag]: print(f"Negative imaginary number {imag}") case [real, imag]: print(f"Complex number {real}+i{imag}") case _: raise ValueError("Not a complex number") test_number([0,0]) test_number([1,0]) test_number([-1,0]) test_number([0,1]) test_number([0,-1]) test_number([1,1])
14. Zpracování příkazů či strukturovaných textových souborů
Poměrně často se můžeme setkat s požadavkem na zpracování víceslovních příkazů nebo (častěji) strukturovaných textových souborů, kde jednotlivé řádky začínají několika slovy či znaky určujícími význam dalšího textu (neboli – svět se zdaleka neotáčí jen okolo XML a JSONů). I v takovém případě můžeme využít pattern matching. Zcela nejjednodušší je varianta, ve které je seznam slov předem daný a není velký. Tehdy lze využít mapu nebo jednoduché rozvětvení:
def perform_command(): response = input("> ") match response: case "quit": return "Quit" case "list employees": return "List employees" case "list departments": return "List departments" case "list rooms": return "List rooms" case _: return "Wrong command" print(perform_command())
Jednodušší bývá rozdělení vstupu na jednotlivá slova. V takovém případě nám opět pomůže pattern matching, tentokrát při hledání prvků v seznamech:
def perform_command(): response = input("> ") match response.split(): case ["quit"]: return "Quit" case ["list", "employees"]: return "List employees" case ["list", "departments"]: return "List departments" case ["list", "rooms"]: return "List rooms" case _: return "Wrong command" print(perform_command())
15. Proměnné části víceslovních příkazů
Některá slova v příkazech nemusí pocházet z krátkého a předem známého „číselníku“. To je naznačeno v dalším příkladu, v němž se používá víceslovní příkaz „info <subjekt>“, přičemž druhé slovo (tedy subjekt) může být jakékoli, ale musí být zapsáno. Toto slovo lze „zachytit“ ve větvi, která je ve výpisu vyznačena tučně:
def perform_command(): response = input("> ") match response.split(): case ["quit"]: return "Quit" case ["list", "employees"]: return "List employees" case ["list", "departments"]: return "List departments" case ["list", "rooms"]: return "List rooms" case ["info", subject]: return f"Info about subject '{subject}'" case _: return "Wrong command" print(perform_command())
16. Vnořené řídicí struktury match
Kromě snahy o tvorbu snadno pochopitelného a testovatelného kódu nám nic jiného nebrání v tom, abychom ve skriptech používali vnořené řídicí struktury match. Tento koncept je ukázán v dalším příkladu, v němž se rozlišují příkazy typu „list employees“, „list departments“ atd. v konstrukci match vložené do jedné z větví case:
def perform_command(): response = input("> ") match response.split(): case ["quit"]: return "Quit" case ["list", obj]: match obj: case "employees": return "List employees" case "departments": return "List departments" case "rooms": return "List rooms" case _: return "Invalid object type: employees, departments, or rooms expected" case ["info", subject]: return f"Info about subject '{subject}'" case _: return "Wrong command" print(perform_command())
Ve skutečnosti můžeme příklad upravit, a to tak, aby se do „větve list“ řízení programu dostalo pouze za předpokladu, že druhé slovo je známé a pochází z předem zadané množiny. Příslušný vzorek je zvýrazněn:
def perform_command(): response = input("> ") match response.split(): case ["quit"]: return "Quit" case ["list", ("employees" | "departments" | "rooms") as obj]: match obj: case "employees": return "List employees" case "departments": return "List departments" case "rooms": return "List rooms" case ["info", subject]: return f"Info about subject '{subject}'" case _: return "Wrong command" print(perform_command())
17. Strukturální pattern matching a objekty
Možnosti nové programové konstrukce zajišťující strukturální pattern matching jdou však ještě dále, například existuje možnost testu, zda je hodnotou nějaký objekt popř. jaké jsou hodnoty atributů tohoto objektu. Pro ukázku upravíme příklad ze třinácté kapitoly tak, aby dokázal rozpoznat komplexní čísla reprezentovaná instancí třídy Complex:
class Complex(): def __init__(self, real, imag): self.real = real self.imag = imag def __str__(self): return f"Complex number {self.real}+i{self.imag} represented as object" def test_number(value): match value: case (0, 0): print("Zero") case (real, 0) if real>0: print(f"Positive real number {real}") case (real, 0): print(f"Negative real number {real}") case (0, imag) if imag<0: print(f"Negative imaginary number {imag}") case (0, imag): print(f"Negative imaginary number {imag}") case (real, imag): print(f"Complex number {real}+i{imag}") case Complex(): print(value) case _: raise ValueError("Not a complex number") test_number((0,0)) test_number((1,0)) test_number((-1,0)) test_number((0,1)) test_number((0,-1)) test_number((1,1)) test_number(Complex(0,0)) test_number(Complex(1,0)) test_number(Complex(-1,0)) test_number(Complex(0,1)) test_number(Complex(0,-1)) test_number(Complex(1,1))
Po spuštění získáme na výstupu tyto řádky:
Zero Positive real number 1 Negative real number -1 Negative imaginary number 1 Negative imaginary number -1 Complex number 1+i1 Complex number 0+i0 represented as object Complex number 1+i0 represented as object Complex number -1+i0 represented as object Complex number 0+i1 represented as object Complex number 0+i-1 represented as object Complex number 1+i1 represented as object
Samozřejmě nám nic nebrání v testu hodnot atributů objektu. Musíme začít, stejně jako v dalších případech, konkrétními objekty a navázat obecnější podmínkou:
match value: ... ... ... case Complex(real=0, imag=0): print(f"Zero complex represented as object") case Complex(): print(value)
Pro zajímavost se nyní podívejme, jak jednoduché či naopak složité je rozšíření předchozího demonstračního příkladu o podporu rozeznávání zlomků. Ty jsou v Pythonu již realizovány, a to konkrétně třídou Fraction. Nově přidané řádky kódu jsou zvýrazněny:
from fractions import Fraction class Complex(): def __init__(self, real, imag): self.real = real self.imag = imag def __str__(self): return f"Complex number {self.real}+i{self.imag} represented as object" def test_number(value): match value: case (0, 0): print("Zero") case (real, 0) if real>0: print(f"Positive real number {real}") case (real, 0): print(f"Negative real number {real}") case (0, imag) if imag<0: print(f"Negative imaginary number {imag}") case (0, imag): print(f"Negative imaginary number {imag}") case (real, imag): print(f"Complex number {real}+i{imag}") case Complex(real=0, imag=0): print(f"Zero complex represented as object") case Complex(): print(value) case Fraction(): print(f"Fraction {value}") case _: raise ValueError("Not a complex number") test_number((0,0)) test_number((1,0)) test_number((-1,0)) test_number((0,1)) test_number((0,-1)) test_number((1,1)) test_number(Complex(0,0)) test_number(Complex(1,0)) test_number(Complex(-1,0)) test_number(Complex(0,1)) test_number(Complex(0,-1)) test_number(Complex(1,1)) test_number(Fraction(0,1)) test_number(Fraction(1,1)) test_number(Fraction(1,2)) test_number(Fraction(1,3))
Výsledek po spuštění tohoto příkladu:
Zero Positive real number 1 Negative real number -1 Negative imaginary number 1 Negative imaginary number -1 Complex number 1+i1 Zero complex represented as object Complex number 1+i0 represented as object Complex number -1+i0 represented as object Complex number 0+i1 represented as object Complex number 0+i-1 represented as object Complex number 1+i1 represented as object Fraction 0 Fraction 1 Fraction 1/2 Fraction 1/3
18. Příloha: část gramatiky Pythonu s pattern matchingem
Syntaxe Pythonu je v současnosti popsána s využitím PEG neboli parsing expression grammar. Víme již, že nové programové konstrukce pro strukturální pattern matching byly přidány do Pythonu 3.10. Z celé (velmi dlouhé) gramatiky Pythonu byly do této přílohy vybrány pouze ty části, které se týkají právě pattern matchingu. Zejména byla upravena gramatika pro složené výrazy:
compound_stmt[stmt_ty]: | &('def' | '@' | ASYNC) function_def | &'if' if_stmt | &('class' | '@') class_def | &('with' | ASYNC) with_stmt | &('for' | ASYNC) for_stmt | &'try' try_stmt | &'while' while_stmt | match_stmt
Samotná syntaxe bloku začínajícího klíčovým slovem match:
match_stmt[stmt_ty]: | "match" subject=subject_expr ':' NEWLINE INDENT cases[asdl_match_case_seq*]=case_block+ DEDENT { CHECK_VERSION(stmt_ty, 10, "Pattern matching is", _PyAST_Match(subject, cases, EXTRA)) } | invalid_match_stmt subject_expr[expr_ty]: | value=star_named_expression ',' values=star_named_expressions? { _PyAST_Tuple(CHECK(asdl_expr_seq*, _PyPegen_seq_insert_in_front(p, value, values)), Load, EXTRA) } | named_expression case_block[match_case_ty]: | invalid_case_block | "case" pattern=patterns guard=guard? ':' body=block { _PyAST_match_case(pattern, guard, body, p->arena) } guard[expr_ty]: 'if' guard=named_expression { guard }
A nakonec následuje syntaxe samotných vzorků (patterns):
patterns[pattern_ty]: | patterns[asdl_pattern_seq*]=open_sequence_pattern { _PyAST_MatchSequence(patterns, EXTRA) } | pattern pattern[pattern_ty]: | as_pattern | or_pattern as_pattern[pattern_ty]: | pattern=or_pattern 'as' target=pattern_capture_target { _PyAST_MatchAs(pattern, target->v.Name.id, EXTRA) } | invalid_as_pattern or_pattern[pattern_ty]: | patterns[asdl_pattern_seq*]='|'.closed_pattern+ { asdl_seq_LEN(patterns) == 1 ? asdl_seq_GET(patterns, 0) : _PyAST_MatchOr(patterns, EXTRA) } closed_pattern[pattern_ty] (memo): | literal_pattern | capture_pattern | wildcard_pattern | value_pattern | group_pattern | sequence_pattern | mapping_pattern | class_pattern # Literal patterns are used for equality and identity constraints literal_pattern[pattern_ty]: | value=signed_number !('+' | '-') { _PyAST_MatchValue(value, EXTRA) } | value=complex_number { _PyAST_MatchValue(value, EXTRA) } | value=strings { _PyAST_MatchValue(value, EXTRA) } | 'None' { _PyAST_MatchSingleton(Py_None, EXTRA) } | 'True' { _PyAST_MatchSingleton(Py_True, EXTRA) } | 'False' { _PyAST_MatchSingleton(Py_False, EXTRA) } # Literal expressions are used to restrict permitted mapping pattern keys literal_expr[expr_ty]: | signed_number !('+' | '-') | complex_number | strings | 'None' { _PyAST_Constant(Py_None, NULL, EXTRA) } | 'True' { _PyAST_Constant(Py_True, NULL, EXTRA) } | 'False' { _PyAST_Constant(Py_False, NULL, EXTRA) }
19. Repositář s demonstračními příklady
Zdrojové kódy všech prozatím popsaných demonstračních příkladů určených pro programovací jazyk Python 3.10 (nikoli ovšem pro starší verze Pythonu 3!) byly uloženy do Git repositáře dostupného na adrese https://github.com/tisnik/most-popular-python-libs. V případě, že nebudete chtít klonovat celý repositář (ten je ovšem stále velmi malý, dnes má velikost zhruba několik desítek kilobajtů), můžete namísto toho použít odkazy na jednotlivé příklady, které naleznete v následující tabulce:
20. Odkazy na Internetu
- Pattern matching (Wikipedia)
https://en.wikipedia.org/wiki/Pattern_matching - Python 3.10 and the Elegance of Pattern Matching
https://python.plainenglish.io/python-3–10-and-the-elegance-of-pattern-matching-2620a02b2379 - More Pattern Matching in Python 3.10
https://towardsdatascience.com/more-advanced-pattern-matching-in-python-3–10–2dbd8598302a - Pattern Matching in Python 3.10
https://towardsdatascience.com/pattern-matching-in-python-3–10–6124ff2079f0 - Python 3.10.0
https://www.python.org/downloads/release/python-3100/ - PEP 634 – Structural Pattern Matching: Specification
https://peps.python.org/pep-0634/ - PEP 635 – Structural Pattern Matching: Motivation and Rationale
https://peps.python.org/pep-0635/ - PEP 636 – Structural Pattern Matching: Tutorial
https://peps.python.org/pep-0636/ - PEP 622 – Structural Pattern Matching
https://peps.python.org/pep-0622/ - Python 3.10 se strukturálním pattern matchingem
https://www.root.cz/zpravicky/python-3–10-se-strukturalnim-pattern-matchingem/ - Coconut: funkcionální jazyk s pattern matchingem kompatibilní s Pythonem
https://www.root.cz/clanky/coconut-funkcionalni-jazyk-s-pattern-matchingem-kompatibilni-s-pythonem/ - How to Install Python 3.10 on CentOS/RHEL 8 & Fedora 35/34
https://tecadmin.net/how-to-install-python-3–10-on-centos-rhel-8-fedora/ - Pattern matching functions in Clojure?
https://stackoverflow.com/questions/8596980/pattern-matching-functions-in-clojure - Clojure core.match
https://github.com/clojure/core.match - The Rust Programming Language: Patterns and Matching
https://doc.rust-lang.org/book/ch18–00-patterns.html#patterns-and-matching - The Rust Programming Language: Pattern Syntax
https://doc.rust-lang.org/book/ch18–03-pattern-syntax.html - Coconut: funkcionální jazyk s pattern matchingem kompatibilní s Pythonem
https://www.root.cz/clanky/coconut-funkcionalni-jazyk-s-pattern-matchingem-kompatibilni-s-pythonem/ - Coconut aneb funkcionální nadstavba nad Pythonem (2.část)
https://www.root.cz/clanky/coconut-aneb-funkcionalni-nadstavba-nad-pythonem-2-cast/ - Programovací jazyky používané v SSSR (část 2 – SNOBOL)
https://www.root.cz/clanky/programovaci-jazyky-pouzivane-v-sssr-cast-2-ndash-snobol/ - Coconut: Simple, elegant, Pythonic functional programming
http://coconut-lang.org/ - Parsing expression grammar
https://en.wikipedia.org/wiki/Parsing_expression_grammar - Abort, Retry, Fail?
https://en.wikipedia.org/wiki/Abort,_Retry,_Fail%3F - SNOBOL4 and SPITBOL Information
http://www.snobol4.com/ - Vanilla Snobol4 Reference Manual
http://burks.bton.ac.uk/burks/language/snobol/catspaw/manual/contents.htm - SNOBOL4.ORG – SNOBOL4 Resources
http://www.snobol4.org/ - Snobol3 – Snobol 3 Interpreter Implemented in Java
http://serl.cs.colorado.edu/~dennis/software/s3.html - Exploring Beautiful Languages – A guick look at SNOBOL
http://langexplr.blogspot.com/2007/12/quick-look-at-snobol.html - Rosetta Code: Roman_numerals
http://rosettacode.org/wiki/Roman_numerals - Category:SNOBOL4
http://rosettacode.org/wiki/Category:SNOBOL4 - An introduction to SNOBOL by James Ford
http://drofmij.awardspace.com/snobol/ - AWK
https://en.wikipedia.org/wiki/AWK - Get started with GAWK: AWK language fundamentals
https://web.archive.org/web/20150427143548/https://www6.software.ibm.com/developerworks/education/au-gawk/au-gawk-a4.pdf - Pattern Matching
https://docs.microsoft.com/en-us/dotnet/fsharp/language-reference/pattern-matching