Struktureller Musterabgleich

Einführung

Muster einer Katze in Tangram

Unter dem strukturellen Musterabgleich (englisch "structural pattern matching") versteht man eine Technik der Programmierung, die es ermöglicht, komplexe Datenstrukturen auf eine prägnante und lesbarere Weise in Vergleichen anzuwenden. Der strukturelle Musterabgleich wird in verschiedenen Programmiersprachen verwendet, unter anderem auch in Python.

Mit der strukturellen Mustererkennung wird eine Datenstruktur (z.B. eine Liste oder ein Tupel) analysiert und nach einem bestimmten Muster durchsucht, welches man vorher definiert hat. Man kann dann unterschiedliche Aktionen ausführen, basierend auf den gefundenen Mustern. Dies kann oft auf eine prägnante und leicht verständliche Weise geschrieben werden, was die Lesbarkeit des Codes erhöht.

Im Allgemeinen kann die strukturelle Mustererkennung die Fehleranfälligkeit verringern und die Produktivität bei der Entwicklung erhöhen, indem sie die Syntax verkürzt und die Lesbarkeit verbessert.

Der "strukturelle Musterabgleich" oder "struktureller Mustervergleich" wurde mit Python-Version 3.10. neu eingeführt. Die Syntax für diese neue Funktionalität wurde in PEP 622 im Juni 2020 vorgeschlagen. Der strukturelle Musterabgleich, wie ihn Python implementiert, wurde von einer ähnlichen Syntax in Scala, Erlang und anderen Sprachen inspiriert. In ihrer einfachsten Form verhält sie sich wie die switch-Anweisung in C, C++ oder Java. Es gibt jedoch noch weitere Anwendungsfälle für diese Funktion, wie man in diesem Kapitel unseres Python-Kurses lernen kann. Wir werden lernen, dass es in Python möglich ist, ein Muster in seine Grundkomponenten zu zerlegen.

Bevor Sie mit diesem Kapitel fortfahren, müssen Sie sicherstellen, dass auf Ihrem System die Python-Version 3.10 oder höher läuft. Der einfachste Weg, dies herauszufinden, ist die Verwendung von version aus sys:

In [1]:
import sys
sys.version
Out[1]:
'3.10.10 (main, Mar 21 2023, 18:45:11) [GCC 11.2.0]'

In Python wird der strukturelle Musterabgleich durch die match-Anweisung eingeführt.

Die match-Anweisung funktioniert, indem sie einen ausgewerteten Ausdruck (auch bekannt als "zu prüfender Ausdruck") mit einem oder mehreren Mustern ("Patterns") vergleicht. Ein Pattern ist ein spezieller Ausdruck, der einem bestimmten Datentyp entspricht oder eine komplexe Datenstruktur wie eine Liste oder ein Tupel enthält.

Schauen wir uns ein einfaches Beispiel für den strukturellen Musterverabgleich an. Wir schreiben eine Funktion, die auf eine ausgewählte Sprache reagiert, also 'chosen_language' ist der zu prüfende Ausdruch:

In [2]:
def greeting(language):
    chosen_language = language.capitalize()
    match chosen_language:
        case 'English':
            print('Hello')
        case 'German':
            print('Hallo!')
            
greeting('english')
Hello

In Pythons' strukturellem Musterabgleich wird der Unterstrich (_) als Platzhalter verwendet, um einen beliebigen Wert zu finden. Das bedeutet für den folgenden Code, dass der Fall mit dem '_' immer dann greift, wenn nicht 'English' oder 'German' eingegeben wird:

In [7]:
def greeting(language):
    chosen_language = language.capitalize()
    match chosen_language:
        case 'English':
            print('Hello')
        case 'German':
            print('Hallo!')
        case _:
            print(f"Hallo, ich kenne {chosen_language} nicht")
            
greeting('english')
greeting('french')
Hello
Hallo, ich kenne French nicht

Wie Sie wahrscheinlich wissen, wird Deutsch nicht nur in Deutschland, sondern auch in Österreich und Teilen der Schweiz gesprochen. Wir sollten nicht vergessen, dass auch in Liechtenstein und Luxemburg Deutsch, zusätzlich zu Französisch, gesprochen wird. Dieses Beispiel zeigt nun einen Anwendungsfall, in dem der strukturelle Musterabgleich die Dinge erleichtert:

In [4]:
def greeting(language):
    chosen_language = language.split()
    match chosen_language:
        case ['English']:
            print('Hello')
        case ['German']:
            print('Hallo!')
        case ['German', 'AT']:
            print(f'Servus!')
        case ['German', 'CH']:
            print(f'Grüezi!')
        case ['German', 'DE']:
            print(f'Hallo')
        case ['German', 'LU']:
            print(f'Gudden Dag')
        case ['German', 'LI']:
            print(f'Grüezi')

for lang in ['English', 'German LU', 'German CH']:
    greeting(lang)
Hello
Gudden Dag
Grüezi!

Ohne 'match' würde der Python-Code ähnlich wie der folgende Code aussehen:

In [5]:
def greeting(language):
    chosen_language = language.split()
    if len(chosen_language) == 1:
        if chosen_language == ['English']:
            print('Hello')
        elif chosen_language == ['German']:
            print('Hallo!')
    elif len(chosen_language) == 2:
        if chosen_language == ['German', 'AT']:
            print(f'Servus!')
        elif chosen_language == ['German', 'CH']:
            print(f'Grüezi!')
        elif chosen_language == ['German', 'DE']:
            print(f'Hallo')
        elif chosen_language == ['German', 'LU']:
            print(f'Gudden Dag')
        elif chosen_language == ['German', 'LI']:
            print(f'Grüezi')

for lang in ['English', 'German LU', 'German CH']:
    greeting(lang)
Hello
Gudden Dag
Grüezi!

Wir könnten den vorherigen Code noch ein klein wenig verbessern, aber es sollte klar sein, dass match in diesem Fall in seiner Klarheit dennoch überlegen ist.

Wir können unser Beispiel weiter verbessern: Was ist, wenn jemand eine unbekannte Sprache oder eine unbekannte Sprache plus eine Region wählt? Das ist sehr einfach. Anstatt ein String-Literal in unserem Muster zu verwenden, benutzen wir Variablennamen:

In [ ]:
def greeting(language):
    chosen_language = language.split()
    match chosen_language:
        case ['English']:
            print('Hello')
        case ['German']:
            print('Hallo!')
        case [unknown_language]:
            print(f"So far, we don't know how to greet in {unknown_language}!")
        case ['German', 'AT']:
            print(f'Servus!')
        case ['German', 'CH']:
            print(f'Grüezi!')
        case ['German', 'DE']:
            print(f'Hallo')
        case ['German', 'LU']:
            print(f'Gudden Dag')
        case ['German', 'LI']:
            print(f'Grüezi')
        case [lang, region]:
            print(f"Sorry, we don't know {lang} in your region {region}!")

for lang in ['English', 'German LU', 'French', 'German CH', 'English CA']:
    greeting(lang)
Hello
Gudden Dag
So far, we don't know how to greet in Dutch!
So far, we don't know how to greet in French!
Grüezi!
Sorry, we don't know English in your region CA!

Wir stellen einen weiteren interessanten Anwendungsfall in Form eines imaginären Abenteuerspiels vor. Die Spieler könnten Aktionen als Strings in den verschiedenen Formaten schreiben, wie

  • 'help'
  • 'show inventory'
  • 'go north'
  • 'go south'
  • 'drop shield'
  • 'drop all weapons'
In [17]:
commands = ['go north','go west', 'drop potion', 'drop all weapons', 'drop shield']
weapons = ['axe','sword','dagger']
shield = True
inventory = ['apple','wood','potion'] + weapons + ['shield']
for command in commands: 
    match command.split():
        case ["help"]:
            print("""You can use the following commands:
            """)
        case ["go", direction]: 
            print('going', direction)
        case ["drop", "all", "weapons"]: 
            for weapon in weapons:
                inventory.remove(weapon)
        case ["drop", item]:
            print('dropping', item)
            inventory.remove(item)
        case ["drop shield"]:
            shield = False 
        case _:
            print(f"Sorry, I couldn't understand {command!r}")
going north
going west
dropping potion
dropping shield
['apple', 'wood']

Noch ein Beispiel, in dem wir die Fakultätsfunktion definieren:

In [29]:
def factorial(n):
    match n:
        case 0 | 1:
            return 1
        case _:
            return n * factorial(n - 1)

for i in range(6):
    print(i, factorial(i))
0 1
1 1
2 2
3 6
4 24
5 120