{
"cells": [
{
"cell_type": "markdown",
"metadata": {
"id": "bsX7qzeTwOwc"
},
"source": [
"# Chapter 5: Functions and Modularization\n",
"\n",
"```{image} abertura-light.png\n",
":alt: Five trees of strokes in which the colour of each branch marks the depth of the recursion that drew it\n",
":class: abertura only-light\n",
"```\n",
"\n",
"```{image} abertura-dark.png\n",
":alt: Five trees of strokes in which the colour of each branch marks the depth of the recursion that drew it\n",
":class: abertura only-dark\n",
"```\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Defining Functions\n",
"\n",
"A function is a named block of code that performs a task and can be called as many times as needed. Instead of repeating the same lines at several points in a program, you write the task once, inside the function, and call the function wherever the task is needed. This makes the program shorter, easier to read and easier to fix: an error in the task is corrected in one place only. Splitting a program into functions, each with one task, is what is called modularization.\n",
"\n",
"To define a function, we use the reserved word `def`. The basic structure of a function includes:\n",
"\n",
"- **Keyword `def`:** Indicates the start of a function definition.\n",
"- **Function name:** Should be descriptive and follow Python naming conventions, as described in [PEP 8](https://peps.python.org/pep-0008/). Use lowercase letters with words separated by underscores (snake_case convention).\n",
"- **Parentheses `()`** containing the **parameters** (if any) separated by commas.\n",
"- **Colon `:`** to end the header line.\n",
"- **Indented code block** that makes up the body of the function.\n",
"- **(Optional) `return`** to return a value from the function. If omitted, the function returns `None` by default.\n",
"\n",
"\n",
"**Simple example** of a function to calculate and return the sum of two numbers.\n",
"\n",
"The animation below shows the path of the data in this example. The values 3 and 4 enter the function through the parameters, the calculation happens inside it, and `return` sends the 7 back to the program. What is active lights up in the code and in the drawing at the same time, and the color indicates the phase: blue on the way in, purple inside the function and orange on the way out.\n",
"\n",
"```{image} funcao-soma2-light.gif\n",
":alt: Animation in nineteen frames of the first example of the chapter, the function sum2, called with sum2 of 3 and 4. On the left, the code, with an arrow on the line that is running, and the output. On the right, the function drawn as a box with two inputs at the top, one for parameter1 and one for parameter2, and an output at the bottom, labelled return, that goes down to the program box, where the variable result is. The values 3 and 4 leave the call and go in through the two inputs. Inside the box, the expression parameter1 plus parameter2 becomes 3 plus 4 and gives 7. The 7 leaves through the return to the result of the program, and print shows it in the output. What is active lights up in the code and in the drawing at the same time, in blue on the way in, in purple inside the function and in orange on the way out\n",
":class: only-light\n",
"```\n",
"\n",
"```{image} funcao-soma2-dark.gif\n",
":alt: Animation in nineteen frames of the first example of the chapter, the function sum2, called with sum2 of 3 and 4. On the left, the code, with an arrow on the line that is running, and the output. On the right, the function drawn as a box with two inputs at the top, one for parameter1 and one for parameter2, and an output at the bottom, labelled return, that goes down to the program box, where the variable result is. The values 3 and 4 leave the call and go in through the two inputs. Inside the box, the expression parameter1 plus parameter2 becomes 3 plus 4 and gives 7. The 7 leaves through the return to the result of the program, and print shows it in the output. What is active lights up in the code and in the drawing at the same time, in blue on the way in, in purple inside the function and in orange on the way out\n",
":class: only-dark\n",
"```\n",
"\n",
"The same code, to run on the page:\n",
"```python\n",
"def sum2(parameter1, parameter2):\n",
" total = parameter1 + parameter2\n",
" return total\n",
"\n",
"# Calling the function\n",
"result = sum2(3, 4)\n",
"print(result)\n",
"```\n",
"\n",
"**Code Output:**\n",
"```\n",
"7\n",
"```\n",
"\n",
"Notice the order in which this code runs. The `def` only creates the function `sum2`: no line of its body runs at that moment. The body runs when the function is **called**, on the line `result = sum2(3, 4)`, and it runs again on every new call.\n",
"\n",
"The names in parentheses in the definition, `parameter1` and `parameter2`, are the **parameters**: variables of the function that receive values on each call. The values written in the call, `3` and `4`, are the **arguments**. They are passed in the order they appear: the first argument goes to the first parameter and the second to the second. Inside the function, then, `parameter1` is 3 and `parameter2` is 4.\n",
"\n",
"### Docstrings and Type Hints\n",
"\n",
"Two practices make a function easier to understand and to use: **docstrings** and **type hints**.\n",
"\n",
"- **Docstrings** are documentation strings written right below the `def` line. They say what the function does, what each parameter receives and what it returns, and they show up when you call `help()` on the function.\n",
"\n",
"- **Type hints** indicate the expected type of each parameter and of the return value. They help whoever reads the code and the tools that check types.\n",
"\n",
" **Docstring and type hints example:**\n",
"\n",
" ```python\n",
" def sum_numbers(a: float, b: float) -> float:\n",
" \"\"\"\n",
" Calculates the sum of two numbers.\n",
"\n",
" Args:\n",
" a (float): First number.\n",
" b (float): Second number.\n",
" \n",
" Returns:\n",
" float: The sum of 'a' and 'b'.\n",
" \"\"\"\n",
" return a + b\n",
" ```\n",
"\n",
" In this example, `a` and `b` should be `float`, and `sum_numbers` returns a `float`. Calling `help(sum_numbers)` shows the docstring.\n",
"\n",
" Python, however, does not check or convert these types at run time: `sum_numbers(2, 3)` returns `5`, an `int`, and even `sum_numbers(\"Py\", \"thon\")` runs and returns `'Python'`. The annotations are for the people who read the code and for tools such as mypy, which check the types without running the program.\n",
"\n",
"\n",
"### Functions without Return (Procedures)\n",
"\n",
"A function without `return`, also called a procedure, performs an action, such as showing something on the screen, and gives back no value (it returns `None`, implicitly).\n",
"\n",
"**Example:**\n",
"\n",
"```python\n",
"def print_header(title: str) -> None:\n",
" \"\"\"\n",
" Prints a formatted header with borders.\n",
"\n",
" Args:\n",
" title: Text to be displayed in the center of the header.\n",
" \"\"\"\n",
" width = len(title) + 6\n",
" print(\"=\" * width)\n",
" print(f\"| {title} |\")\n",
" print(\"=\" * width)\n",
"\n",
"# Calling the function\n",
"print_header(\"MONTHLY REPORT\")\n",
"```\n",
"\n",
"**Code Output:**\n",
"```\n",
"====================\n",
"| MONTHLY REPORT |\n",
"====================\n",
"```\n",
"\n",
"Notice how the `-> None` type hint makes it explicit that this function exists for its **side effect** (printing to the screen), not to return a value, which is typical of a procedure.\n",
"\n",
"### Functions with Return\n",
"\n",
"A function with a return gives a value back to whoever called it, which can store it in a variable or use it in a calculation. Next, an example without a default value and one with a default value.\n",
"\n",
"**Example without Default Value:**\n",
"\n",
"```python\n",
"def sum_numbers(a: float, b: float) -> float:\n",
" \"\"\"\n",
" Calculates the sum of two numbers.\n",
" \n",
" Args:\n",
" a: First number to be added.\n",
" b: Second number to be added.\n",
" \n",
" Returns:\n",
" The sum of the values 'a' and 'b'.\n",
" \"\"\"\n",
" return a + b\n",
"\n",
"# Calling the function with arguments\n",
"result = sum_numbers(3.5, 5.0)\n",
"print(f\"Result: {result}\")\n",
"```\n",
"\n",
"**Code Output:**\n",
"```\n",
"Result: 8.5\n",
"```\n",
"\n",
"**Example with Default Value:**\n",
"\n",
"```python\n",
"def sumP(a: float, b: float = 0.0) -> float:\n",
" \"\"\"\n",
" Calculates the sum of two numbers.\n",
"\n",
" Args:\n",
" a: First number to be added.\n",
" b: Second number to be added, default is 0.0.\n",
" \n",
" Returns:\n",
" The sum of the values 'a' and 'b'.\n",
" \"\"\"\n",
" return a + b\n",
"\n",
"# Calling the function with different arguments\n",
"result1 = sumP(3.5, 5.0) # Two arguments\n",
"result2 = sumP(3.5) # Using the default value of b\n",
"print(f\"Results: {result1}, {result2}\")\n",
"```\n",
"\n",
"**Code Output:**\n",
"```\n",
"Results: 8.5, 3.5\n",
"```\n",
"\n",
"In the second example, `b` has the default value `0.0`. A parameter with a default value is optional in the call: when the argument is left out, as in `sumP(3.5)`, the parameter takes the value written in the definition.\n",
"\n",
"### `return` ends the function\n",
"\n",
"When a `return` runs, the function ends right there and gives back the value; the lines below it do not run. This makes it possible to write a decision as a sequence of tests, each with its own `return`:\n",
"\n",
"```python\n",
"def status(grade: float) -> str:\n",
" if grade >= 7:\n",
" return \"passed\"\n",
" if grade >= 5:\n",
" return \"make-up exam\"\n",
" return \"failed\"\n",
"\n",
"print(status(8.5))\n",
"print(status(6.0))\n",
"print(status(3.0))\n",
"```\n",
"\n",
"**Code Output:**\n",
"```\n",
"passed\n",
"make-up exam\n",
"failed\n",
"```\n",
"\n",
"With the grade `8.5`, the first `return` already ends the function, and the second test is never evaluated. The last `return` is reached only when none of the previous tests succeeded, so there is no need for `elif` or `else`. The same holds inside a loop: a `return` inside a `for` ends the loop and the function at once.\n",
"\n",
"### `print` is not `return`\n",
"\n",
"A common confusion at the start is to think that a function that **prints** a value also **returns** it. They are different things: `print` shows the value on the screen, and the program does not keep it; `return` hands the value to whoever called the function, which can store it in a variable or use it in a calculation.\n",
"\n",
"```python\n",
"def double_prints(x: int) -> None:\n",
" print(x * 2)\n",
"\n",
"def double_returns(x: int) -> int:\n",
" return x * 2\n",
"\n",
"a = double_prints(5)\n",
"b = double_returns(5)\n",
"print(a)\n",
"print(b)\n",
"```\n",
"\n",
"**Code Output:**\n",
"```\n",
"10\n",
"None\n",
"10\n",
"```\n",
"\n",
"The first call prints `10` right away, but returns nothing: `a` receives `None`, the value every function without `return` gives back. The second prints nothing; the `10` goes to `b` and only shows up on the screen when the program prints `b`. That is why only the second one can continue a calculation: `double_returns(5) + 1` is `11`, while `double_prints(5) + 1` prints `10` and stops with an error (`TypeError`), because `None` cannot be added to `1`. If the value will be used later, the function should use `return`.\n",
"\n",
"### Positional and Keyword Arguments\n",
"\n",
"In a call, arguments can be passed in two ways: **positional** and **keyword**.\n",
"\n",
"- **Positional arguments:** go to the parameters in the order they appear.\n",
"\n",
" **Example:**\n",
" ```python\n",
" def display_info(name: str, age: int) -> None:\n",
" print(f\"Name: {name}, Age: {age}\")\n",
"\n",
" # Calling the function with positional arguments\n",
" display_info(\"Alice\", 30)\n",
" ```\n",
"\n",
" **Code Output:**\n",
" ```\n",
" Name: Alice, Age: 30\n",
" ```\n",
"\n",
"- **Keyword arguments:** carry the name of the parameter, in the form `name=value`, so the order does not matter.\n",
"\n",
" **Example:**\n",
" ```python\n",
" def display_info(name: str, age: int) -> None:\n",
" print(f\"Name: {name}, Age: {age}\")\n",
"\n",
" # Calling the function with keyword arguments\n",
" display_info(age=30, name=\"Alice\")\n",
" ```\n",
" \n",
" **Code Output:**\n",
" ```\n",
" Name: Alice, Age: 30\n",
" ```\n",
"\n",
"The two forms can be mixed, as long as the positional arguments come first: `display_info(\"Alice\", age=30)` works, and `display_info(name=\"Alice\", 30)` is a syntax error.\n",
"\n",
"### Scope: local and global variables\n",
"\n",
"A variable created inside a function is **local**: it exists only while the function runs and can only be used inside it. Parameters are local variables too. A variable created outside any function is **global**.\n",
"\n",
"```python\n",
"def compute_average(grade1: float, grade2: float) -> float:\n",
" average = (grade1 + grade2) / 2\n",
" return average\n",
"\n",
"result = compute_average(8.0, 6.0)\n",
"print(result)\n",
"```\n",
"\n",
"**Code Output:**\n",
"```\n",
"7.0\n",
"```\n",
"\n",
"The variable `average` exists only during the call; when the function ends, only its value comes out, through `return`. A `print(average)` outside the function would stop the program with a `NameError`, because outside it that name does not exist. To use the average outside the function, use the returned value, as `result` does.\n",
"\n",
"A global variable, on the other hand, can be read inside the function:\n",
"\n",
"```python\n",
"rate = 0.25\n",
"\n",
"def price_with_rate(price: float) -> float:\n",
" return price * (1 + rate)\n",
"\n",
"print(price_with_rate(50.0))\n",
"```\n",
"\n",
"**Code Output:**\n",
"```\n",
"62.5\n",
"```\n",
"\n",
"Assigning a value to a name inside the function, however, creates a new local variable, even if a global with the same name exists, and the global does not change. Python has the `global` keyword to change a global variable from inside a function, but the clearer way is for the function to return the value with `return` and let the program decide what to do with it.\n",
"\n",
"### Functions with Multiple Return Values\n",
"\n",
"A function can return more than one value in a single call, separating them with commas in the `return`.\n",
"\n",
"**Example: Returning Two Values**\n",
"\n",
"In the example below, `sum_diff_values` returns the sum and the difference of two numbers.\n",
"\n",
"```python\n",
"def sum_diff_values(a: float, b: float) -> tuple[float, float]:\n",
" \"\"\"\n",
" Calculates the sum and difference of two numbers.\n",
"\n",
" Args:\n",
" a: First number.\n",
" b: Second number.\n",
"\n",
" Returns:\n",
" A tuple containing the sum and difference of the numbers.\n",
" \"\"\"\n",
" sum_result = a + b\n",
" diff_result = a - b\n",
" return sum_result, diff_result\n",
"\n",
"# Calling the function\n",
"sum_values, diff_values = sum_diff_values(10, 5)\n",
"print(f\"The sum is: {sum_values}\")\n",
"print(f\"The difference is: {diff_values}\")\n",
"```\n",
"\n",
"**Code Output:**\n",
"```\n",
"The sum is: 15\n",
"The difference is: 5\n",
"```\n",
"\n",
"The results come out as `15` and `5`, with no decimal part: the arguments `10` and `5` are integers, and the `float` annotation does not convert them.\n",
"\n",
"The values come back together in a tuple, and the call can \"unpack\" them into separate variables, as in `sum_values, diff_values = sum_diff_values(10, 5)`.\n",
"\n",
"**Example: Returning Values of Different Types**\n",
"\n",
"Next, the `analyze_list` function returns the sum of the elements in a list and a boolean value indicating whether all numbers are positive.\n",
"\n",
"```python\n",
"def analyze_list(numbers: list[int]) -> tuple[int, bool]:\n",
" \"\"\"\n",
" Analyzes a list of integers.\n",
"\n",
" Args:\n",
" numbers: List of integers.\n",
"\n",
" Returns:\n",
" - The sum of the elements.\n",
" - A boolean value indicating if all numbers are positive.\n",
" \"\"\"\n",
" total_sum = sum(numbers)\n",
" \n",
" # Checking if all numbers are positive without using all\n",
" all_positive = True\n",
" for num in numbers:\n",
" if num <= 0:\n",
" all_positive = False\n",
" break\n",
"\n",
" return total_sum, all_positive\n",
"\n",
"# Calling the function\n",
"sum_res, positive_res = analyze_list([3, -1, 5, 7, 2])\n",
"print(f\"Sum of elements: {sum_res}\")\n",
"print(f\"Are all elements positive? {positive_res}\")\n",
"```\n",
"\n",
"**Code Output:**\n",
"```\n",
"Sum of elements: 16\n",
"Are all elements positive? False\n",
"```\n",
"\n",
"The function returns the sum of the elements and checks if all are positive, returning `False` if any is not.\n",
"\n",
"### Functions that Receive Other Functions as Arguments\n",
"\n",
"In Python, a function can be passed as an argument to another, like any other value.\n",
"\n",
"**Example: Passing Functions as Arguments**\n",
"\n",
"The example below shows a function that applies an operation, defined by another function, to each element of a list.\n",
"\n",
"```python\n",
"def apply_operation(operation, my_list: list[int]) -> list[int]:\n",
" \"\"\"\n",
" Applies an operation to each element of a list.\n",
"\n",
" Args:\n",
" operation: Function that defines the operation to be applied.\n",
" my_list: List of elements.\n",
"\n",
" Returns:\n",
" A new list with the results of the applied operation.\n",
" \"\"\"\n",
" return [operation(element) for element in my_list]\n",
"\n",
"# Functions for mathematical operations\n",
"def square(x: int) -> int:\n",
" return x * x\n",
"\n",
"def cube(x: int) -> int:\n",
" return x * x * x\n",
"\n",
"# Applying the square function to a list\n",
"numbers = [1, 2, 3, 4, 5]\n",
"square_result = apply_operation(square, numbers)\n",
"print(f\"The square of each number in the list is: {square_result}\")\n",
"\n",
"# Applying the cube function to a list\n",
"cube_result = apply_operation(cube, numbers)\n",
"print(f\"The cube of each number in the list is: {cube_result}\")\n",
"\n",
"```\n",
"\n",
"**Code Output:**\n",
"```\n",
"The square of each number in the list is: [1, 4, 9, 16, 25]\n",
"The cube of each number in the list is: [1, 8, 27, 64, 125]\n",
"```\n",
"- The `apply_operation` function receives:\n",
" - A function (`operation`) that defines the transformation logic.\n",
" - A list (`my_list`) whose elements will be processed.\n",
"- We use a list comprehension to apply the operation to each element of the list.\n",
"- The `square` and `cube` functions are examples of operations that can be passed to `apply_operation`.\n",
"\n",
"That way, `apply_operation` works for any operation that takes a number and returns another.\n"
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "-6SCX2vvz_Bp"
},
"source": [
"## Anonymous Functions (Lambda)\n",
"\n",
"An anonymous function, or lambda, is a short function, without a name, written on a single line: the `lambda` keyword, the parameters, a colon (`:`) and the expression it returns.\n",
"\n",
"The basic syntax of a lambda function is:\n",
"\n",
"```text\n",
"lambda parameters: expression\n",
"```\n",
"\n",
"### Example with one parameter\n",
"```python\n",
"print((lambda x: x * 2)(3))\n",
"```\n",
"\n",
"**Code Output:**\n",
"```\n",
"6\n",
"```\n",
"\n",
"In this example, a lambda function that doubles a number is defined and immediately called with the value `3`. The function calculates `3 * 2` and returns `6`, which `print` displays on the screen.\n",
"\n",
"\n",
"### Example with two parameters\n",
"```python\n",
"add_nums = lambda x, y: x + y\n",
"print(f\"The sum of 5 and 7 is: {add_nums(5, 7)}\")\n",
"```\n",
"**Code Output:**\n",
"```\n",
"The sum of 5 and 7 is: 12\n",
"```\n",
"\n",
"Here, the lambda receives two parameters, `x` and `y`, and returns their sum. Assigned to the variable `add_nums`, it is called like a regular function. The PEP 8 style guide recommends `def` when the function will get a name; the example does it this way only to show the syntax.\n",
"\n",
"\n",
"### Example with lists and `map()`\n",
"```python\n",
"numbers = [1, 2, 3, 4, 5]\n",
"squares = list(map(lambda x: x ** 2, numbers))\n",
"print(f\"The squares of the numbers {numbers} are: {squares}\")\n",
"```\n",
"**Code Output:**\n",
"```\n",
"The squares of the numbers [1, 2, 3, 4, 5] are: [1, 4, 9, 16, 25]\n",
"```\n",
"\n",
"The `map()` function applies the lambda `lambda x: x ** 2` to each element of `numbers` and returns a `map` object, which `list()` turns into a list.\n",
"\n",
"\n",
"### Example with multiple returns\n",
"```python\n",
"square_and_cube = lambda x: (x ** 2, x ** 3)\n",
"result = square_and_cube(5)\n",
"print(f\"The square and cube of 5 are: {result}\")\n",
"```\n",
"**Code Output:**\n",
"```\n",
"The square and cube of 5 are: (25, 125)\n",
"```\n",
"\n",
"The lambda returns **two values** in a **tuple**: the square and the cube of `x`. With the argument `5`, it returns `(25, 125)`.\n",
"\n",
"Lambdas are for short functions, a single expression, used once, usually as an argument to another function, such as `map()` and `filter()`. `reduce()`, which also takes a function, is not *built-in*: it lives in the `functools` module and needs `from functools import reduce`. For anything bigger than one expression, `def` is more readable.\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Recursive Functions\n",
"\n",
"**Recursion** is a technique in which a function calls itself to solve a smaller version of the same problem. With each new call, the problem is reduced to a simpler case, until it reaches a situation whose answer is known directly. This stopping point is called the **base case**. Without it, the function would keep calling itself indefinitely and the program would crash with a stack overflow.\n",
"\n",
"Every recursive function therefore combines two parts:\n",
"\n",
"- **Base case:** condition that ends the recursion without making a new call.\n",
"- **Recursive case:** call to the function itself with a smaller input.\n",
"\n",
"It works well for problems that contain smaller versions of themselves, such as factorial, Fibonacci, tree traversals and list processing, and it usually gives shorter code than the version with a loop.\n",
"\n",
"### Example: Sum of Numbers from 1 to N\n",
"\n",
"The sum of numbers from 1 to a number `n` can be calculated recursively:\n",
"\n",
"```python\n",
"def sum_recursive(n: int) -> int:\n",
" \"\"\"\n",
" Sums the positive integers from 1 to n.\n",
"\n",
" Args:\n",
" n: Upper bound of the sum.\n",
"\n",
" Returns:\n",
" The sum 1 + 2 + ... + n. For n <= 0, returns 0.\n",
" \"\"\"\n",
" if n <= 0: # Protection: avoids infinite recursion for non-positive values\n",
" return 0\n",
" if n == 1: # Base case\n",
" return 1\n",
" return n + sum_recursive(n - 1) # Recursive case\n",
"\n",
"# Testing the function\n",
"print(sum_recursive(5))\n",
"```\n",
"\n",
"**Code Output:**\n",
"```\n",
"15\n",
"```\n",
"\n",
"The function `sum_recursive(5)` calls `sum_recursive(4)`, which calls `sum_recursive(3)`, and so on, until it reaches `sum_recursive(1)`, which returns 1. The values are added back along the call chain.\n",
"\n",
"**Recursive Representation**\n",
"\n",
"The function's execution can be visualized as a call stack. In the animation below, each call is a step that waits for the answer of the step below it. On the way down, in blue, `sum_recursive(5)` calls `sum_recursive(4)`, and so on, until `sum_recursive(1)`. That is the base case, in purple, which answers 1 without making a new call. On the way back, in orange, each answer climbs one step and is added there: 1, 3, 6, 10 and 15. A step that has already answered fades, and the stack empties.\n",
"\n",
"```{image} pilha-soma-light.gif\n",
":alt: Animation in thirty-nine frames of the call stack of sum_recursive of 5. On the left, the code of the function sum_recursive without the docstring and the comments, with an arrow on the line that is running, and the output. On the right, a staircase of five steps, one per call, each with the call on top and, below it, what it is waiting for. On the way down, sum_recursive of 5 waits for 5 plus sum_recursive of 4 and calls sum_recursive of 4, which opens the next step, and so on until sum_recursive of 1, the base case, which answers 1 directly. On the way back, the answer climbs one step at a time and is added there, 2 plus 1 equals 3, 3 plus 3 equals 6, 4 plus 6 equals 10 and 5 plus 10 equals 15, and the step that has already answered fades. At the end print shows 15. What is active lights up in the code and on the staircase at the same time, in blue on the way down, in purple at the base case and in orange on the way back\n",
":class: only-light\n",
"```\n",
"\n",
"```{image} pilha-soma-dark.gif\n",
":alt: Animation in thirty-nine frames of the call stack of sum_recursive of 5. On the left, the code of the function sum_recursive without the docstring and the comments, with an arrow on the line that is running, and the output. On the right, a staircase of five steps, one per call, each with the call on top and, below it, what it is waiting for. On the way down, sum_recursive of 5 waits for 5 plus sum_recursive of 4 and calls sum_recursive of 4, which opens the next step, and so on until sum_recursive of 1, the base case, which answers 1 directly. On the way back, the answer climbs one step at a time and is added there, 2 plus 1 equals 3, 3 plus 3 equals 6, 4 plus 6 equals 10 and 5 plus 10 equals 15, and the step that has already answered fades. At the end print shows 15. What is active lights up in the code and on the staircase at the same time, in blue on the way down, in purple at the base case and in orange on the way back\n",
":class: only-dark\n",
"```\n",
"\n",
"**Accumulated resolution on the way back:**\n",
"\n",
"```text\n",
"sum_recursive(1) = 1\n",
"sum_recursive(2) = 2 + sum_recursive(1) = 2 + 1 = 3\n",
"sum_recursive(3) = 3 + sum_recursive(2) = 3 + 3 = 6\n",
"sum_recursive(4) = 4 + sum_recursive(3) = 4 + 6 = 10\n",
"sum_recursive(5) = 5 + sum_recursive(4) = 5 + 10 = 15\n",
"```\n",
"\n",
"```{admonition} Warning - Recursion depth limit\n",
":class: warning\n",
"Each recursive call consumes a slot on the Python execution stack. The interpreter limits this stack to about 1000 calls by default. For small inputs like `sum_recursive(5)`, this is not a problem, but calling `sum_recursive(2000)` raises `RecursionError`. For very large problems, prefer an equivalent iterative version.\n",
"```\n",
"\n",
"### Example: Sum of Numbers in a Range\n",
"\n",
"To calculate the sum of numbers between `a` and `b`:\n",
"\n",
"```python\n",
"def sum_range(a: int, b: int) -> int:\n",
" \"\"\"\n",
" Sums the integers in the range [a, b], inclusive on both ends.\n",
"\n",
" Args:\n",
" a: Lower bound of the range.\n",
" b: Upper bound of the range.\n",
"\n",
" Returns:\n",
" The sum a + (a+1) + ... + b. Returns 0 if a > b.\n",
" \"\"\"\n",
" if a > b: # Protection: invalid range\n",
" return 0\n",
" if a == b: # Base case: we reached the upper bound\n",
" return a\n",
" return a + sum_range(a + 1, b) # Recursive case\n",
"\n",
"# Testing the function\n",
"print(sum_range(3, 7))\n",
"```\n",
"\n",
"**Code Output:**\n",
"```\n",
"25\n",
"```\n",
"\n",
"On each call, `a` goes up by 1. The recursion stops when `a` reaches `b`, the base case, which returns `a` itself.\n",
"\n",
"\n",
"### Example: Factorial of a Number\n",
"\n",
"The factorial of a number `n`, denoted `n!`, is the product of all positive integers from 1 to `n`. It counts, for example, in how many different orders 5 distinct books can be arranged on a shelf: there are 5 options for the first book, 4 for the second, 3 for the third, and so on, and `5! = 5 × 4 × 3 × 2 × 1 = 120`. After choosing the first book, the same problem remains with one book fewer, and that is what recursion takes advantage of: `5! = 5 × 4!`.\n",
"\n",
"```python\n",
"def factorial(n: int) -> int:\n",
" \"\"\"\n",
" Calculates the factorial of n (n!).\n",
"\n",
" Args:\n",
" n: Non-negative integer.\n",
"\n",
" Returns:\n",
" The product 1 × 2 × ... × n. By convention, 0! = 1.\n",
" Returns 0 for negative inputs (invalid value).\n",
" \"\"\"\n",
" if n < 0: # Protection: factorial is not defined for negative numbers\n",
" return 0\n",
" if n == 0 or n == 1: # Base case\n",
" return 1\n",
" return n * factorial(n - 1) # Recursive case\n",
"\n",
"# How many ways are there to arrange 5 distinct books on a shelf?\n",
"print(factorial(5))\n",
"```\n",
"\n",
"**Code Output:**\n",
"```\n",
"120\n",
"```\n",
"\n",
"The function calls itself with `n - 1` until `n` is 0 or 1, returning 1. In our example, `factorial(5)` computes `5 × factorial(4) = 5 × 24 = 120`, meaning there are 120 distinct ways to arrange the 5 books on the shelf.\n",
"\n",
"```{admonition} Note - Why 0! equals 1\n",
":class: note\n",
"By mathematical convention, the factorial of 0 is 1 (not 0). This definition keeps the recurrence `n! = n × (n-1)!` consistent for `n = 1`, since `1! = 1 × 0! = 1 × 1 = 1`. It also appears naturally in combinatorial formulas, such as the number of ways to arrange zero objects, which is exactly one way (the empty arrangement).\n",
"```\n",
"\n",
"### Example: Sum of Elements in a List\n",
"\n",
"The sum of elements in a list can be done recursively:\n",
"\n",
"```python\n",
"def sum_list(my_list: list[int]) -> int:\n",
" \"\"\"\n",
" Sums all elements of an integer list recursively.\n",
"\n",
" Args:\n",
" my_list: List of integers.\n",
"\n",
" Returns:\n",
" The sum of the elements. For an empty list, returns 0.\n",
" \"\"\"\n",
" if len(my_list) == 0: # Base case: empty list\n",
" return 0\n",
" return my_list[0] + sum_list(my_list[1:]) # Recursive case\n",
"\n",
"# Testing the function\n",
"example_list = [1, 2, 3, 4, 5]\n",
"print(f\"The sum of numbers in the list is: {sum_list(example_list)}\")\n",
"```\n",
"\n",
"**Code Output:**\n",
"```\n",
"The sum of numbers in the list is: 15\n",
"```\n",
"\n",
"The function takes the first element of the list and calls itself with the rest. The process continues until the list is empty.\n",
"\n",
"```{admonition} Tip - Cost of slicing in recursion\n",
":class: tip\n",
"The expression `my_list[1:]` creates a new list on every recursive call. For a list of size `n`, this results in `n` partial copies throughout the recursion, totaling `O(n²)` in memory and time. For large lists, prefer passing an index as an additional argument and walking the list by position, rather than slicing.\n",
"```\n",
"\n",
"### Advantages and Disadvantages of Recursion\n",
"\n",
"| **Advantages** | **Disadvantages** |\n",
"|---|---|\n",
"| Shorter and more expressive code, close to the mathematical definition of the problem. | Each call consumes memory on the execution stack (parameters, local variables, and return address). |\n",
"| Ideal for problems with naturally recursive structure: factorial, Fibonacci, tree traversals, and linked-list processing. | Python limits call depth to about 1000 by default (`sys.setrecursionlimit()`). Exceeding this limit raises `RecursionError: maximum recursion depth exceeded`. |\n",
"| Avoids auxiliary variables and loops when the solution is defined in a self-referential way. | Without a well-defined base case, a stack overflow occurs from calls that never end. |\n",
"\n",
"For large inputs, where the number of calls approaches the stack depth limit, an iterative version of the same algorithm is usually safer. Compare the recursive factorial already presented with its equivalent iterative version:\n",
"\n",
"```python\n",
"def factorial_iterative(n: int) -> int:\n",
" \"\"\"Iterative version of the factorial, without recursion.\"\"\"\n",
" if n < 0:\n",
" return 0\n",
" result = 1\n",
" for i in range(2, n + 1):\n",
" result *= i\n",
" return result\n",
"\n",
"print(factorial_iterative(5))\n",
"```\n",
"\n",
"**Code Output:**\n",
"```\n",
"120\n",
"```\n",
"\n",
"The iterative version uses only one variable (`result`) and a loop, without stacking calls. For small inputs, the two versions are interchangeable. For large values (for example, `n = 10000`), the recursive version raises `RecursionError`, while the iterative version completes normally.\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Other *Built-in* Functions\n",
"\n",
"The previous chapters already used several *built-in* functions, which come ready in Python, without `import`: `print()`, `input()`, `int()`, `float()`, `str()`, `len()` and `range()`. The table shows others that have not appeared yet.\n",
"\n",
"{.codigo-sem-quebra}\n",
"| Function | What it does |\n",
"| -------- | ------------ |\n",
"| `max()` | Returns the largest value in a sequence.
`max([2, 8, 5])` ➞ `8` |\n",
"| `min()` | Returns the smallest value in a sequence.
`min([2, 8, 5])` ➞ `2` |\n",
"| `sum()` | Adds up the elements of a sequence of numbers.
`sum([1, 2, 3])` ➞ `6` |\n",
"| `sorted()` | Returns a sorted list.
`sorted([3, 1, 2])` ➞ `[1, 2, 3]` |\n",
"| `all()` | Returns `True` if all elements are truthy.
`all([True, True, False])` ➞ `False` |\n",
"| `enumerate()` | Yields (index, value) pairs while going through a sequence.
`list(enumerate(['a', 'b']))` ➞ `[(0, 'a'), (1, 'b')]` |\n",
"| `zip()` | Joins the elements of several sequences into tuples.
`list(zip([1, 2], ['a', 'b']))` ➞ `[(1, 'a'), (2, 'b')]` |\n",
"| `hash()` | Returns an integer computed from the object. For strings, it changes on every run of the program.
`hash(\"text\")` ➞ an integer |\n",
"| `help()` | Shows the documentation of an object.
`help(print)` ➞ the documentation of `print` |\n",
"| `dir()` | Lists the attributes and methods of an object.
`dir(\"text\")` ➞ the methods of the string |\n",
"| `open()` | Opens a file and returns a file object.
`open('file.txt', 'r')` ➞ a file object |\n",
"| `eval()` | Computes an expression written in a string.
`eval(\"2 + 2\")` ➞ `4` |\n",
"| `exec()` | Runs Python code written in a string.
`exec(\"x = 5\")` ➞ creates the variable `x` |\n",
"\n",
"---\n",
"\n",
"```{admonition} Warning - Risks of eval and exec\n",
":class: warning\n",
"`eval()` and `exec()` execute Python code contained in a string. This is dangerous when the string comes from an external source (user input, file, network request), because it allows arbitrary code execution. For example, `eval(input())` lets anyone run any command inside your program.\n",
"\n",
"For simple text-to-number conversions, use `int()`, `float()`, or `bool()`. To safely evaluate Python literals (lists, dictionaries, tuples, numbers), use `ast.literal_eval()` from the `ast` module. Avoid `eval` and `exec` in real code, except in very specific cases where you fully control the input.\n",
"```\n",
"\n",
"```{admonition} Note - the math module\n",
":class: note\n",
"Functions such as `math.sqrt()` and `math.factorial()` are not *built-in*: they live in the `math` module, which must be imported with `import math`.\n",
"```\n",
"\n",
"Example:\n",
"\n",
"```python\n",
"import math\n",
"root = math.sqrt(16)\n",
"print(root)\n",
"```\n",
"\n",
"**Code Output:**\n",
"```\n",
"4.0\n",
"```\n",
"\n",
"```{admonition} Official documentation\n",
":class: seealso\n",
"For the full list of *built-in* functions and details about each one, see the [official Python documentation](https://docs.python.org/3/library/functions.html).\n",
"```\n",
"\n",
"## Organizing a Program into Functions\n",
"\n",
"So far, each example showed a function on its own. In a real program, they work together: each one does part of the task, and the main program just calls them in the right order. That is what the opening of the chapter called modularization.\n",
"\n",
"The program below takes two grades, computes the average and shows the student's status, all in a single block:\n",
"\n",
"```python\n",
"grade1 = 8.0\n",
"grade2 = 6.0\n",
"average = (grade1 + grade2) / 2\n",
"if average >= 7:\n",
" status = \"passed\"\n",
"elif average >= 5:\n",
" status = \"make-up exam\"\n",
"else:\n",
" status = \"failed\"\n",
"print(f\"Average: {average:.1f} ({status})\")\n",
"```\n",
"\n",
"**Code Output:**\n",
"```\n",
"Average: 7.0 (passed)\n",
"```\n",
"\n",
"The same program, organized into functions:\n",
"\n",
"```python\n",
"def compute_average(grade1: float, grade2: float) -> float:\n",
" average = (grade1 + grade2) / 2\n",
" return average\n",
"\n",
"def status(grade: float) -> str:\n",
" if grade >= 7:\n",
" return \"passed\"\n",
" if grade >= 5:\n",
" return \"make-up exam\"\n",
" return \"failed\"\n",
"\n",
"def show_result(average: float) -> None:\n",
" print(f\"Average: {average:.1f} ({status(average)})\")\n",
"\n",
"def main() -> None:\n",
" average = compute_average(8.0, 6.0)\n",
" show_result(average)\n",
"\n",
"main()\n",
"```\n",
"\n",
"**Code Output:**\n",
"```\n",
"Average: 7.0 (passed)\n",
"```\n",
"\n",
"Both versions print the same thing. The second is longer, but each function has a single task and a name that says which one, and `main()` reads like a summary of the program: compute the average and show the result. To change the passing rule, you only touch `status`; to change the output format, only `show_result`. And `compute_average` and `status` are the same ones from the scope and `return` sections: a function written once is reused in another program.\n",
"\n",
"Calling the main function `main` is a convention, not a Python rule. Chapter 8 takes the next step: keeping functions in separate files, the **modules**, and using them with `import`.\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Exercises\n",
"\n",
"1. **Minimum and Maximum of Three Numbers**\n",
"\n",
"Develop a program that determines the smallest and largest number among three values provided by the user. The program must use functions to perform the necessary operations. Follow the instructions:\n",
"\n",
"- Create a function called `read_numbers()` that asks the user to enter three numbers and returns these values.\n",
"\n",
"- Implement a function called `find_min_max(a, b, c)` that receives three numbers as parameters and returns a tuple containing the minimum and maximum value, in that order.\n",
"\n",
"- In the main program, use the created functions to:\n",
" - Obtain the three numbers from the user.\n",
" - Calculate the minimum and maximum value.\n",
" - Display the results.\n",
"\n",
"Example of use in the main program:\n",
"\n",
"```python\n",
"num1, num2, num3 = read_numbers()\n",
"minimum, maximum = find_min_max(num1, num2, num3)\n",
"print(f\"The smallest number is: {minimum}\")\n",
"print(f\"The largest number is: {maximum}\")\n",
"```\n",
"\n",
"