Alternative To if...elif...else Statements - Structural Pattern Matching In Python
Note: The original post can be found here
Thanks to rawpixel.com for the wonderful images
Introduction
A switch statement is a control flow statement that allows a program to execute a different block of code based on the value of a variable or expression. It is similar to an if statement, but it can be more concise and easier to read in certain cases.
In Python, a similar feature called structural pattern matching has been implemented since version 3.10.
Structural pattern matching allows you to match the structure of data, such as the type of an object or the shape of a data structure, and take different actions based on the result.
Structural pattern matching is performed using the match statement, which is similar to a switch statement in other programming languages but with some additional features and capabilities. Structural pattern matching was introduced by PEP 636.
It can be used as an alternative to using?if...elif...else statements for performing type-based dispatch and data deconstruction. Using structural pattern matching is fairly straightforward, and this article will highlight the differences between using if...elif...else statements and structural pattern matching for these purposes.
Let's dive into this and learn more about pattern matching.
Prerequisites
To be able to use structural pattern matching, you'll need to run Python 3.10 and above.?
Content
The article contains the following:
Download The Code
All code shown in this article can be accessed via the following repository on GitHub:
What Is Structural pattern matching?
Structural pattern matching is a new feature introduced in Python 3.10.
It allows you to match the structure of an object against a pattern in a concise and expressive way.
Each case statement is followed by a pattern to match against.
This pattern can be something like an action and an object, like a string object: 'John'.?
With structural pattern matching, we can use literal values in patterns, as well as more complex expressions. Matches are performed from top to bottom, and we can create more conditional cases with more complex patterns. The wildcard pattern, also known as the catching case, is a default case that always matches and should be placed last in the pattern as it will always execute.
The syntax for this looks like the following:
match obj
case [action]:
... # interpret single-verb action
case [action, obj]:
... # interpret action, obj
which could be something like the following example:
# determine action like obj and verb
def determine_action(x):
match x:
case (obj, verb): # data deconstruction
return f"{obj} is {verb} becase it is type {type(x)}"
case _: # default case
return f"{x} is not a tuple becase it is type {type(x)}"
print(determine_action(('dog', 'barking')))
print(determine_action(('cat', 'meowing')))
# not a tuple
print(determine_action('dog'))
Now, don't get frightened, there is some complexity in this code, i.e. data deconstruction that we will go through later. For now, it's enough to just understand that the function takes a single argument x and returns a string based on the type and structure of x.
Example - type-based dispatch
In the following example, we will use type-based dispatch, a technique for selecting which code to execute based on the type of an object or expression.
This can be useful when you want to perform different actions depending on the type of an object, or when you want to take advantage of the specific behavior or properties of a particular type.
We will write a function called?check_type() that takes an object as an argument and returns a message?based on the type of the object.?
The function provides a way to determine the type of an object in Python and return a string indicating the type.
We will look at how we can do this using both if...elif...else and also with pattern matching.
type-based dispatch using if...elif...else
The following is an example using type-based dispatch in Python with an if...elif...else statement:
# type based dispatch using if else
def check_type(x):
if type(x) == int:
return f"{x} is an integer"
elif type(x) == float:
return f"{x} is a float"
elif type(x) == list:
return f"{x} is a list"
elif type(x) == tuple:
return f"{x} is a tuple"
elif type(x) == dict:
return f"{x} is a dict"
elif type(x) == str:
return f"{x} is a string"
else:
return f"{x} is a type that I don't know about"
This code defines a function check_type that takes an object as an argument and returns a message based on the type of the object.
The?if...elif...else statement checks the type of x using the type function, and returns a different message for each possible type.
type-based dispatch using pattern matching
Here's the same example as above, but using structural pattern matching with the?match statement:
'''
devoriales.com
Path: pattern_matching/example_0.py
description: type based dispatch using match case
'''
# type based using structural pattern matching
def check_type(x):
print('match statement is working')
match x:
case int():
return f"{x} is an integer"
case float():
return f"{x} is a float"
case list():
return f"{x} is a list"
case tuple():
return f"{x} is a tuple"
case dict():
return f"{x} is a dict"
case str():
return f"{x} is a string"
case _:
return f"{x} is a type that I don't know about"
print(check_type(1)) # 1 is an integer
print(check_type(1.0)) # 1.0 is a float
print(check_type([])) # [] is a list
print(check_type(())) # () is a tuple
print(check_type({})) # {} is a dict
print(check_type('')) # is a string
In both examples, the code is performing type-based dispatch by selecting which code to execute based on the type of the x argument.
This code defines a function check_type that takes an object as an argument and returns a message based on the type of the object.
The?match statement checks the type of x and returns a different message for each possible type.
Example - Literal Values In Match Patterns
A literal value is a value that is written directly into the code. It is a value that is not calculated and is not the result of an expression or a function.
Literal values can be strings, integers, floating-point numbers, and boolean values.
A literal value in match patterns is a value that is exactly what it appears to be.
For example, in the code below, 'James' is a literal value.
We will start by writing an if...elif...else version of the same application.
The check_common_name function takes a single argument name and uses an if statement with multiple elif clauses to check if name is one of five specific names.
If?name is any of these names, the function returns True. If name is not one of these names, the function returns False.
if...elif...else version:
# Devoriales.com, 2022
# Path: switch_case/example_1.py
# checks if a name is common or not. Uses if else statements
def check_common_name(name):
# switch case
if name == 'John':
return True
elif name == 'Mary':
return True
elif name == 'James':
return True
elif name == 'Sarah':
return True
elif name == 'Robert':
return True
else:
return False
print(check_common_name('John')) # checks if a name is common or not Output: True
print(check_common_name('Alex')) # checks if a name is common or not Output: False
Output:
True
False
The next step is to write the same logic, but this time, we will use pattern matching:
# Devoriales.com, 2022
# Path: pattern_matching/example_1.py
# Description: Checking if a name is common using Structural Pattern Matching
def check_common_name(name):
# switch case
match name:
case 'John':
return True
case 'Mary':
return True
case 'James':
return True
case 'Sarah':
return True
case 'Robert':
return True
case _: # default case which is the same as else
return False
print(check_common_name('John')) # checks if a name is common or not
print(check_common_name('Aleks')) # checks if a name is common or not
When we run this code, we get the following output, just like the previous code:
True
False
The check_common_name function takes a single argument name and uses a switch statement implemented with the match and case keywords to check if name is one of five specific names.
?If name is any of these names, the function returns True. If name is not one of these names, the function returns False.
Example - Movie Menu app
Assume that we are running a movie-streaming business and we want our users to select a movie from a menu.
The movie menu app?is a simple command-line program that allows the user to choose a movie from a list of options.
The function that we will write takes a single argument?movie, which is the user's selection.
We will start by writing an version that is based on if...elif...else statements.
Code: menu data is kept in a dictionary
# movie dictionary
movies = {
'1': 'The Matrix',
'2': 'The Avengers',
'3': 'The Dark Knight',
'4': 'The Godfather',
}
print('Movie Menu', end='\n****************\n')
for key, value in movies.items():
print(f'{key}: {value}')
Now, let's create a function that will take input as an argument. The argument will be the actual movie key-value from a dictionary.
Next, it will perform some if...elif...else statements to check what selection the user has chosen:
# movie menu with if else statements
def movie_menu(movie):
# if else statements
if movie == '1':
return 'Loading The Matrix'
elif movie == '2':
return 'Loading The Avengers'
elif movie == '3':
return 'Loading The Dark Knight'
elif movie == '4':
return 'Loading The Godfather'
else:
return 'Loading Invalid Choice'
# movie dictionary
movies = {
'1': 'The Matrix',
'2': 'The Avengers',
'3': 'The Dark Knight',
'4': 'The Godfather',
}
print('Movie Menu', end='\n****************\n')
for key, value in movies.items():
print(f'{key}: {value}')
input = input('Select movie to watch: ')
print(movie_menu(input))
The menu will be presented like this:
Movie Menu
****************
1: The Matrix
2: The Avengers
3: The Dark Knight
4: The Godfather
Select movie to watch: 2
Loading The Avengers
Next, we will turn this logic into structural pattern matching and notice how cleaner it becomes.
# devoriales.com, 2022
# Movie Menu
# Path: pattern_matching/example_2.py
# Description: Movie Menu using Structural Pattern Matching
# movie menu with match statement
def movie_menu(movie):
# switch case
match movie:
case '1':
return 'Loading The Matrix'
case '2':
return 'Loading The Avengers'
case '3':
return 'Loading The Dark Knight'
case '4':
return 'Loading The Godfather'
case _:
return 'Loading Invalid Choice'
# movie dictionary
movies = {
'1': 'The Matrix',
'2': 'The Avengers',
'3': 'The Dark Knight',
'4': 'The Godfather',
}
print('Movie Menu', end='\n****************\n')
for key, value in movies.items():
print(f'{key}: {value}')
input = input('Select movie to watch: ')
print(movie_menu(input))
The menu in the terminal looks the same for both versions:
Movie Menu
****************
1: The Matrix
2: The Avengers
3: The Dark Knight
4: The Godfather
Select movie to watch: 3
Loading The Dark Knight
?The last wildcard matching pattern is defined as an underscore _
You can name it as you like, but in Python, it is a good convention to write it as '_' as the last evaluation. You don't want other Python developers to hate you ??
Example - Movie Streaming class
In this example, we will define a?Movie class that has three instance variables: name, year, and rating.?
领英推荐
The class has a class method get_movie, which takes an integer option as input and returns a Movie instance based on the value of option.
The code defines a Movie class that has three instance variables: name, year, and rating. It also has a class method get_movie, which takes an integer option as input and returns a Movie instance based on the value of option.
The?get_movie class method uses structural pattern matching to create a?Movie instance based on the value of option.
For example, If?option is 1, it returns a Movie instance with the name 'The Matrix', year 1999, and rating 8.7. If option is 2, it returns a Movie instance with the name 'The Avengers' and so on.
If?option is any other value, it returns a Movie instance with the name 'Invalid Choice', year 0, and rating 0. The __str__ method of the Movie class returns a string representation of the Movie instance with the name, year, and rating variables.
?In this example, we have used the Object-Oriented Programming style where we have created a class instance. Not only that, we have used something called an alternative constructor from the class method. If you want to learn Object Oriented Programming in Python, read the following OOP article series here
Based on our selection in the menu, we will instantiate a class instance and send in the required arguments that are taken as parameters by our Movie class:
# devoriales.com, 2022
# Movie Streaming Service using Structural Pattern Matching
# Path: pattern_matching/example_3.py
# Description: Movie Menu using Structural Pattern Matching and Classes
class Movie():
def __init__(self, name, year, rating):
self.name = name
self.year = year
self.rating = rating
@classmethod
def get_movie(cls, option: int):
match option:
case 1:
return cls('The Matrix', 1999, 8.7)
case 2:
return cls('The Avengers', 2012, 8.1)
case 3:
return cls('The Dark Knight', 2008, 9.0)
case 4:
return cls('The Godfather', 1972, 9.2)
case _:
return cls('Invalid Choice', 0, 0)
def __str__(self):
return f'Name: {self.name} Year: {self.year} - Rating: {self.rating}'
# movie class instance
movies = {
'1': 'The Matrix',
'2': 'The Avengers',
'3': 'The Dark Knight',
'4': 'The Godfather',
}
if __name__ == '__main__':
# Presenting a movie menu
print('Movie Menu', end='\n****************\n')
for key, value in movies.items():
print(f'{key}: {value}')
# Get user input
input = input('Select movie to watch: ')
# Create a movie class instance from the alternative constructor
movie = Movie.get_movie(int(input))
print(movie) # print movie details from our class instance
The code also defines a dictionary movies with keys '1', '2', '3', and '4' and corresponding values '
It presents a movie menu to the user with a list of movies from the movies dictionary and prompts the user to select a movie to watch. The user input is taken as an integer and passed to the get_movie method to create a Movie instance. The details of the Movie instance is then printed to the screen.
Output:
Movie Menu
****************
1: The Matrix
2: The Avengers
3: The Dark Knight
4: The Godfather
Select movie to watch: 2
Name: The Avengers Year: 2012 - Rating: 8.1
Example - HTTP status codes matching
In the following example, we will write a simple function that accepts an HTTP status code and returns an output. By now, we have seen several if...elif...else equivalent examples, this time, we will only write the Switch-Case statement:
# devoriales.com, 2022
# Path : pattern_matching/example_4.py
# check http status codes using match statement
# switch case
def status_code(status_code):
# switch case
match status_code:
case 200:
return 'OK'
case 404:
return 'Not Found'
case 500:
return 'Internal Server Error'
case _:
return 'Unknown'
print(status_code(500)) # check status code
Output:
Internal Server Error
The function status_code is a simple implementation of a switch statement that takes an integer as input and returns a string based on the value of the integer.
For example, if the value of?status_code is 200, the function returns 'OK'.
Example - Switch Case with OR condition
Sometimes we need to write OR condition statements like the following ( in the following example, we're writing an if...elif...else statement :
>>> if x or y:
... return True
This is also achievable with pattern matching.
# devoriales.com, 2022
# Path: pattern_matching/example_5.py
# description: benchmarking switch case vs if else in python
# match statement
def sportcars(car):
print(car)
# switch case
match car:
case 'Ferrari' | 'Lamborghini' | 'Porsche' | 'Aston Martin': # multiple cases with OR condition
return 'Sportscar'
case _:
return 'Not a sportscar'
print(sportcars('Ferrari')) # multiple cases with OR condition will return Sportscar
print(sportcars('Fiat')) # multiple cases with OR condition will return Not a sportscar
Output:
Ferrari
Sportscar
Fiat
Not a sportscar
The code above is defining a function called sportcars that takes in a parameter car. The function has a match statement that is used to perform pattern matching on the value of car.
Example - Switch Case with AND condition
The following example is similar to the previous one, except this time we will write a condition that has to match two arguments.
With if...elif...elsestatement, it would look like the following:
# devoriales.com, 2022
# Path: pattern_matching/example_6.py
# description: multiple cases with AND condition
# if else
def sportcars(*car):
if car[0] == 'Ferrari' and car[1] == 'Lamborghini':
return 'Sportscars'
else:
return 'Not a sportscar'
print(sportcars('Ferrari', 'Lamborghini')) # multiple cases with AND condition will return Sportscars
print(sportcars('Fiat', 'Lamborghini')) # multiple cases with AND condition will return Not a sportscar
Now, we will do similar with pattern matching:
# devoriales.com, 2022
# Path: pattern_matching/example_6.py
# description: multiple cases with AND condition
# with AND condition
def sportcars(*car):
print(car)
# switch case
match car:
case 'Ferrari', 'Lamborghini': # multiple cases with AND condition
return 'Sportscars'
case _:
return 'Not sportscars'
print(sportcars('Ferrari', 'Lamborghini')) # Returns "Sportscars"
print(sportcars('Fiat', 'Lamborghini')) # Returns "Not sportscars"
print(sportcars('Ferrari', 'Fiat')) # Returns "Not sportscars"
print(sportcars('Ferrari', 'Lamborghini', 'Porsche')) # Returns "Not sportscars"
Output:
Sportscars
Not a sportscar
Not a sportscar
Sportscars
Example - More complex pattern matching
In this example, we will create an application that manages files and directories. We will use the os library, which provides the ability to run real system commands.
The following code defines a function that executes different actions based on the content of a command string, using structural pattern matching to match the structure of the string and take different actions based on the result:
# devoriales.com, 2022
# Path: pattern_matching/manage_files/file_handler.py
# Description: Handling files using switch case and match statement
'''
command is the parameter of the commander function
*file gets unpacked and is used as a parameter for the os.system function
'''
import os
command = input("Enter a command and a file: ")
def commander(command):
match command.split():
case ["help" | "h" | "?", *file ] if "--ask" in command: # if --ask is in the command then print the help message
print("Commands: cat, rm, ls, mv, cp, touch, mkdir, rmdir, pwd, cd")
case ["cat", *file]: # if cat is in the command then print the file
files = [print(os.system(f"cat {x}")) for x in file] # prints the file if it exists by using list comprehension
case ["rm" | "remove" | "delete", *file]:
print(os.system(f"rm {file}"))
case ["ls" | "list", *file]:
print(os.system(f"ls {file}"))
case ["mv" | "move", *file]:
print(os.system(f"mv {file}"))
case ["cp" | "copy", *file]:
print(os.system(f"cp {file}"))
case ["touch", *file]:
for x in file:
print(os.system(f"touch {x}"))
case ["mkdir" | "make directory", *file]:
print(os.system(f"mkdir {file}"))
case ["rmdir" | "remove directory", *file]:
print(os.system(f"rmdir {file}"))
case ["pwd" | "print working directory", *file]:
print(os.system(f"pwd {file}"))
case ["cd" | "change directory", *file]:
print(os.system(f"cd {file}"))
case _:
print("Command not found")
commander(command)
The code defines a function called?commander that takes a single argument command and executes different actions based on the content of the command string.
The commander function uses structural pattern matching to match the structure of the?command string, which is split into a list of words using the split method. Each case in the match statement checks for a specific sequence of words in the command list and performs a different action based on the result.
Examples of what you can do in the terminal:
python file_handler.py
Enter a command and a file: help --ask
Commands: cat, rm, ls, mv, cp, touch, mkdir, rmdir, pwd, cd
Enter a command and a file: cat log1 log2
yyyy/mm/dd hh:mm:ss.sss pid tid message-id message(LANG=en)
0046 2003/12/06 19:51:32.250 my_app1 00EB7859 012A54F9 AP1_10000-I application1 start.
0048 2003/12/06 19:51:32.265 my_app1 00EB7859 012A54F9 AP1_10100-E Catch an exception!
0049 2003/12/06 19:51:32.265 my_app1 00EB7859 012A54F9 AP1_10001-I application1 end.
0
yyyy/mm/dd hh:mm:ss.sss pid tid message-id message(LANG=en)
0046 2003/12/06 19:51:32.250 my_app1 00EB7859 012A54F9 AP1_10000-I application1 start.
0048 2003/12/06 19:51:32.265 my_app1 00EB7859 012A54F9 AP1_10100-E Catch an exception!
0049 2003/12/06 19:51:32.265 my_app1 00EB7859 012A54F9 AP1_10001-I application1 end.
Example - Data Deconstruction
Data deconstruction?refers to the process of extracting individual elements from data structures such as lists, tuples, and dictionaries, and assigning them to separate variables.
consider the following example:
my_tuple = ('Woman', 'singing')
obj, verb = my_tuple
print(obj) # 'woman'
print(verb) # 'singing'
In this example, the variables obj and verb are assigned the values of the first and second elements of the tuple, respectively. This allows you to access and manipulate the individual elements of the tuple separately.
Data deconstruction can be a convenient way to extract and work with specific elements of a data structure and can be particularly useful when combined with structural pattern matching, as seen in some examples before.
Let's have a look at an example with if...elif..else statement.
The following function takes a single argument x and uses an if statement to check if the value of x is a tuple.
If True, the function returns a string that includes the first, second, and third elements of the tuple. If the value of x is not a tuple, the function returns a string indicating that x is not a tuple.
'''
devoriales.com, 2022
Path: pattern_matching/data_deconstruction.py
description: data deconstruction - tuple unpacking
'''
# data deconstruction with if else
def data_deconstruction(x):
if type(x) == tuple:
return f"{x[0]} is working as a {x[1]} at {x[2]}"
else:
return f"{x} is not a tuple"
print(data_deconstruction(('Oliver', 'developer', 'Google')))
Now we can make the same logic as in the previous version, but this time with match pattern.
The data_deconstruction function takes a single argument x and uses structural pattern matching to deconstruct the value of x into three separate variables: name, job, and company.
The match statement uses a tuple pattern to match a tuple with three elements, and assigns the first element to the name variable, the second element to the job variable, and the third element to the company variable. If the value of x is a tuple with three elements, the function returns a string that includes the values of the name, job, and company variables.
If the value of x is not a tuple with three elements, the function uses the default case (indicated by the _ wildcard) to return a string indicating that x is not a tuple.
'''
devoriales.com, 2022
Path: pattern_matching/data_deconstruction.py
description: data deconstruction - tuple unpacking
'''
# data deconstruction with match case
def data_deconstruction(x):
match x:
case (name, job, company):
return f"{name} is working as a {job} at {company}"
case _:
return f"{x} is not a tuple"
print(data_deconstruction(('Sanja', 'designer', 'Apple')))
Output:
Sanja is working as a designer at Apple
Benchmark Switch Case Statement vs If...Else statement
To compare the if...elif...else statement against the match statement, we will write code that compares the performance of two functions:?check_common_name(), which uses the match pattern, and check_with_if(), which uses the if...elif...else statement.
To do this, we will use the following process:
Code:?
# devoriales.com, 2022
# Path: pattern_matching/benchmarking.py
# description: benchmarking switch case vs if else in python
import time
# create a list of names
my_name_list = []
# context manager to open the file and read the names and append them to the list
with open ('data', 'r') as f:
for line in f:
my_name_list.append(line)
# add a name to the list to the very end
my_name_list.append('Moby Dick')
# check if name is in list with switch case
def check_common_name(name):
# switch case
for x in my_name_list:
match name:
case str(x):
return True
case _:
return False
# check if name is in list with if else
def check_with_if(name):
# if else
for x in range(len(my_name_list)):
if name == my_name_list[x].splitlines()[0]:
return True
return False
# time the functions
start_if = time.time() # time the function with if else
print(check_with_if('Moby Dick'))
end_if = time.time()
print(end_if - start_if)
result_if = end_if - start_if # get the result of the function with if else
start_switch = time.time() # time the function with switch case
print(check_common_name('Moby Dick'))
end_switch = time.time()
print(end_switch - start_switch)
result_switch = end_switch - start_switch # get the result of the function with switch case
#compare the results
if result_if > result_switch:
print('switch case is faster')
else:
print('if else is faster')
Output:
True
0.04650712013244629
True
1.4781951904296875e-05
switch statement is faster
As we can see, the switch statement is faster. This is a very simple benchmark, and it probably takes a much larger data set to come to a fair conclusion.
This is what the code is doing:
Summary
In this article, you have learned what structural pattern matching in Python is and how it compares to if...elif...else statements.
We have seen several simpler use cases and examples and learned how to match single arguments and multiple arguments and use AND / OR conditions in our matching patterns.?
Structural pattern matching ?does not necessarily require significantly less code compared to?if...elif...else statements, but there is a slightly smaller amount of code you need to write when writing match cases compared to if...elif...else.
Structural pattern matching is also slightly more performant and faster according to the benchmarking we have done in this article, which could be another reason to start using it in future projects.
In my opinion, the biggest reason for using a switch case over if...elif...else statements is that it provides much clearer code readability.
About the Author
Aleksandro?Matejic, a Cloud Architect, began working in the IT industry over?21 years ago?as a technical specialist, right after his studies. Since then, he has worked in various companies and industries in various system engineer and IT architect roles. He currently works on designing?Cloud solutions, Kubernetes, software development, and DevOps technologies.
In his spare time, Aleksandro works on?different development projects?such as developing?devoriales.com, a blog and learning platform launching in 2022/2023. In addition, he likes to read and write technical articles about software development and DevOps methods and tools. You can contact Aleksandro by visiting his?LinkedIn Profile.