0%
Functions in Python: Organizing and Reusing Code

Functions

Structure and reuse your code effectively

10-15 min

Functions in Python: Organizing and Reusing Code

Functions are one of the fundamental concepts of programming. They let you group sequences of instructions into reusable blocks, organize code into logical components, and avoid repetition. In this tutorial, we will take an in-depth look at functions in Python, from basic syntax to advanced concepts.

What Is a Function?

A function is a named block of code that performs a specific task. Functions can accept input data (parameters), perform operations on that data, and return a result. They allow you to:

  • Reuse code without having to rewrite it
  • Simplify maintenance by compartmentalizing logic
  • Improve readability by breaking a complex problem into smaller subproblems
  • Make testing easier by isolating specific behaviors

Defining a Simple Function

In Python, functions are defined with the def keyword followed by the function name and parentheses that may contain parameters.

def saluer():
    """This function prints a simple greeting message."""
    print("Bonjour, monde !")

# Calling the function
saluer()  # Prints: Bonjour, monde !

Anatomy of a Function

  • Definition: Starts with def, followed by the function name and parentheses
  • Docstring: Documentation string (optional but recommended) enclosed in triple quotes
  • Body: Indented code that makes up the function
  • return statement: Returns a value (optional)

Parameters and Arguments

Parameters allow functions to accept input data, making them more flexible and reusable.

Positional Parameters

def saluer_personne(prenom, nom):
    """Greets a person by their full name."""
    print(f"Bonjour, {prenom} {nom} !")

# Call with positional arguments
saluer_personne("Marie", "Dupont")  # Prints: Bonjour, Marie Dupont !

Default Parameters

Default parameters define a value that will be used if no argument is provided.

def saluer_avec_formule(nom, formule="Bonjour"):
    """Greets with a customizable phrase."""
    print(f"{formule}, {nom} !")

# Function calls
saluer_avec_formule("Jean")  # Uses the default phrase
# Prints: Bonjour, Jean !

saluer_avec_formule("Pierre", "Salut")  # Overrides the default phrase
# Prints: Salut, Pierre !

Keyword Arguments

Keyword arguments let you explicitly specify which parameter receives which value.

def decrire_personne(nom, age, profession, ville):
    """Prints a description of a person."""
    print(f"{nom}, {age} ans, est {profession} à {ville}.")

# Call with keyword arguments (different order from the definition)
decrire_personne(
    ville="Paris", 
    profession="ingénieure", 
    nom="Sophie", 
    age=32
)
# Prints: Sophie, 32 ans, est ingénieure à Paris.

Variable Number of Arguments

Python lets you define functions that accept a variable number of arguments.

Variable Positional Arguments (*args)

def calculer_somme(*nombres):
    """Calculates the sum of all the numbers provided."""
    resultat = 0
    for nombre in nombres:
        resultat += nombre
    return resultat

# Calls with different numbers of arguments
print(calculer_somme(1, 2))  # 3
print(calculer_somme(1, 2, 3, 4, 5))  # 15
print(calculer_somme())  # 0

Variable Keyword Arguments (**kwargs)

def afficher_informations(**infos):
    """Prints all the information provided as key-value pairs."""
    for cle, valeur in infos.items():
        print(f"{cle}: {valeur}")

# Call with several keyword arguments
afficher_informations(
    nom="Alice", 
    age=28, 
    email="alice@example.com"
)
# Prints:
# nom: Alice
# age: 28
# email: alice@example.com

Combining Argument Types

def fonction_complete(arg1, arg2, *args, kwarg1="défaut", **kwargs):
    """Demonstrates all possible argument types."""
    print(f"arg1: {arg1}")
    print(f"arg2: {arg2}")
    print(f"args: {args}")
    print(f"kwarg1: {kwarg1}")
    print(f"kwargs: {kwargs}")

fonction_complete(1, 2, 3, 4, 5, kwarg1="personnalisé", x=10, y=20)
# Prints:
# arg1: 1
# arg2: 2
# args: (3, 4, 5)
# kwarg1: personnalisé
# kwargs: {'x': 10, 'y': 20}

Return Values

A function can return a value using the return statement. If there is no return statement, the function implicitly returns None.

Returning a Single Value

def carre(nombre):
    """Returns the square of a number."""
    return nombre ** 2

resultat = carre(4)
print(resultat)  # 16

Returning Multiple Values

def obtenir_dimensions():
    """Returns the width and height of a screen."""
    largeur = 1920
    hauteur = 1080
    return largeur, hauteur  # Returns a tuple

dimensions = obtenir_dimensions()
print(dimensions)  # (1920, 1080)

# Unpacking the returned tuple
largeur, hauteur = obtenir_dimensions()
print(f"Largeur: {largeur}, Hauteur: {hauteur}")  # Largeur: 1920, Hauteur: 1080

Returning Different Values Depending on Conditions

def analyser_nombre(n):
    """Analyzes a number and returns different information depending on its value."""
    if n < 0:
        return "négatif"
    elif n == 0:
        return "zéro"
    else:
        return "positif", "pair" if n % 2 == 0 else "impair"

# Calls with different values
print(analyser_nombre(-5))  # négatif
print(analyser_nombre(0))   # zéro
print(analyser_nombre(7))   # ('positif', 'impair')
print(analyser_nombre(8))   # ('positif', 'pair')

Early Exit with return

def diviser_securise(a, b):
    """Performs a safe division."""
    if b == 0:
        print("Division par zéro impossible")
        return None  # Early exit
    return a / b

print(diviser_securise(10, 2))  # 5.0
print(diviser_securise(10, 0))  # None (after printing the error message)

Variable Scope and Namespaces

A variable’s scope determines where that variable is accessible in your code.

Local and Global Variables

# Global variable
x = 10

def modifier_variable():
    # Local variable
    y = 5
    print(f"Dans la fonction, x = {x}")  # Access to the global variable x
    print(f"Dans la fonction, y = {y}")

modifier_variable()
print(f"Hors fonction, x = {x}")
# print(f"Hors fonction, y = {y}")  # Error: y is not defined

Modifying a Global Variable

To modify a global variable inside a function, you must use the global keyword.

compteur = 0

def incrementer():
    global compteur  # Declares that we are using the global variable
    compteur += 1
    print(f"Compteur = {compteur}")

incrementer()  # Compteur = 1
incrementer()  # Compteur = 2

Nonlocal Variables (Closures)

In nested functions, the nonlocal keyword gives you access to variables from the enclosing function.

def creer_compteur():
    """Creates and returns a counting function."""
    count = 0
    
    def incrementer():
        nonlocal count  # Access to the enclosing function's variable
        count += 1
        return count
    
    return incrementer

mon_compteur = creer_compteur()
print(mon_compteur())  # 1
print(mon_compteur())  # 2
print(mon_compteur())  # 3

Lambda Functions (Anonymous Functions)

Lambda functions are anonymous functions defined on a single line with a concise syntax.

Lambda Function Syntax

# Standard function
def carre(x):
    return x ** 2

# Equivalent with lambda
carre_lambda = lambda x: x ** 2

print(carre(5))        # 25
print(carre_lambda(5)) # 25

Typical Use with Higher-Order Functions

Lambdas are often used with functions such as map(), filter(), and sorted().

# Using map() to transform a list
nombres = [1, 2, 3, 4, 5]
carres = list(map(lambda x: x ** 2, nombres))
print(carres)  # [1, 4, 9, 16, 25]

# Using filter() to filter a list
nombres = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
pairs = list(filter(lambda x: x % 2 == 0, nombres))
print(pairs)  # [2, 4, 6, 8, 10]

# Using sorted() to sort a list
etudiants = [
    {"nom": "Alice", "note": 85},
    {"nom": "Bob", "note": 92},
    {"nom": "Charlie", "note": 78}
]
etudiants_tries = sorted(etudiants, key=lambda etudiant: etudiant["note"], reverse=True)
for etudiant in etudiants_tries:
    print(f"{etudiant['nom']}: {etudiant['note']}")
# Bob: 92
# Alice: 85
# Charlie: 78

Limitations of Lambda Functions

Lambda functions are limited to a single expression and cannot contain multiple statements or complex control structures. For more complex functions, it is better to use the def syntax.

Recursive Functions

A recursive function is a function that calls itself.

Basic Example: Factorial

def factorielle(n):
    """Calculates the factorial of n recursively."""
    if n <= 1:  # Base case
        return 1
    else:  # Recursive call
        return n * factorielle(n - 1)

print(factorielle(5))  # 120 (5 * 4 * 3 * 2 * 1)

Advanced Example: Traversing a Nested Structure

def parcourir_structure(structure, niveau=0):
    """Recursively traverses a nested data structure."""
    # Print the current structure, indented according to the level
    indentation = "  " * niveau
    print(f"{indentation}Niveau {niveau}: {structure}")
    
    # If the structure is a dictionary or a list, traverse it recursively
    if isinstance(structure, dict):
        for cle, valeur in structure.items():
            print(f"{indentation}  Clé: {cle}")
            parcourir_structure(valeur, niveau + 1)
    elif isinstance(structure, list):
        for i, item in enumerate(structure):
            print(f"{indentation}  Index: {i}")
            parcourir_structure(item, niveau + 1)

# Usage example
structure_exemple = {
    "personne": {
        "nom": "Dupont",
        "adresses": [
            {"type": "domicile", "ville": "Paris"},
            {"type": "travail", "ville": "Lyon"}
        ]
    }
}

parcourir_structure(structure_exemple)

Recursion Limits

Python limits recursion depth to prevent stack overflows. By default, this limit is generally 1000 calls.

import sys
print(sys.getrecursionlimit())  # Prints the current limit (usually 1000)

# To change the limit (use with caution)
# sys.setrecursionlimit(2000)

To avoid deep recursion problems, some recursive algorithms can be rewritten iteratively or by using techniques such as memoization.

Function Decorators

Decorators are an advanced concept that lets you modify or extend the behavior of functions.

Basic Concept

def mon_decorateur(fonction):
    def wrapper():
        print("Avant l'appel de la fonction")
        fonction()
        print("Après l'appel de la fonction")
    return wrapper

@mon_decorateur
def dire_bonjour():
    print("Bonjour !")

dire_bonjour()
# Prints:
# Avant l'appel de la fonction
# Bonjour !
# Après l'appel de la fonction

Decorator That Preserves Arguments

def decorateur_avec_arguments(fonction):
    def wrapper(*args, **kwargs):
        print(f"Arguments: {args}, Kwargs: {kwargs}")
        resultat = fonction(*args, **kwargs)
        print(f"Résultat: {resultat}")
        return resultat
    return wrapper

@decorateur_avec_arguments
def addition(a, b):
    return a + b

addition(3, 5)
# Prints:
# Arguments: (3, 5), Kwargs: {}
# Résultat: 8

Decorators with Parameters

def repeter(n):
    def decorateur(fonction):
        def wrapper(*args, **kwargs):
            for _ in range(n):
                resultat = fonction(*args, **kwargs)
            return resultat
        return wrapper
    return decorateur

@repeter(3)
def dire_bonjour(nom):
    print(f"Bonjour, {nom} !")
    return nom

dire_bonjour("Alice")
# Prints 3 times:
# Bonjour, Alice !

Documentation and Type Annotations

Docstrings

Docstrings (documentation strings) let you document your functions.

def calculer_moyenne(nombres):
    """
    Calculates the arithmetic mean of a list of numbers.
    
    Args:
        nombres (list): A list of numbers (int or float)
        
    Returns:
        float: The mean of the numbers
        
    Raises:
        ZeroDivisionError: If the list is empty
        
    Examples:
        >>> calculer_moyenne([1, 2, 3, 4, 5])
        3.0
    """
    return sum(nombres) / len(nombres)

Docstrings can be accessed with the help() function:

help(calculer_moyenne)

Type Annotations (Python 3.5+)

Type annotations improve readability and enable static code checking.

def saluer(nom: str, age: int = 30) -> str:
    """Returns a personalized greeting message."""
    return f"Bonjour {nom}, vous avez {age} ans !"

# Annotations are stored in the __annotations__ attribute
print(saluer.__annotations__)
# {'nom': <class 'str'>, 'age': <class 'int'>, 'return': <class 'str'>}

For more complex types, use the typing module:

from typing import List, Dict, Tuple, Optional, Union, Callable

def traiter_donnees(
    valeurs: List[int],
    config: Dict[str, str],
    limites: Tuple[int, int],
    callback: Optional[Callable[[int], bool]] = None
) -> Union[int, str]:
    """Processes data with complex types."""
    # Implementation...
    return 0

Best Practices for Functions

General Principles

  1. One function, one responsibility: Each function should have a single, clear purpose.

  2. Descriptive names: Choose names that explain what the function does, not how it does it.

    # Good
    def calculer_moyenne(nombres):
        return sum(nombres) / len(nombres)
    
    # Avoid
    def fonction1(liste):
        return sum(liste) / len(liste)
  3. Reasonable size: Functions should be relatively short (generally fewer than 20-30 lines).

  4. Immutable default parameters: Avoid using mutable objects as default values.

    # Problematic
    def ajouter_element(element, liste=[]):
        liste.append(element)
        return liste
    
    # Better approach
    def ajouter_element(element, liste=None):
        if liste is None:
            liste = []
        liste.append(element)
        return liste
  5. Clear documentation: Use docstrings to explain the purpose, parameters, and return values.

Naming conventions

Follow PEP 8 for naming conventions:

  • Use lowercase names separated by underscores (snake_case)
  • Prefer verbs for functions that perform actions
  • Be consistent in your conventions
# Good
def calculer_moyenne(nombres):
    pass

def obtenir_utilisateur_par_id(user_id):
    pass

def est_valide(donnee):
    pass

Complete example: Putting functions to work

Here is an example that combines several function concepts to build a simple task management system:

from typing import List, Dict, Optional, Tuple, Callable
import time

# Custom types
Tache = Dict[str, any]
ListeTaches = List[Tache]

def creer_tache(
    titre: str, 
    description: str = "", 
    priorite: int = 1
) -> Tache:
    """
    Creates a new task with an automatic timestamp.
    
    Args:
        titre: The task title
        description: A detailed description (optional)
        priorite: Priority level (1-5, 5 being the most important)
        
    Returns:
        A dictionary representing the task
    """
    return {
        "id": int(time.time()),
        "titre": titre,
        "description": description,
        "priorite": max(1, min(5, priorite)),  # Ensures the priority is between 1 and 5
        "termine": False,
        "date_creation": time.strftime("%Y-%m-%d %H:%M:%S")
    }

def ajouter_tache(
    liste_taches: ListeTaches, 
    tache: Tache
) -> ListeTaches:
    """Adds a task to the list and returns the updated list."""
    liste_taches.append(tache)
    return liste_taches

def marquer_comme_termine(
    liste_taches: ListeTaches, 
    tache_id: int
) -> Tuple[bool, str]:
    """
    Marks a task as completed.
    
    Returns:
        A tuple (success, message)
    """
    for tache in liste_taches:
        if tache["id"] == tache_id:
            tache["termine"] = True
            return True, f"Tâche '{tache['titre']}' marquée comme terminée"
    return False, "Tâche non trouvée"

def filtrer_taches(
    liste_taches: ListeTaches, 
    termine: Optional[bool] = None, 
    priorite_min: int = 0
) -> ListeTaches:
    """
    Filters tasks based on several criteria.
    
    Args:
        liste_taches: List of tasks to filter
        termine: If specified, filters tasks by their status
        priorite_min: Filters tasks with a priority greater than or equal to this value
        
    Returns:
        A new list containing the filtered tasks
    """
    resultat = []
    for tache in liste_taches:
        # Check the task status if the parameter is specified
        if termine is not None and tache["termine"] != termine:
            continue
        
        # Check the minimum priority
        if tache["priorite"] < priorite_min:
            continue
            
        resultat.append(tache)
    return resultat

def afficher_taches(
    liste_taches: ListeTaches,
    formatter: Callable[[Tache], str] = None
) -> None:
    """
    Displays a list of tasks.
    
    Args:
        liste_taches: List of tasks to display
        formatter: Optional function to format each task
    """
    if not liste_taches:
        print("Aucune tâche à afficher.")
        return
    
    # Default formatter
    if formatter is None:
        def formatter(tache):
            statut = "✓" if tache["termine"] else "☐"
            return f"{statut} [{tache['priorite']}] {tache['titre']}"
    
    # Display each task
    for i, tache in enumerate(liste_taches, 1):
        print(f"{i}. {formatter(tache)}")

# Using our functions
if __name__ == "__main__":
    mes_taches = []
    
    # Add a few tasks
    mes_taches = ajouter_tache(
        mes_taches, 
        creer_tache("Apprendre les fonctions Python", "Tutoriel complet", 4)
    )
    mes_taches = ajouter_tache(
        mes_taches, 
        creer_tache("Faire les courses")
    )
    mes_taches = ajouter_tache(
        mes_taches, 
        creer_tache("Répondre aux emails", priorite=5)
    )
    
    # Display the tasks
    print("Toutes les tâches:")
    afficher_taches(mes_taches)
    
    # Mark a task as completed
    succes, message = marquer_comme_termine(mes_taches, mes_taches[1]["id"])
    print(f"\nRésultat: {message}")
    
    # Filter and display incomplete tasks with priority >= 4
    taches_importantes = filtrer_taches(mes_taches, termine=False, priorite_min=4)
    print("\nTâches importantes non terminées:")
    afficher_taches(taches_importantes)
    
    # Use a custom formatter
    print("\nAffichage détaillé:")
    afficher_taches(mes_taches, lambda t: f"{t['titre']} - Priorité: {t['priorite']} - Créée le: {t['date_creation']}")

Conclusion

Functions are one of the pillars of Python programming. They let you structure your code and make it more modular, more readable, and easier to maintain. By mastering the techniques presented in this tutorial, you will be able to:

  • Break complex problems down into manageable subproblems
  • Avoid code duplication
  • Build libraries of reusable functions
  • Write more expressive and maintainable code

In the next tutorial, we will explore modules and packages in Python, which will let you organize your functions and classes into logical, reusable units.


Need to review data structures? Check out our tutorial on data structures in Python.

Comments

Comments are powered by GitHub Discussions

Sign in with GitHub to join the discussion

Link copied!