Structural pattern matching
1. Scripts
2. Videos
Make sure that you have watched all the videos from this playlist!
- Introduction,
- Defining recap,
- Operations recap,
- Input function,
- f-Strings,
- Accessing characters,
- For loop,
- Slicing,
- Modifying,
- Looping once again,
- Logical operations,
- Comparisons,
- Built-in functions,
- Regular expressions,
- re pattern matching.
3. Requirements
3.1. Python >= 3.10
To be able to use structural pattern matching you have to work with Python 3.10
or higher. Please check what version of Python are you currently using:
python3 --version #while working on Linux
or
python --version # depend if there is python 2 on your computer.
The same syntax is needed while working on Windows. In the case of macOS please use -
instead of --
. In case of using Google Colab:
!python3 --version
If your Python
version is lower than 3.10 try to update your Python
version or follow the installation procedure provided under this link.
Make sure that your Python
version is appropriate. In another way, you won’t be able to perform any structural pattern matching.
4. Structural Pattern Matching - into
4.1. Python version of switch/case syntax
A switch statement is significantly faster than an if-else ladder if there are many nested if-else involved. This is due to the creation of a jump table for switches during compilation. As a result, instead of checking which case is satisfied throughout execution, it just decides which case must be completed. The number of comparisons made is lesser hence, reducing the compile time. Hence, while selecting from a large set of values, the switch would work better. Based on this article.
What is more a typical if - else
statement is used to choose between two options, one may use many elif
statements to extend the number of possible options but this significantly reduces the speed of this logical condition. Using switch/case (from now we will refer to match/case
) allows for numerous options, from the very beginning. match/case
is easy to modify, and there might be no logical connection between options, which is a great advantage over if-else
statement. Using match/case
allows for replacing multiple if-else
statements needed to use in some cases. But what is this match/case
?
4.2. match/case
Is a statement which compares the condition value with multiple cases. Matches are performed by going through the list of cases from top to bottom. When the match is found Python executes the statement in the corresponding case block, and then skips the remaining cases. A match statement typically has more than one/two cases
. The syntax looks like this:
match command:
case <pattern_1>:
<action_1>
case <pattern_2>:
<action_2>
...
...
...
case _:
<action_wildcard>
Important notes:
- there is no:
return
,break
,stop
, etc. keyword at the end ofmatch
; - ` _ ` (underscore) - is a wildcard, which matches anything. This is why it is the last case of the whole statement.
match
&case
are soft keywords. There won’t be a syntax error after using these words as variables in the code.- Pattern checks are made with equality checking
pattern == command
.The match statement is not a function, nor does it return anything. It simply demarcates the beginning of one or more case clauses, like a daisy chain of if statements. Based on link
5. Simple examples
Let us see how we can use match/case
for finding the number of days of each month. So far we would probably do it like this:
months = ['January', 'February', 'March', "April", 'May', 'Jun', 'July', 'August', 'September', 'October', 'November', 'December']
month_dict ={months[i]: i+1 for i in range(len(months))}
def how_many_days(month_name):
if month_name in months:
month_number = month_dict[month_name]
if month_number in [1,3,5,7,8,9,12]:
number_of_days = '31'
elif month_number in [4, 6, 10, 11]:
number_of_days = '30'
else:
number_of_days = '28 or 29'
else:
number_of_days = 'There is no such a month'
return number_of_days
So the how_many_days
returns the number of days for provided month name. In the case of the match/case
approach:
def how_many_days_match(month_name):
if month_name in months:
month_number = month_dict[month_name]
match month_number:
case 1 | 3 | 5 | 7 | 8 | 9 | 12:
number_of_days = '31'
case 4 | 6 | 10 | 11:
number_of_days = '30'
case _:
number_of_days = '28 or 29'
else:
number_of_days = 'There is no such a month'
return number_of_days
Please compare by yourself these 2 codes. In the second example ` | ` stands for ‘or’, so case 1 | 3 | 5 | 7 | 8 | 9 | 12
is equivalent to if month_number in [1,3,5,7,8,9,12]
.
In this particular example, there is almost no difference in using match/case
instead of if - else
. However, this example should give you a taste of using match/case
and the whole logic hidden behind it.
6. More sophisticated example
From this place, we will focus on match/case
mostly. Let us go through some examples line by line:
6.1. Example 1
A program that asks a user to provide some string, based on which some action takes place:
command = input()
match command.split():
case ["exit"]:
exit()
case ["open", filename]:
open(filename)
case ["close", filename]:
close(filename)
case _:
print (f"No such '{command}' a command")
command = input()
program asks a user to provide some text, which would be assigned to thecommand
variable;match command.split():
we create amatch/case
. Here we use ` . ` to use asplit()
function as well as to match against the contents of thecommand
variable.case ["exit"]:
Python checks if the first element ofcommand.split()
matches with"exit"
if yes then Python performsexit()
.case ["open", filename]:
If the previouscase
didn’t matchcommand.split()
anothercase
is tested. In this scenario Python looks for'open'
as the first element ofcommand.split()
, and if it is true then the second element is taken and theopen(filename)
function is called.case ["close", filename]:
same as for the previous one, with the exception that now Python is closing a file instead of opening.case _:
If non of the cases above matches thecommand.split()
this case is executed.
6.2. Example 2
Based on PEP
# point is an (x, y) tuple
match point:
case (0, 0):
print("Origin")
case (0, y):
print(f"Y={y}")
case (x, 0):
print(f"X={x}")
case (x, y):
print(f"X={x}, Y={y}")
case _:
raise ValueError("Not a point")
In this example, we are facing a cod which is printing for us ‘X’ and ‘Y’ coordinates for provided point (a tuple).
case (0, 0):
If a point is located in(0, 0)
then the program prints ‘Origin’.case (0, y):
This case is matched if the first element of the tuple is0
and the second might be any.case (x, 0):
In this case, the second element must be0
and the first might be any.case (x, y):
For this scenario(x,y)
might be any numbers.case _:
awaits for anypoint
value which does not match any of these above.
Please take some time and test this example. Check what would happen if you change the order of provided cases. How the logic of this match/case
example works? Can we move the 4th case
into 1st or 2nd place, what do you expect after this change?
7. Pattern matching
As a pattern, we may use simple values or complex ones with matching logic. Structural pattern matching allows for matching against objects with specific values. Now we will discuss the most common examples of patterns. We have already used some of them.
- Matching against a single value
'value'
case ‘value’: - Matching against the collection
['value', 'value_2']
case [‘value’, ‘value_2’]: - Matching against either
'value','value_2' or 'value_3'
case (‘value’ | ‘value_2’ | ‘value_3’): - Matching against any collection of items, which starts with
'a'
case [“a”, _]: - Matching against the collection
['value', value]
, wherevalue
captures the second value of the collection case [‘value’, value]: - Matching against the collection
['value', *values]
, which collects at least one value.*values
captures each subsequent value. case [‘value’, *values]:
8. Guards
Guards are not patterns. They start to work after the patterns are matched, but before the code inside the case
is executed. So the syntax of match/case
looks like this:
match command:
case <pattern_1> if guard:
<action_1>
case <pattern_2> if guard:
<action_2>
...
...
...
case _:
<action_wildcard>
This can be used in the following example of checking the matched pattern.
def age_check(age):
match age:
case 0:
return "You are starting to exist. Welcome!"
case int() if age > 0:
return f"You are now {age} years old. If you don't die, in five years you will be {age + 5} years old."
case int():
return f'You will be born in {-age} years.'
case float() if age > 0:
return f'You are a very precise person. Now you are {age} years old.'
case float():
return f'You will be born in {-age} float years.'
case _:
return 'Your age was not provided correctly'
The age_check
function expects int or float as an argument, and depending on provided age
returns a different output. Our guards simply check if age
is positive and based on that distinguish what is returned. Examples:
>>> age_check(0)
'You are starting to exist. Welcome!'
>>> age_check(20)
"You are now 20 years old. If you don't die, in five years you will be 25 years old."
>>> age_check(-15)
'You will be born in 15 years.'
>>> age_check(50.6)
'You are a very precise person. Now you are 50.6 years old.'
>>> age_check(-5.1)
'You will be born in 5.1 float years.'
>>> age_check('100')
'Your age was not provided correctly'
You can try to rewrite this example using only the if-else-elif
structure. Function isinstance
may be handy.
You may find some additional knowledge and examples under these links 1, 2, 3
9. Exercises
9.1. Ex. 1 (4 points)
Write an exchange
function which expects a string. This string should contain 3 information: currency name, currency rate, and amount of currency you want to have in the from 'currency rate amount'
. This function should return one of these 3 sentences:
'You have to prepare amount_calculated PLN to get amount currency'
,'currency should be a string and rate and amount should be a number'
,'Something went wrong. Input must have a from currency rate amount'
.
Examples:
>>> exchange('EUR 4.66 175')
'You have to prepare 815.5 PLN to get 175 EUR'
>>> exchange('USD 4.39 24')
'You have to prepare 105.36 PLN to get 24 USD'
>>> exchange('NOK forty_five 50')
'currency should be a string and rate and amount should be a number'
>>> exchange('NOK 0.45 fifty')
'currency should be a string and rate and amount should be a number'
>>> exchange('123 0.45 50')
'currency should be a string and rate and amount should be a number'
>>> exchange('NOK 0.45')
'Something went wrong. Input must have a from currency rate amount'
>>> exchange('0.45 50')
'Something went wrong. Input must have a from currency rate amount'
>>> exchange('50')
'Something went wrong. Input must have a from currency rate amount'
amount_calculated should be rounded to two decimal places. We assume that the exchange rate is in the indirect form, which means that it is information on how much PLN is needed to buy one unit of foreign currency. You are not allowed to use the if-elif-else
structure.
Good luck!
9.2. Ex. 2 (6 points)
Write a quadratic
function which expects a tuple with 3 elements. The function returns information about root/s and calculates its values. Consider multiple options for the parameter value. The quadratic equation looks like this
and argument should has the form (a, b, c)
. Consider the following returns:
- Origin
- Linear function with no root
- Linear function with the root at …
- Quadratic function with no real roots
- Quadratic function with one root at …
- Quadratic function with 2 roots: …, …
- Improper input
Replace … with correct values.
Examples:
>>> quadratic((0, 0, 0))
'Origin'
>>> quadratic((0, 0, 6))
'Linear function with no root'
>>> quadratic((0, 2, 3))
'Linear function with the root at -1.5'
>>> quadratic((0, 2, 0))
'Linear function with the root at 0.0' # or just 0
>>> quadratic((3, 0, 0))
'Quadratic function with one root at 0.0' # or just 0
>>> quadratic((3, 0, 2))
'Quadratic function with no real roots'
>>> quadratic((3, 0, -2))
'Quadratic function with 2 roots: 0.82, -0.82'
>>> quadratic((3, 1, 1 / 12))
'Quadratic function with one root at -0.17'
>>> quadratic((0, 0, '0'))
'Improper input'
>>> quadratic(('0', 0, 0))
'Improper input'
>>> quadratic((0, '0', 0))
'Improper input'
>>> quadratic((0, 0))
'Improper input'
>>> quadratic((0,))
'Improper input'
>>> quadratic('improper_input')
'Improper input'
Roots should be rounded to two decimal places and every time if at least one of the elements of the tuple is not an int or float or we don’t have the needed number of points, the function should return 'Improper input'
. You are not allowed to use the if-elif-else
structure.
Good luck!