THEORY AND PRACTICE
-
Input and Output Data
- Tasks
-
Conditions
- Tasks
-
For Loop
- Tasks
-
Strings
- Tasks
-
While Loop
- Tasks
-
Lists
- Tasks
-
Two-Dimensional Arrays
- Tasks
-
Dictionaries
- Tasks
-
Sets
- Tasks
-
Functions and Recursion
- Tasks
Lesson 10. Functions and recursions in python
Introduction to code
Python function: purpose, examples
A function in Python is a named, independent block of code that performs a specific task. You can call this block of code by name in any part of your program whenever you need to complete this task. You have already used the built-in functions, for example, print() to display information on the screen or len() to determine the length of an object.
# Example of using the built-in print() function
print("Hello world!")
# An example of using the built-in len()
my_list = [1, 2, 3, 4, 5]
list_length = len(my_list)
print("List length:", list_length)
Why use functions
The main reason is to follow the DRY principle (Don't Repeat Yourself). If you have a piece of code that needs to be executed in several places in the program, it is better to put it in a function. This makes the code more organized, readable, and easier to maintain. Instead of copying and pasting the same code, you just call the function.
Advantages of code reuse
Code reuse through functions provides several key advantages:
- Reliability: You debug the code once in one place. If you find an error, you will need to fix it only in the function body, and all calls to this function will start working correctly.
- Saving time: You don't have to write the same code over and over again.
- Readability: A program broken down into small, logically complete functions is much easier to understand than one large monolithic block of code.
Functions: arguments, definition in Python
Syntax of the function def in Python
To create (or define) your own The function uses the keyword def. It is followed by the function name, with parentheses () and colon :.
The syntax looks like this: def function_name():
Function body and margins
All code related to the function should be placed after the colon and indented (usually 4 spaces). The indentation shows Python where the function body begins and ends.
Example of a simple function without parameters
This function does not accept any information from the outside. It just performs the same action every time it is called.
# Definition of a simple
def greet():
"""This function simply prints a greeting."""
print("--------------------")
print("Welcome!")
print("--------------------")
# Calling
print("Start of the program.")
greet() # Calling the function here
print("The program continues.")
greet() # And we can call it again
print("End of program.")
Parameters and arguments
Parameters in the function definition
Parameters are variables that you specify in parentheses when defining a function. They serve as "placeholders" for the data that the function will receive when called.
Arguments when called
Arguments are the actual values that you pass to the function when it is called. These values are assigned to the function parameters.
Transmission of different types of data
Any type of data can be passed to the function: numbers, strings, lists, dictionaries, and even other functions.
# Definition of a function with one parameter 'name'
def personal_greet(name):
"""This function takes the name as an argument and prints a personal greeting."""
print(f"Hello, {name}! How are you?")
# Defining a function for processing different types of data
def process_data(data):
"""This function outputs the data type and its value."""
print(f" Data received: {data}")
print(f"Data type: {type(data)}")
print("---")
# Calling functions with different arguments
personal_greet("Anna") # "Anna" is the argument
personal_greet("Ivan") # "Ivan" is an argument
process_data(100) # Passing the number
process_data("This is a test string") # Passing the string
process_data([1, "two", 3.0]) # Passing the list
Function value returned
Usage return
Often a function must not just perform an action, but calculate something and return the result. The return operator is used for this. When Python encounters return, it immediately exits the function and passes the specified value to the place where the function was called.
Functions with and without return
A function without return implicitly returns a special value None. Functions that calculate something almost always use return.
4.3. Saving the result to a variable
The result returned by the function can be saved to a variable for future use.
# Function with no return value (will implicitly return None)
def print_sum(a, b):
print(f"The sum of {a} and {b} is equal to: {a + b}")
# Function with return value
def calculate_sum(a, b):
"""This function calculates the sum of two numbers and returns it."""
result = a + b
return result
# Calling a function without return
print_sum(5, 3)
result_from_print = print_sum(10, 2)
print(f"Result saved from print_sum: {result_from_print}") # Outputs None
print("-" * 20)
# Calling a function with return and saving the result
sum_result = calculate_sum(5, 3)
print(f"Result obtained from calculate_sum: {sum_result}")
# You can use the result directly
if calculate_sum(10, 20) > 25:
print("Amount greater than 25")
Default arguments
Setting default values
You can set default values for the parameters directly in the function definition. If no argument is passed for such a parameter when calling the function, the default value will be used.
Calling a function with and without arguments
This makes the function more flexible. It can be called either with all arguments specified, or omitting those for which there are default values.
# The 'power' parameter has a default value of 2
def raise_to_power(number, power=2):
"""Raises the number to the specified power. By default, in a square."""
return number ** power
# Calling a function without specifying a second argument (default value is used)
result_squared = raise_to_power(5)
print(f"5 to the power of 2 (default) = {result_squared}")
# Calling a function with the second argument
result_cubed = raise_to_power(5, 3)
print(f"5 to the power of 3 = {result_cubed}")
Named arguments, positional arguments
Passing arguments by name
In addition to passing arguments by position (the first argument for the first parameter, etc.), you can pass them by name (named or keyword arguments).
Improving code readability
Named arguments make the code much more readable, especially if the function has many parameters. You can see exactly what value is assigned to which parameter. The order of named arguments is not important.
def create_user_profile(username, age, city, status="active"):
"""Creates user profile."""
print(f"Profile created for {username}.")
print(f"Age: {age}, City: {city}, Status: {status}")
print("---")
# Positional arguments (order is important)
create_user_profile("JohnDoe", 30, "New York")
# Named (keyword) arguments (order is not important)
create_user_profile(city="Moscow", age=25, username="AnnaK")
# Mixed usage: positional first, then named
create_user_profile("PeterJ", 35, status="inactive", city="London")
Variable number of parameters, processing in Python
Arguments *args in Python
Passing any number of positional arguments
If you want the function to be able to accept any number of positional arguments, use the *args construction (the name args is a convention, you can use any other one, but with an asterisk).
Processing as a tuple
Inside the function, all arguments passed in this way are collected in a tuple.
def sum_all_numbers(*args):
"""Summarizes all passed numbers."""
print(f"Received tuple of arguments: {args}")
total = 0
for number in args:
total += number
return total
print(sum_all_numbers(1, 2, 3))
print(sum_all_numbers(10, 20, 30, 40, 50))
print(sum_all_numbers())
Argument values **kwargs in Python
Passing any number of named arguments
To accept any number of named arguments, the **kwargs (keyword arguments) construct is used.
Dictionary processing
Inside the function, kwargs becomes a dictionary, where the keys are the names of the arguments and the values are their values.
def display_user_info(**kwargs):
"""Displays user information passed as named arguments."""
print(f"Received a dictionary of arguments: {kwargs}")
for key, value in kwargs.items():
print(f"{key.capitalize()}: {value}")
print("---")
display_user_info(name="Igor", age=28, city="Samara")
display_user_info(username="tech_guru", email="guru@example.com ", has_premium=True)
Combination of *args and **kwargs
The order of placement in the function definition
You can use all types of parameters in one function. Strict order: standard parameters, *args, **kwargs.
Example of a combined function
def process_data(processor_name, *data_points, **metadata):
"""Processes data with meta information."""
print(f"Handler: {processor_name}")
print(f"Data points (tuple): {data_points}")
print(f"Sum of data points: {sum(data_points)}")
print("Metadata (dictionary):")
for key, value in metadata.items():
print(f" - {key}: {value}")
process_data("MainProcessor", 10, 20, 30, source="Sensor A", timestamp="2025-05-06")
Documentation of the function in Python, description
Description docstring in Python
A docstring is a string that is the first expression in the body of a function. It is used to describe what the function does.
Placement inside the function
Docstring is enclosed in triple quotes ("""...""" or "'..."') and is placed immediately after the line def.
Getting a description via __doc__
You can access the documentation string of a function using its special attribute __doc__.
def factorial(n):
"""
Calculates the factorial of a non-negative integer.
Args:
n (int): A non-negative integer.
Returns:
int: The factorial of the number n.
"""
if n < 0:
return "The factorial is defined only for non-negative numbers"
if n == 0:
return 1
else:
return n * factorial(n - 1)
# Print documentation string output
print(factorial.__doc__)
Recursive algorithms in Python
Defining recursion in Python
Recursion is the process by which a function calls itself to solve a problem. The recursive approach breaks down a complex task into simpler subtasks of the same type.
Termination condition
The key element of any recursive function is the base case (or termination condition). This is a condition under which the function stops calling itself and returns a specific value. Without it, the recursion will be endless.
Examples: factorial, Fibonacci in Python
Factorial of the number [ n! ] is defined as the product of all integers from 1 to [ n ]. Recursive definition: [ n! = n \times (n-1)! ], base case [ 0! = 1 ].
# Recursive function for calculating factorial
def factorial_recursive(n):
"""Recursively calculates factorial."""
# Base case
if n == 0 or n == 1:
return 1
# Recursive step
else:
return n * factorial_recursive(n - 1)
# Recursive function for Fibonacci numbers
def fibonacci_recursive(n):
"""Recursively calculates the nth Fibonacci number."""
# Base cases
if n <= 1:
return n
# Recursive step
else:
return fibonacci_recursive(n - 2) + fibonacci_recursive(n - 1)
print(f"Factorial 5: {factorial_recursive(5)}")
print(f"10th Fibonacci number: {fibonacci_recursive(10)}") # Careful, it's very slow for large n!
Recursion limits
Recursion depth
Python has a limit on the maximum number of recursive calls (recursion depth) to prevent overflow of the call stack.
Error RecursionError
If you exceed this limit, Python will generate an error RecursionError.
Limit change via sys
You can check and change this limit using the sys module. This should be done with caution.
import sys
# Get the current recursion
current_limit = sys.getrecursionlimit()
print(f"Current recursion depth limit: {current_limit}")
# Set a new limit
# sys.setrecursionlimit(2000)
# print(f"New recursion depth limit: {sys.getrecursionlimit()}")
# Example that causes RecursionError (if the standard limit is about 1000)
try:
factorial_recursive(1500)
except RecursionError as e:
print(f"Error occurred: {e}")
Alternatives to recursion
For many problems solved recursively, there are more efficient iterative (using loops) solutions.
Iterative approach to factorial
Using a for or while loop is often faster and has no restrictions on the depth of recursion.
def factorial_iterative(n):
"""Iteratively calculates the factorial."""
if n < 0: return "Error"
result = 1
for i in range(1, n + 1):
result *= i
return result
print(f"Iterative factorial 5: {factorial_iterative(5)}")
print(f"Iterative factorial 1500 (without error): {factorial_iterative(1500)}")
Iterative implementation of Fibonacci
The iterative version of the Fibonacci numbers is much more efficient than the recursive version, as it does not perform repeated calculations.
def fibonacci_iterative(n):
"""Iteratively calculates the nth Fibonacci number."""
a, b = 0, 1
for _ in range(n):
a, b = b, a + b
return a
print(f"Iterative 10th Fibonacci number: {fibonacci_iterative(10)}")
print(f"Iterative 50th Fibonacci number: {fibonacci_iterative(50)}")
Lambda functions in Python: application
Syntax lambda
Lambda functions are small anonymous (without a name) functions. They are defined using the lambda keyword. Syntax: lambda arguments: expression. They can have any number of arguments, but only one expression, the result of which they return.
Python examples
Lambda functions are useful when you need a simple function for a short time.
# The usual
def add(x, y):
return x + y
# Equivalent lambda function
add_lambda = lambda x, y: x + y
print(f"Regular function: {add(10, 5)}")
print(f"Lambda function: {add_lambda(10, 5)}")
Usage in map, filter, sorted
The main use of lambda functions is as arguments for higher-order functions such as map, filter and sorted.
numbers = [1, 2, 3, 4, 5, 6, 7, 8]
# map: applies a function to each element
squared_numbers = list(map(lambda x: x**2, numbers))
print(f"Squares of numbers (map): {squared_numbers}")
# filter: selects the elements for which the function returns True
even_numbers = list(filter(lambda x: x %2 == 0, numbers))
print(f"Even numbers (filter): {even_numbers}")
# sorted: uses lambda as the key for sorting
points = [(1, 5), (3, 2), (8, -1), (4, 4)]
# Sorting by the second element of the tuple
points_sorted = sorted(points, key=lambda point: point[1])
print(f"Sort by Y (sorted): {points_sorted}")
Functions as arguments
Transferring functions to other functions
In Python, functions are "first class objects". This means that they can be assigned to variables, stored in data structures, and, most importantly, passed as arguments to other functions.
Examples: map, filter, custom apply()
Functions that take other functions as arguments are called higher-order functions. map and filter are excellent built—in examples. We can also write our own.
def square(x):
"""Squares a number."""
return x * x
def is_odd(x):
"""Checks if the number is odd."""
return x %2 != 0
def apply_function_to_list(func, data_list):
"""
Applies the passed function 'func' to each element of the list 'data_list'.
"""
result = []
for item in data_list:
result.append(func(item))
return result
my_numbers = [1, 2, 3, 4, 5]
# Passing the square function to our custom function
squared_by_custom = apply_function_to_list(square, my_numbers)
print(f"Applied 'square' via custom apply: {squared_by_custom}")
# Using a filter with a predefined is_odd function
odd_numbers = list(filter(is_odd, my_numbers))
print(f"Filtered with 'is_odd': {odd_numbers}")