Python Tutorial #2: Advanced Control Flow and Functions
Control Structures
Control structures are fundamental elements in Python that direct the flow of execution of a program, allowing us to make decisions and repeat actions efficiently. In this section, we will explore the main structures such as conditionals (IF), loops (For), the Range() function, controlling loops with break, continue, and else, the pass statement, and the match structure introduced in Python 3.10. Mastering these tools is essential to writing elegant and effective Python code, and through practical examples, we will learn how and when to use each of them in real programming situations.
Conditionals (IF)
The if is a fundamental concept in programming that allows conditional execution of code. In Python, this control structure evaluates a condition and, if true, executes a specific block of code. It can be extended with elif for multiple conditions and else to handle all other cases, providing a flexible and powerful control flow.
edad = 18
if edad < 18:
print("Eres menor de edad")
elif edad == 18:
print("Acabas de alcanzar la mayoría de edad")
else:
print("Eres mayor de edad")
Loop (for):
The for loop in Python is an iteration concept that allows you to loop through sequences efficiently. Unlike other languages, Python’s for is designed to directly iterate over the elements of any sequence (such as lists, tuples, or strings), making it intuitive and versatile. This concept is essential for processing data collections in an elegant and concise manner.
frutas = ["manzana", "banana", "cereza"]
for fruta in frutas:
print(f"Me gusta comer {fruta}")
Iterables
The concept of iterable is fundamental in Python. An iterable is any object capable of returning its elements one at a time. This concept allows you to work with data sequences efficiently, since it does not require all elements to be in memory simultaneously. Iterables are the basis for many operations in Python, from for loops to list comprehensions and higher-order functions.
range() function:
range() is a concept that generates numerical sequences efficiently. It is an essential tool in Python, especially useful in combination with for loops. The range() concept allows you to generate custom sequences with different start, end and step points, making it easy to create complex numerical iterations without the need to explicitly define lists.
for i in range(5):
print(i) # Imprime 0, 1, 2, 3, 4
Loop control:
These are flow control concepts within loops. They allow finer control over loop execution, making it easier to handle special cases and create more sophisticated iteration logic.
break
The break statement allows you to immediately exit a loop. When inside a nested loop, break will only exit the innermost loop.
for n in range(2, 10):
for x in range(2, n):
if n % x == 0:
print(f"{n} es divisible por {x}")
break
if x == n - 1:
print(f"{n} es un número primo")
continue
The continue is a flow control concept within loops that allows you to jump to the next loop iteration immediately, skipping the rest of the code in the loop body for that specific iteration. This concept is useful when you want to avoid executing certain parts of the code under specific conditions, without completely terminating the loop.
for num in range(2, 10):
if num % 2 == 0:
print(f"Encontrado un número par: {num}")
continue
print(f"Encontrado un número impar: {num}")
else
The else clause in a loop provides a block that is executed when the loop terminates normally (without break). It is similar to the else in a try-except structure, in that it is executed when a specific condition (in this case, a break) does not occur.
for n in range(2, 10):
for x in range(2, n):
if n % x == 0:
print(f"{n} es divisible por {x}")
break
else:
print(f"{n} es un número primo")
pass
pass is a concept in Python that represents a null operation. It is used when a statement is syntactically required, but no code is desired to be executed. This concept is useful as a placeholder in functions, classes, or conditionals that have not yet been implemented, allowing the programmer to outline the structure of the code without going into implementation details.
class MiClaseVacia:
pass
matches
match is a relatively new concept in Python (introduced in Python 3.10) that provides an advanced form of pattern matching. Similar to the switch structure in other languages, but more powerful, match allows you to compare an expression against multiple patterns and execute code based on the match. This concept makes it easier to write cleaner, more expressive code to handle multiple cases or decompose complex data structures.
def analizador_http(status):
match status:
case 400:
return "Bad request"
case 404:
return "Not found"
case 418:
return "I'm a teapot"
case _:
return "Something's wrong with the internet"
Patterns in match
Within the match statement, patterns are a key concept. They can be simple (such as literals) or complex (such as data structures). Patterns allow not only comparing values, but also unpacking and assigning parts of the matching data structure to variables. This concept makes match especially powerful for working with structured data and performing complex matches in a concise manner.
Unpacking patterns
The patterns in match can unpack data structures such as tuples and objects, assigning their components to variables. This concept allows for an elegant and concise way to extract and work with specific parts of complex data structures.
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}")
Patterns with classes
Class names followed by arguments can be used to match instances of specific classes and capture their attributes. This concept allows for more object-oriented handling within the match structure.
match point:
case Point(x=0, y=0):
print("Origin")
case Point(x=0, y=y):
print(f"Y={y}")
match_args
This special attribute on a class allows you to define the order of arguments for positional patterns in match. Makes it easier to write more concise patterns when working with class instances.
class Point:
__match_args__ = ('x', 'y')
Nested patterns
Patterns in match can be nested, allowing complex matches in multi-level data structures. This concept is powerful for working with hierarchical or complex data structures.
match points:
case []:
print("No points")
case [Point(0, 0)]:
print("The origin")
case [Point(x, y)]:
print(f"Single point {x}, {y}")
Saves in patterns
Additional conditions can be added to patterns using a if clause. This concept allows you to specify more refined conditions for each match.
match point:
case Point(x, y) if x == y:
print(f"The point lies on the diagonal Y=X at {x}")
Features
Functions in Python are reusable blocks of code that perform a specific task. They are a fundamental part of Python programming, enabling a wide range of programming techniques, from simple code encapsulations to advanced design patterns and functional programming.
def saludar(nombre):
return f"Hola, {nombre}!"
print(saludar("Alice")) # Imprime: Hola, Alice!
Function arguments
Functions in Python offer great flexibility in the way arguments are passed, allowing you to write more readable and versatile code. A fundamental concept is the use of keyword arguments (or named arguments), which allow parameter values to be specified by name rather than position. This is particularly useful when a function has multiple optional parameters.
Python also allows you to define functions with different types of parameters: positional, keywords, or a combination of both. Special parameters, marked / and *, allow developers to specify how arguments should be passed to the function. This improves the readability of the code and allows finer control over how functions can be called.
List of arbitrary arguments
Python allows a function to accept an arbitrary number of arguments using the syntax *args for positional arguments and **kwargs for keyword arguments. This provides great flexibility in defining functions.
-
*args: Allows a function to accept any number of positional arguments. These arguments are packed into a tuple within the function. -
**kwargs: Allows a function to accept any number of keyword arguments. These arguments are packaged into a dictionary within the function.
def suma(*args):
return sum(args)
print(suma(1, 2, 3, 4)) # Imprime: 10
Argument unpacking
Python also allows argument unpacking, which is essentially the opposite of *args and **kwargs:
-
The
*operator can unpack a list or tuple to pass them as separate arguments to a function. -
The
**operator can unpack a dictionary to pass its elements as named arguments.
def suma(a, b, c):
return a + b + c
numeros = [1, 2, 3]
print(suma(*numeros)) # Imprime: 6
Lambda expressions
Lambda expressions in Python are a concise way to create small anonymous functions. They are defined using the keyword lambda, followed by the arguments, a colon, and a single expression. The basic syntax is lambda argumentos: expresión. These functions are single expression and can have any number of arguments, but are limited to a single expression. They are particularly useful for creating simple, short-lived functions, especially as arguments to higher-order functions such as map(), filter(), or sort().
Lambdas are characterized by their brevity and the fact that they are evaluated and their result returned immediately. They cannot contain statements or annotations, which makes them ideal for simple operations but limits them in terms of complexity. For example, a simple lambda function that calculates the square of a number would look like this:
cuadrado = lambda x: x**2
print(cuadrado(5)) # Imprime: 25
Lambda expressions shine in situations where a simple function is needed as an argument. For example, using map() to apply a function to each element in a list:
numeros = [1, 2, 3, 4, 5]
cuadrados = list(map(lambda x: x**2, numeros))
print(cuadrados) # Imprime: [1, 4, 9, 16, 25]
Or when using filter() to select items from a list based on a condition:
numeros = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
pares = list(filter(lambda x: x % 2 == 0, numeros))
print(pares) # Imprime: [2, 4, 6, 8, 10]
Lambdas are also useful for sorting lists based on specific criteria:
personas = [('Alice', 25), ('Bob', 30), ('Charlie', 22)]
personas_ordenadas = sorted(personas, key=lambda x: x[1])
print(personas_ordenadas) # Imprime: [('Charlie', 22), ('Alice', 25), ('Bob', 30)]
Despite their usefulness, it is important to use lambda expressions sparingly. They are great for simple one-time functions, but can reduce readability if abused or used for complex logic. Additionally, by not having a name, they can make debugging difficult. For more complex functions, requiring multiple lines of logic, documentation or unit tests, it is preferable to define a normal function.
In short, lambda expressions are a powerful tool in Python when used appropriately. They provide an elegant way to write small inline functions, which can make the code more concise and expressive in certain contexts. However, it is important to balance brevity with clarity and only use lambdas when they actually improve the readability and structure of the code.
Feature Documentation
Documenting functions is an essential practice in Python to improve code readability and maintainability. Python provides several tools and conventions to document functions effectively. The two main ones are docstrings and function annotations.
Proper documentation of functions has several benefits:
-
Improve code understanding for other developers (and for yourself in the future).
-
Facilitates the use of automatic documentation generation tools.
-
Allows the use of static analysis and type checking tools.
-
Improve the development experience in integrated development environments (IDEs) with autocomplete and hints.
Documentation chains
Docstrings are an important feature in Python to provide documentation built into your code. They are text strings that appear as the first declaration in a module, function, class or method.
Characteristics:
-
They are defined between triple quotes (
""" """). -
They can be single line or multiline.
-
They are accessible at runtime through the
__doc__attribute. -
They follow specific formatting conventions (PEP 257).
def area_circulo(radio):
"""
Calcula el área de un círculo.
:param radio: El radio del círculo
:return: El área del círculo
"""
import math
return math.pi * radio**2
print(area_circulo.__doc__)
Function annotation
Function annotations are a Python feature that allows you to add metadata to a function’s parameters and its return value. Although they do not affect the runtime behavior of the function, they are useful for documentation, type checking, and static analysis tools.
Characteristics:
-
They are defined using a colon (
:) for the parameters and an arrow (->) for the return value. -
They can be of any type, including complex or custom types.
-
They are stored in the
__annotations__attribute of the function. -
They are optional and do not affect the execution of the function.
def saludo(nombre: str) -> str:
return f"Hola, {nombre}!"
print(saludo.__annotations__) # Imprime: {'nombre': <class 'str'>, 'return': <class 'str'>}
Slogans
We have acquired valuable knowledge and have the necessary resources at our disposal. Now is the time to apply what you have learned and start creating. GitHub repository
Control structure(7)
-
Number Classifier: Write a program that prompts the user for a number and determines whether it is positive, negative, or zero.
- Use conditionals (
if,elif,else) for this exercise.
- Use conditionals (
-
Multiplication table: Create a program that prints the multiplication table from 1 to 10 for a number entered by the user.
- Use a for loop and the
range()function for this exercise.
- Use a for loop and the
-
Guess the number: Implement a game where the program chooses a random number between 1 and 100, and the user must guess it.
- The program should give clues of “too high” or “too low” and use a
whileloop until the user guesses the number. - Use
breakwhen the user guesses correctly.
- The program should give clues of “too high” or “too low” and use a
-
Factorial Calculator: Write a function that calculates the factorial of a number entered by the user.
- If the user enters a negative number, the program must handle this situation appropriately.
- Use a
forloop and theelsestatement in the loop.
-
Password Validator: Create a program that validates a password entered by the user based on the following criteria:
- Must be at least 8 characters
- Must contain at least one uppercase letter, one lowercase letter, and one number
- Should not contain spaces
- Use a
whileloop with the break statement to prompt for the password until it is valid.
-
Triangle Classifier: Write a program that prompts the user for the lengths of the three sides of a triangle and determines whether it is equilateral, isosceles, or scalene.
- Use the statement
matchfor this exercise.
- Use the statement
-
Fibonacci sequence generator: Implements a Fibonacci sequence generator up to a term n entered by the user.
- It uses a for loop and the
range()function. - If the user enters a negative number, use the
passstatement to handle this case.
- It uses a for loop and the
Features(7)
-
Calculator with default arguments: Create a function called ‘calculator’ that accepts two numbers and an operation (addition, subtraction, multiplication, division) as arguments.
- The operation must have a default value of ‘sum’.
- Uses default value arguments and keyword arguments.
-
Function with arbitrary arguments: Write a function called ‘average’ that calculates the average of an arbitrary number of arguments.
- Use
*argsto handle the arguments.
- Use
-
Runtime Decorator: Create a decorator that measures the execution time of a function and prints it.
- Apply this decorator to a function that performs a task that takes at least one second (you can use
time.sleep()).
- Apply this decorator to a function that performs a task that takes at least one second (you can use
-
Higher order function: Implement a higher order function called ‘apply_operation’ that takes an operation (function) and a list of numbers as arguments.
- The function must apply the operation to each number in the list and return a new list with the results.
- Use this function with at least two different operations (for example, doubling a number and squaring it).
-
Closure for counter: Create an external function called ‘create_counter’ that returns an internal function.
- The internal function must be a counter that increments and returns a value each time it is called.
- Demonstrate the use of this closure by creating two independent counters.
-
Prime number generator: Implement a generator that produces prime numbers.
- The generator must accept an optional parameter to limit the number of primes generated.
- Use the ‘yield’ keyword to create the generator.
-
Function with type annotations: Write a function called ‘process_text’ that takes a text string and an integer as arguments.
- The function should return the first n words of the text, where n is the given integer.
- Uses type annotations for the arguments and return value.
- Include a docstring that explains the purpose of the function and its parameters.
Sections covered today
-
- 4.1. The if statement
- 4.2. The for statement
- 4.3. The range() function
- 4.4. Break, continue, and else statements in loops
- 4.5. The pass sentence
- 4.6. The match statement
- 4.7. Define functions
- 4.8. More on function definition
- 4.8.1. Arguments with default values
- 4.8.2. Keywords as arguments
- 4.8.3. Special parameters
- 4.8.3.1. Positional or keyword arguments
- 4.8.3.2. Positional Only Parameters
- 4.8.3.3. Keyword-only arguments
- 4.8.3.4. Examples of Functions
- 4.8.3.5. Summary
- 4.8.4. Arbitrary argument lists
- 4.8.5. Unpacking a list of arguments
- 4.8.6. Lambda expressions
- 4.8.7. Documentation text strings
- 4.8.8. Function annotation
- 4.9. Intermezzo: Programming style