Programmation fonctionnelle

durant la classe de première j’ai pu évoquer de manière très rapide et superficielle les effets de bords. Pour être sûr de partir du bon pied on va reprendre de A à Z cette notion afin d’avoir de bonnes bases pour la programmation fonctionnelle.

L’Effet de Bord : Le Vilain Petit Canard de la Programmation ! 🦆

Imagine que tu donnes une mission à quelqu’un. Tu t’attends à ce qu’il fasse juste cette mission, n’est-ce pas ? En programmation, c’est un peu pareil ! Quand une fonction, disons notre amie fct, s’amuse à changer la valeur d’une variable qui n’est pas censée être de son ressort (comme notre i qui passe de 3 à 4 tout seul), on appelle ça un effet de bord. C’est un peu comme si ta fonction faisait de la « magie noire » en coulisses ! ✨

Exemple 1 : Modification d’une variable globale

Dans cet exemple, la fonction incremente_compteur() modifie la valeur d’une variable compteur qui est définie en dehors de la fonction (une variable globale).

compteur = 0 # Variable globale

def incremente_compteur():
  global compteur # On déclare qu'on veut modifier la variable globale
  compteur += 1
  print(f"Le compteur est maintenant : {compteur}")

print(f"Compteur avant appel : {compteur}")
incremente_compteur()
print(f"Compteur après 1er appel : {compteur}")
incremente_compteur()
print(f"Compteur après 2ème appel : {compteur}")

Pourquoi c’est un effet de bord ? La fonction incremente_compteur() ne se contente pas de faire un calcul ou de retourner une valeur ; elle modifie l’état d’une variable qui existe en dehors de son propre périmètre.

Exemple 2 : Modification d’une liste passée en argument

Dans cet exemple, la fonction ajouter_element() modifie une liste qui lui est passée en argument. En Python, les listes (et d’autres objets mutables comme les dictionnaires) sont passées par référence, ce qui signifie que la fonction travaille directement sur l’objet original.

ma_liste = [1, 2, 3] # Une liste

def ajouter_element(liste_a_modifier, element):
  liste_a_modifier.append(element)
  print(f"La liste à l'intérieur de la fonction : {liste_a_modifier}")

print(f"Liste avant appel : {ma_liste}")
ajouter_element(ma_liste, 4)
print(f"Liste après appel : {ma_liste}")

Pourquoi c’est un effet de bord ? La fonction ajouter_element() modifie directement ma_liste qui est une variable définie à l’extérieur. Si tu ne t’attends pas à ce comportement, cela peut créer des bugs. Souvent, pour éviter cet effet de bord avec les listes, on préférera retourner une nouvelle liste modifiée :

# Version sans effet de bord (ou effet de bord "maîtrisé" via le retour)
ma_liste_originale = [1, 2, 3]

def ajouter_element_sans_effet_de_bord(liste_initiale, element):
  nouvelle_liste = list(liste_initiale) # On crée une copie de la liste
  nouvelle_liste.append(element)
  return nouvelle_liste

print(f"Liste originale avant appel : {ma_liste_originale}")
liste_modifiee = ajouter_element_sans_effet_de_bord(ma_liste_originale, 4)
print(f"Liste originale après appel (inchangée) : {ma_liste_originale}")
print(f"Nouvelle liste modifiée : {liste_modifiee}")


Ces exemples montrent bien comment des fonctions peuvent avoir des « effets secondaires » inattendus sur l’état général de ton programme !

Pourquoi c’est « mal » les effets de bord ?

Même si dans les petits codes, ça peut sembler anodin, dans les programmes géants, les effets de bord peuvent vite transformer ton code en véritable casse-tête chinois (ou en plat de spaghettis si tu préfères ! 🍝).

  • Comportements imprévus : Ils sont les rois des surprises désagréables. Tu penses que ta variable a une certaine valeur, et paf, un effet de bord l’a changée sans prévenir. Bonjour les bugs !
  • Code illisible : Tenter de comprendre un programme truffé d’effets de bord, c’est comme essayer de démêler un nœud gordien les yeux bandés. C’est un vrai cauchemar pour la lecture et la maintenance.
  • Prédictibilité zéro : Quand l’état de tes variables peut changer à tout moment, impossible de savoir à quoi t’attendre. Ton code devient une boîte noire imprévisible.

En gros, les effets de bord, c’est la pagaille assurée ! C’est pourquoi, en général, on essaie de les éviter comme la peste. L’utilisation du mot-clé global ? À consommer avec une extrême modération !

Heureusement, il y a un super-héros pour ça : la programmation fonctionnelle ! Ce style de programmation a pour mission de minimiser au maximum ces vilains effets de bord, pour un code plus propre, plus lisible et plus prévisible.


Alors, prêt à dompter les effets de bord dans tes prochains codes ?

Plongeon dans la Programmation Fonctionnelle : Le Code Anti-Surprise ! 🦸‍♂️

Tu connais déjà la programmation impérative (on donne des ordres, étape par étape) et peut-être même la programmation orientée objet (on organise le code autour de « choses » et leurs actions). Eh bien, la programmation fonctionnelle, c’est un autre style de pensée, un peu comme une philosophie de code !

Son credo ? ZÉRO effet de bord ! 🚫 Pour faire simple, en programmation fonctionnelle, on évite comme la peste de modifier les variables existantes. Imagine que tes variables sont des rochers immuables : une fois posées, elles ne bougent plus !

Mais comment on fait alors ? On utilise des fonctions (d’où le nom !), mais attention, pas n’importe quelles fonctions ! On parle ici de fonctions pures.

La Fonction Pure : Le Ninja du Code Prévisible 🥋

Une fonction pure, c’est comme un mini-ordinateur super fiable :

  1. Son résultat ne dépend que de ce que tu lui donnes en entrée (ses paramètres).
  2. Elle ne touche à rien d’autre en dehors d’elle-même. Pas de modification de variables externes, pas de surprises !

C’est simple : même si tu l’appelles 100 fois avec les mêmes entrées, tu auras toujours le même résultat ! La prédictibilité, c’est son super-pouvoir !


À l’Action : Fonctions Pures vs. Fonctions Imprévues !

Test express 1 : Le Détective de Variables Étrangères

Regarde ce code :

Python

niveau_de_difficulte = 5 # Une variable globale qui traîne par là

def est_difficile():
  if niveau_de_difficulte > 5: # Oups, on regarde une variable externe !
    return "C'est un défi !"
  else :
    return "Facile comme bonjour."

print(est_difficile())

Alors, la fonction est_difficile() est-elle pure ? 🤔

La réponse : Non ! La pauvre est_difficile() n’est pas pure car elle jette un œil à niveau_de_difficulte, une variable qui vit en dehors d’elle. Si niveau_de_difficulte change, le comportement de la fonction change sans que tu ne modifies ses paramètres. Mystère et boule de gomme !


Test express 2 : La Pureté Incarnée

Maintenant, observe celle-ci :

Python

def est_difficile_pure(niveau): # Le niveau est passé en paramètre, nickel !
  if niveau > 5:
    return "C'est un défi !"
  else :
    return "Facile comme bonjour."

print(est_difficile_pure(7))
print(est_difficile_pure(3))

Et celle-ci, pure ou pas pure ? 🤔

La réponse : Oui, c’est une fonction pure ! Son résultat (facile ou difficile) dépend uniquement du niveau que tu lui donnes en entrée. Elle ne va pas fouiner ailleurs, elle est super indépendante !


La Règle d’Or : Créer plutôt que Modifier !

La programmation fonctionnelle, c’est comme être un collectionneur méticuleux. Plutôt que de « retoucher » un objet existant, tu en crées un tout nouveau à partir de l’ancien.

Test express 3 : L’Artiste du Changement (qui modifie en douce)

Python

liste_de_courses = ["pommes", "lait"]

def ajouter_article(article):
  liste_de_courses.append(article) # Attention, on modifie la liste originale !
  print(f"Ma liste dans la fonction : {liste_de_courses}")

print(f"Ma liste AVANT : {liste_de_courses}")
ajouter_article("pain")
print(f"Ma liste APRÈS : {liste_de_courses}")

Est-ce que ça respecte le mode fonctionnel ? 🧐

La réponse : Non, non et re-non ! La fonction ajouter_article est une véritable rebelle : elle modifie directement liste_de_courses. C’est un effet de bord en plein ! La méthode append() en Python, par exemple, est un classique des opérations qui modifient une donnée existante.


Test express 4 : L’Artiste du Nouveau (le chic fonctionnel)

Et cette version, alors ?

Python

liste_de_courses_originale = ["pommes", "lait"]

def ajouter_article_pure(liste_actuelle, article):
  # On ne modifie RIEN ! On crée une NOUVELLE liste avec l'ancien contenu + le nouvel article
  nouvelle_liste = liste_actuelle + [article]
  return nouvelle_liste

print(f"Ma liste originale AVANT : {liste_de_courses_originale}")
ma_nouvelle_liste = ajouter_article_pure(liste_de_courses_originale, "pain")
print(f"Ma liste originale APRÈS (elle est restée intacte !) : {liste_de_courses_originale}")
print(f"Ma NOUVELLE liste : {ma_nouvelle_liste}")

Alors, ça, c’est du fonctionnel pur et dur ! 🤩

La réponse : Oui, bingo ! La fonction ajouter_article_pure est un modèle de pureté fonctionnelle. Elle ne touche pas à la liste_de_courses_originale. Au lieu de ça, elle prend la liste existante, y ajoute l’article, et crée une toute nouvelle liste qu’elle te renvoie. L’originale est saine et sauve ! Plus de risque d’effet de bord caché !


Choisir Son Super-Pouvoir de Programmation 💪

Impératif, Orienté Objet, Fonctionnel… Il n’y a pas de « meilleur » paradigme. Chaque approche a ses forces et ses faiblesses. Le vrai talent du programmeur, c’est de savoir choisir le bon outil pour le bon travail. Parfois, la puissance de l’objet est parfaite ; d’autres fois, la clarté et la prévisibilité du fonctionnel sont indispensables.

Alors, quel style de code vas-tu maîtriser ensuite ? 😉

Applications

Exercice 1 : Le Détective de Super-Héros ! 🦸‍♀️

Imagine que tu as une liste de super-héros, et tu veux trouver le premier qui correspond à un certain critère (par exemple, « Est-ce qu’il peut voler ? »).

Exercice : Écris une fonction trouve_super_heros(critere, equipe_de_heros) qui prend en entrée :

  • critere : une fonction qui teste si un super-héros a le super-pouvoir recherché (elle renvoie True ou False).
  • equipe_de_heros : une liste de super-héros (chaque super-héros est une chaîne de caractères).

La fonction devra retourner le nom du premier super-héros qui satisfait le critere. Si aucun ne convient, elle retourne None, car il n’y a pas de héros à la hauteur !

Connaissances nécessaires : Pas de nouvelle connaissance majeure, c’est une application directe de la compréhension des fonctions comme arguments et des boucles.


Python

# Solution Exercice 1 : Le Détective de Super-Héros

def trouve_super_heros(critere, equipe_de_heros):
  """
  Recherche le premier super-héros dans l'équipe qui satisfait un critère donné.

  Args:
    critere: Une fonction qui prend un super-héros (chaîne) et retourne True/False.
    equipe_de_heros: Une liste de noms de super-héros (chaînes).

  Returns:
    Le nom du premier super-héros qui correspond au critère, ou None si aucun.
  """
  for heros in equipe_de_heros:
    if critere(heros):
      return heros # On a trouvé notre champion !
  return None # Personne à l'horizon...

# --- Testons notre détective ! ---

# Nos super-pouvoirs de détection (fonctions de critère)
def peut_voler(heros):
  return "vole" in heros.lower() or "volant" in heros.lower()

def est_costume_rouge(heros):
  return "rouge" in heros.lower()

def a_cape(heros):
  return "cape" in heros.lower()

equipe_alpha = ["SuperMan", "Batman", "WonderWoman", "Flash"]
equipe_beta = ["Spider-Man", "Iron Man", "Captain America", "Hulk"]
equipe_gamma = ["Aquaman", "Cyborg"]

print(f"Équipe Alpha : {equipe_alpha}")
print(f"Premier héros qui vole : {trouve_super_heros(peut_voler, equipe_alpha)}")
print(f"Premier héros en costume rouge : {trouve_super_heros(est_costume_rouge, equipe_alpha)}")
print(f"Premier héros avec cape : {trouve_super_heros(a_cape, equipe_alpha)}")
print("-" * 30)

print(f"Équipe Beta : {equipe_beta}")
print(f"Premier héros qui vole : {trouve_super_heros(peut_voler, equipe_beta)}")
print(f"Premier héros en costume rouge : {trouve_super_heros(est_costume_rouge, equipe_beta)}")
print(f"Premier héros avec cape : {trouve_super_heros(a_cape, equipe_beta)}")
print("-" * 30)

print(f"Équipe Gamma : {equipe_gamma}")
print(f"Premier héros avec cape : {trouve_super_heros(a_cape, equipe_gamma)}") # Aucun avec cape


Exercice 2 : La Boîte à Magie !

Imagine que tu as une boîte pleine d’ingrédients (ta liste), et tu as une baguette magique (ta fonction f) qui peut transformer chaque ingrédient individuellement. Tu veux que ta fonction te donne une nouvelle boîte avec tous les ingrédients transformés, sans toucher à la boîte d’origine !

Exercice : Écris une fonction transforme_ingredients(recette_magique, liste_ingredients) qui prend :

  • recette_magique : une fonction qui va transformer un ingrédient.
  • liste_ingredients : une liste d’ingrédients.

La fonction devra retourner une nouvelle liste (une nouvelle boîte !) où chaque ingrédient de départ a été transformé par la recette_magique.

Fais-le de deux façons :

  1. Avec une bonne vieille boucle for (la méthode « traditionnelle »).
  2. Avec la notation par compréhension de liste (la méthode « magique » et compacte de Python) !

Connaissances nécessaires : La compréhension de liste est une nouvelle notion à aborder ici.

Explication de la compréhension de liste : C’est une syntaxe super cool et compacte en Python pour créer des listes. Au lieu d’écrire une boucle for et d’utiliser append(), tu peux tout faire en une seule ligne, entre crochets [].

Format : [expression for item in iterable if condition]

  • expression : Ce que tu veux ajouter à ta nouvelle liste pour chaque élément.
  • item : La variable qui prendra la valeur de chaque élément de l’itérable.
  • iterable : La liste, la chaîne, etc., sur laquelle tu boucles.
  • if condition (optionnel) : Une condition pour filtrer les éléments.

Exemple rapide :

Python

nombres = [1, 2, 3, 4]
carres = [n*n for n in nombres] # [1, 4, 9, 16]
pairs = [n for n in nombres if n % 2 == 0] # [2, 4]


Python

# Solution Exercice 2 : La Boîte à Magie

def transforme_ingredients_boucle(recette_magique, liste_ingredients):
  """
  Transforme chaque ingrédient d'une liste en utilisant une boucle for.
  Retourne une NOUVELLE liste.
  """
  nouvelle_liste = []
  for ingredient in liste_ingredients:
    nouvelle_liste.append(recette_magique(ingredient))
  return nouvelle_liste

def transforme_ingredients_comprehension(recette_magique, liste_ingredients):
  """
  Transforme chaque ingrédient d'une liste en utilisant une compréhension de liste.
  Retourne une NOUVELLE liste.
  """
  # C'est la syntaxe magique !
  return [recette_magique(ingredient) for ingredient in liste_ingredients]

# --- Testons nos boîtes à magie ! ---

# Quelques recettes magiques (fonctions)
def double_valeur(x):
  return x * 2

def rend_majuscule(chaine):
  return chaine.upper()

def ajoute_exclamation(texte):
  return texte + " !"

mes_chiffres = [1, 5, 10, 100]
mes_mots = ["pomme", "banane", "cerise"]

print(f"Chiffres originaux : {mes_chiffres}")
print(f"Chiffres doublés (boucle) : {transforme_ingredients_boucle(double_valeur, mes_chiffres)}")
print(f"Chiffres doublés (compréhension) : {transforme_ingredients_comprehension(double_valeur, mes_chiffres)}")
print("-" * 30)

print(f"Mots originaux : {mes_mots}")
print(f"Mots en majuscules (boucle) : {transforme_ingredients_boucle(rend_majuscule, mes_mots)}")
print(f"Mots en majuscules (compréhension) : {transforme_ingredients_comprehension(rend_majuscule, mes_mots)}")
print("-" * 30)

print(f"Mots avec exclamation (boucle) : {transforme_ingredients_boucle(ajoute_exclamation, mes_mots)}")
print(f"Mots avec exclamation (compréhension) : {transforme_ingredients_comprehension(ajoute_exclamation, mes_mots)}")


Exercice 3 Le Chef d’Orchestre des Opérations ! 🎶

Ton exercice original :

Imagine que tu as une liste de « notes de musique » (x1, x2, ...). Tu veux d’abord les « arranger » avec une fonction f (par exemple, les mettre en « majeur » ou « mineur »), puis tu veux les « enchaîner » toutes ensemble avec une opération op (comme les « lier » les unes aux autres).

Exercice : Écris une fonction chef_dorchestre(f_transforme, op_enchaine, partition) qui prend :

  • f_transforme : une fonction pour transformer chaque élément de la liste.
  • op_enchaine : une fonction pour combiner (enchaîner) deux éléments transformés.
  • partition : la liste des éléments de départ.

La fonction doit d’abord appliquer f_transforme à chaque élément de la partition, puis combiner tous les résultats avec op_enchaine, de gauche à droite.

Ensuite, utilise cette fonction chef_dorchestre pour :

  • Prendre une liste de nombres (par exemple [10, 20, 30]).
  • Transformer chaque nombre en sa représentation textuelle (par exemple, 10 devient "10").
  • Combiner ces textes en une seule chaîne, où les nombres sont séparés par une virgule et un espace (par exemple "10, 20, 30").

Connaissances nécessaires : Aucune nouvelle connaissance spécifique, mais cela combine la manipulation de fonctions en arguments et le concept de « réduction » (appliquer une opération de manière cumulative sur une liste), ce qui est un classique de la programmation fonctionnelle (souvent appelé reduce ou fold).


Python

# Solution Exercice 3 : Le Chef d'Orchestre des Opérations

def chef_dorchestre(f_transforme, op_enchaine, partition):
  """
  Applique une transformation f_transforme à chaque élément de la partition,
  puis combine les résultats avec l'opération op_enchaine.
  La partition est supposée non vide.
  """
  if not partition:
    raise ValueError("La partition ne doit pas être vide pour le chef d'orchestre !")

  # Étape 1 : Transformer chaque élément (utiliser ce qu'on a appris à l'Exercice 2 !)
  elements_transformes = [f_transforme(x) for x in partition]

  # Étape 2 : Enchaîner les éléments transformés
  resultat_final = elements_transformes[0]
  for i in range(1, len(elements_transformes)):
    resultat_final = op_enchaine(resultat_final, elements_transformes[i])

  return resultat_final

# --- Appliquons notre chef d'orchestre pour créer des chaînes stylées ! ---

# La "recette" pour transformer un nombre en texte (f_transforme)
def en_chaine(nombre):
  return str(nombre)

# L'"opération" pour enchaîner les textes avec une virgule (op_enchaine)
def concat_avec_virgule(texte1, texte2):
  return texte1 + ", " + texte2

mes_chiffres_pour_affichage = [1, 2, 3, 4]
autres_nombres = [100, 200, 300, 400, 500]
nombres_aleatoires = [42, 7, 99, 13, 27]


print(f"Liste originale : {mes_chiffres_pour_affichage}")
chaine_finale = chef_dorchestre(en_chaine, concat_avec_virgule, mes_chiffres_pour_affichage)
print(f"Représentation en chaîne : \"{chaine_finale}\"")
print("-" * 30)

print(f"Liste originale : {autres_nombres}")
chaine_finale_2 = chef_dorchestre(en_chaine, concat_avec_virgule, autres_nombres)
print(f"Représentation en chaîne : \"{chaine_finale_2}\"")
print("-" * 30)

print(f"Liste originale : {nombres_aleatoires}")
chaine_finale_3 = chef_dorchestre(en_chaine, concat_avec_virgule, nombres_aleatoires)
print(f"Représentation en chaîne : \"{chaine_finale_3}\"")


Exercice 4 : Le Multiplicateur de Pouvoirs ! ⚡

Imagine que tu as un super-pouvoir (une fonction f). Tu veux une machine qui, quand tu lui donnes ce super-pouvoir, te crée un nouveau super-pouvoir qui fait exactement la même chose… mais deux fois de suite !

Exercice : Écris une fonction amplificateur_de_pouvoirs(super_pouvoir) qui prend une fonction super_pouvoir en argument. Cette fonction amplificateur_de_pouvoirs doit te retourner une nouvelle fonction. Cette nouvelle fonction, quand tu l’appelleras avec une valeur, appliquera super_pouvoir deux fois de suite à cette valeur.

Ensuite, essaie de deviner (puis vérifie avec le code) le résultat de : amplificateur_de_pouvoirs(amplificateur_de_pouvoirs(lambda x: x + 3))(5) (C’est un peu un défi pour les méninges, mais on y va étape par étape !)

Connaissances nécessaires :

  • Fonctions comme valeurs de retour : Une fonction peut non seulement prendre des fonctions en argument, mais aussi en retourner ! C’est une notion clé en programmation fonctionnelle.
  • lambda fonctions (fonctions anonymes) : Elles permettent de créer des petites fonctions très rapidement, sans leur donner de nom. Utile pour des fonctions simples qu’on n’utilisera qu’une fois.
    • Format : lambda arguments: expression
    • Exemple : lambda x: x * 2 est une fonction qui prend x et retourne x * 2.

Python

# Solution Exercice 4 : Le Multiplicateur de Pouvoirs

def amplificateur_de_pouvoirs(super_pouvoir):
  """
  Prend une fonction (super_pouvoir) et retourne une NOUVELLE fonction
  qui applique le super_pouvoir deux fois de suite.
  """
  def nouveau_super_pouvoir(argument):
    # Applique le super_pouvoir une première fois
    resultat_une_fois = super_pouvoir(argument)
    # Applique le super_pouvoir une deuxième fois sur le résultat précédent
    return super_pouvoir(resultat_une_fois)
  
  return nouveau_super_pouvoir # On retourne la NOUVELLE fonction !

# --- Testons le défi de l'amplificateur ! ---

# Notre super_pouvoir de base : ajouter 3
ajoute_trois = lambda x: x + 3
print(f"Fonction de base (ajoute 3) : lambda x: x + 3")

# Étape 1 : un premier amplificateur
# amplificateur_de_pouvoirs(ajoute_trois) retourne une fonction qui fait (x + 3) + 3 = x + 6
ajoute_six = amplificateur_de_pouvoirs(ajoute_trois)
print(f"Après 1er amplificateur (ajoute 6) : {ajoute_six(5)}") # Devrait donner 5 + 6 = 11

# Étape 2 : un deuxième amplificateur sur le premier amplificateur !
# amplificateur_de_pouvoirs(ajoute_six) retourne une fonction qui fait (x + 6) + 6 = x + 12
ajoute_douze = amplificateur_de_pouvoirs(ajoute_six)
print(f"Après 2ème amplificateur (ajoute 12) : {ajoute_douze(5)}") # Devrait donner 5 + 12 = 17

# Le défi complet : amplificateur_de_pouvoirs(amplificateur_de_pouvoirs(lambda x: x + 3))(5)
# Cela se traduit par : ajoute_douze(5)
resultat_defi = amplificateur_de_pouvoirs(amplificateur_de_pouvoirs(ajoute_trois))(5)
print(f"Résultat du défi : amplificateur_de_pouvoirs(amplificateur_de_pouvoirs(lambda x: x + 3))(5) = {resultat_defi}")

# Vérification :
# lambda x: x + 3 appliquée une fois donne x + 3
# amplificateur_de_pouvoirs(lambda x: x + 3) -> fonction qui fait (x + 3) + 3 = x + 6
# amplificateur_de_pouvoirs(cela) -> fonction qui fait (x + 6) + 6 = x + 12
# Donc, (lambda x: x + 12)(5) = 5 + 12 = 17. Le résultat est bien 17.


Exercice 5 : Le Mixeur de Potions Magiques ! 🧪


Imagine que tu as deux potions magiques : une potion A (g) et une potion B (f). Tu veux créer une troisième potion (h) qui combine les effets des deux : tu verses d’abord l’ingrédient dans la potion A, et le résultat de la potion A est ensuite versé dans la potion B.

Exercice : Écris une fonction mixeur_de_potions(potion_b, potion_a) qui prend deux fonctions (potion_b et potion_a) en arguments. Elle doit retourner une nouvelle fonction (potion_combine) qui représente leur composition. Autrement dit, si tu donnes un ingrédient x à potion_combine, elle appliquera d’abord potion_a à x, puis appliquera potion_b au résultat de potion_a(x).

Connaissances nécessaires : Aucune nouvelle connaissance, c’est une application directe de la notion de fonction retournant une fonction, comme dans l’exercice précédent.


Python

# Solution Exercice 5 : Le Mixeur de Potions Magiques

def mixeur_de_potions(potion_b, potion_a):
  """
  Compose deux fonctions (potion_b après potion_a) et retourne la fonction combinée.
  """
  def potion_combine(ingredient_de_base):
    # D'abord, l'ingrédient passe par la potion_a
    resultat_de_a = potion_a(ingredient_de_base)
    # Ensuite, le résultat de la potion_a passe par la potion_b
    resultat_final = potion_b(resultat_de_a)
    return resultat_final
  
  return potion_combine # On retourne la nouvelle potion combinée !

# --- Testons notre mixeur ! ---

# Nos potions de base
def potion_grossissante(nombre): # Potion A (g)
  return nombre * 10

def potion_effacante(nombre): # Potion B (f)
  return str(nombre) + " effacé !"

def potion_magique_carre(x): # Une autre potion A
  return x * x

def potion_magique_plus_un(y): # Une autre potion B
  return y + 1

# Créons une potion combinée : d'abord multiplier par 10, puis ajouter " effacé !"
ma_potion_finale = mixeur_de_potions(potion_effacante, potion_grossissante)
print(f"Effet de ma_potion_finale(5) : {ma_potion_finale(5)}") # Devrait donner "50 effacé !"
print(f"Effet de ma_potion_finale(12) : {ma_potion_finale(12)}") # Devrait donner "120 effacé !"
print("-" * 30)

# Créons une autre potion combinée : d'abord au carré, puis +1
potion_carre_plus_un = mixeur_de_potions(potion_magique_plus_un, potion_magique_carre)
print(f"Effet de potion_carre_plus_un(3) : {potion_carre_plus_un(3)}") # Devrait donner (3*3)+1 = 10
print(f"Effet de potion_carre_plus_un(5) : {potion_carre_plus_un(5)}") # Devrait donner (5*5)+1 = 26


Exercice 6 L’Alchimiste des Ensembles ! 🧪✨

Dans le monde de la programmation fonctionnelle, même quand on manipule des collections, on essaie de ne pas les « abîmer » ou les modifier directement. Python nous aide avec ses ensembles (sets) qui ont des opérations super pratiques et non destructives pour la plupart.

Connaissances nécessaires :

  • Les ensembles (sets) en Python : Ce sont des collections non ordonnées d’éléments uniques. Ils sont très utiles pour les opérations mathématiques d’ensembles.
  • Création d’un ensemble : mon_set = {1, 2, 3} ou mon_set = set([1, 2, 3])
  • Opérations d’ensembles (non destructives) :
    • Union (|) : Combine les éléments des deux ensembles. A | B
    • Intersection (&) : Trouve les éléments communs aux deux ensembles. A & B
    • Différence (-) : Trouve les éléments du premier ensemble qui ne sont pas dans le second. A - B

Exercice : Imagine une « identité » mystérieuse entre trois ensembles A, B, C (des groupes d’éléments, par exemple) : A ∩ (B ∪ C) = (A ∩ B) ∪ (A ∩ C)

C’est une loi de la logique et des ensembles appelée la distributivité de l’intersection sur l’union.

Écris un code Python qui :

  1. Définit trois ensembles d’exemple (par exemple, des nombres ou des lettres).
  2. Calcule le côté gauche de l’identité : A ∩ (B ∪ C).
  3. Calcule le côté droit de l’identité : (A ∩ B) ∪ (A ∩ C).
  4. Vérifie si les deux résultats sont égaux.

L’objectif est de n’utiliser que les opérations d’ensembles |, -, et & qui renvoient de nouveaux ensembles sans modifier les originaux !


Python

# Solution Exercice 6 : L'Alchimiste des Ensembles

# Nos trois ensembles magiques (ils restent intacts, promis !)
set_A = {1, 2, 3, 4, 5, 6}
set_B = {4, 5, 6, 7, 8, 9}
set_C = {2, 3, 7, 8, 10, 11}

print(f"Ensemble A : {set_A}")
print(f"Ensemble B : {set_B}")
print(f"Ensemble C : {set_C}")
print("-" * 30)

# Calcul du côté gauche : A ∩ (B ∪ C)
# Étape 1 : B ∪ C (Union de B et C)
b_union_c = set_B | set_C
print(f"B ∪ C : {b_union_c}")

# Étape 2 : A ∩ (résultat de l'étape 1) (Intersection de A et (B ∪ C))
cote_gauche = set_A & b_union_c
print(f"A ∩ (B ∪ C) : {cote_gauche}")
print("-" * 30)

# Calcul du côté droit : (A ∩ B) ∪ (A ∩ C)
# Étape 1 : A ∩ B (Intersection de A et B)
a_inter_b = set_A & set_B
print(f"A ∩ B : {a_inter_b}")

# Étape 2 : A ∩ C (Intersection de A et C)
a_inter_c = set_A & set_C
print(f"A ∩ C : {a_inter_c}")

# Étape 3 : (résultat étape 1) ∪ (résultat étape 2) (Union de (A ∩ B) et (A ∩ C))
cote_droit = a_inter_b | a_inter_c
print(f"(A ∩ B) ∪ (A ∩ C) : {cote_droit}")
print("-" * 30)

# Vérification finale : Sont-ils identiques ?
est_egal = (cote_gauche == cote_droit)
print(f"Les deux côtés sont-ils égaux ? {est_egal}")

if est_egal:
  print("L'identité de distributivité de l'intersection sur l'union est bien vérifiée !")
else:
  print("Oups, quelque chose ne va pas... L'identité n'est pas vérifiée.")


Exercice 7 Le Compte à Rebours Zen ! 🧘

un :

Imagine que tu es un maître zen et que tu dois répéter une méditation (f) un certain nombre de fois (n), mais chaque méditation doit être suivie d’un moment de silence paisible (t secondes de pause).

Exercice : Écris une fonction compte_a_rebours_zen(meditation_fonction, nombre_repetitions, duree_pause_en_secondes) qui prend :

  • meditation_fonction : une fonction à appeler. Cette fonction recevra le numéro de l’appel (0, 1, 2, …) comme argument.
  • nombre_repetitions : le nombre de fois que la meditation_fonction doit être appelée.
  • duree_pause_en_secondes : la durée de la pause entre chaque appel.

La fonction devra appeler meditation_fonction de 0 à nombre_repetitions - 1, en faisant une pause après chaque appel.

Connaissances nécessaires :

  • Le module time et sa fonction time.sleep().

Explication de time.sleep() : Cette fonction met en pause l’exécution de ton programme pendant le nombre de secondes spécifié. C’est très utile pour simuler des délais, ou dans notre cas, pour représenter des moments de silence.

Python

import time # N'oublie pas d'importer le module time !

# Exemple :
print("Début...")
time.sleep(2) # Attend 2 secondes
print("...Fin !")


Python

# Solution Exercice 7 : Le Compte à Rebours Zen

import time # Indispensable pour les pauses !

def compte_a_rebours_zen(meditation_fonction, nombre_repetitions, duree_pause_en_secondes):
  """
  Appelle une fonction 'nombre_repetitions' fois, avec une pause entre chaque appel.

  Args:
    meditation_fonction: La fonction à appeler. Elle recevra le numéro d'itération.
    nombre_repetitions: Le nombre total d'appels.
    duree_pause_en_secondes: La durée de la pause en secondes après chaque appel.
  """
  print(f"Début du compte à rebours zen pour {nombre_repetitions} répétitions de {meditation_fonction.__name__}...")
  for i in range(nombre_repetitions):
    meditation_fonction(i) # Appel de la fonction avec le numéro de l'itération
    if i < nombre_repetitions - 1: # On ne fait pas de pause après le dernier appel
      time.sleep(duree_pause_en_secondes)
  print("Fin du compte à rebours zen. Namaste !")

# --- Testons notre maître zen ! ---

# Une fonction de méditation simple
def petite_meditation(numero):
  print(f"Méditation n°{numero + 1} : 'Inspirer la sérénité...'")

def verification_chrono(etape):
  print(f"Vérification de l'étape {etape} : Top !")

print("--- Test 1 : Méditation courte ---")
compte_a_rebours_zen(petite_meditation, 3, 1) # 3 méditations, 1 seconde de pause

print("\n--- Test 2 : Vérification rapide ---")
compte_a_rebours_zen(verification_chrono, 5, 0.5) # 5 vérifications, 0.5 seconde de pause


Exercice 8 Le Chronométreur Magique ! ⏱️✨

Parfois, on veut savoir non seulement ce que fait notre sortilège (fonction), mais aussi combien de temps il lui faut pour opérer sa magie ! C’est là que le chronométreur magique entre en scène.

Exercice : Écris une fonction chronometreur_magique(sortilege, cible_magique) qui prend :

  • sortilege : une fonction à tester.
  • cible_magique : l’argument sur lequel lancer le sortilege.

Cette fonction doit retourner un duo magique :

  1. Le résultat du sortilege appliqué à la cible_magique.
  2. Le temps (en secondes) qu’il a fallu pour que le sortilege s’exécute.

Utilise la fonction time.perf_counter() pour une mesure de temps précise (elle est super pour mesurer des durées courtes et répétées !).

Ensuite, mesure le temps pris par une boucle_infini_lente qui simule une tâche longue et ennuyeuse :

Python

def boucle_infini_lente(iterations):
    # Imagine qu'on compte des grains de sable un par un
    compteur_sable = 0
    for i in range(iterations):
        compteur_sable += 1 # Un grain de sable de plus
    return compteur_sable

Mesure le temps pris par boucle_infini_lente avec un argument de 10 000 000 (dix millions). Prépare-toi, ça peut prendre un petit moment !

Connaissances nécessaires :

  • Le module time et la fonction time.perf_counter().

Explication de time.perf_counter() : time.perf_counter() retourne la valeur d’un compteur de performance, qui est très précis pour mesurer des durées (il n’est pas affecté par les ajustements de l’horloge système). Pour mesurer une durée :

  1. Appelle time.perf_counter() avant l’opération.
  2. Appelle time.perf_counter() après l’opération.
  3. La différence entre les deux est le temps écoulé.

import time

temps_debut = time.perf_counter()
# Fais quelque chose qui prend du temps ici
for _ in range(1000000):
    pass
temps_fin = time.perf_counter()
temps_ecoule = temps_fin - temps_debut
print(f"Temps écoulé : {temps_ecoule} secondes")


Python

# Solution Exercice 8 : Le Chronométreur Magique

import time

def chronometreur_magique(sortilege, cible_magique):
  """
  Mesure le temps d'exécution d'un sortilège (fonction) sur une cible.

  Args:
    sortilege: La fonction à chronométrer.
    cible_magique: L'argument à passer à la fonction.

  Returns:
    Une paire (tuple) : (résultat_du_sortilege, temps_pris_en_secondes).
  """
  temps_avant = time.perf_counter() # On démarre le chrono
  resultat = sortilege(cible_magique) # On lance le sortilège
  temps_apres = time.perf_counter() # On arrête le chrono
  
  temps_total = temps_apres - temps_avant
  return (resultat, temps_total)

# La fonction de notre "boucle_infini_lente"
def boucle_infini_lente(iterations):
    compteur_sable = 0
    for i in range(iterations):
        compteur_sable += 1
    return compteur_sable

# --- Testons le chronométreur sur notre boucle lente ! ---

grand_nombre_iterations = 10_000_000 # Dix millions (les underscores rendent le nombre plus lisible)

print(f"Mesure du temps pour 'boucle_infini_lente' avec {grand_nombre_iterations} itérations...")
resultat_boucle, temps_boucle = chronometreur_magique(boucle_infini_lente, grand_nombre_iterations)

print(f"Le résultat de la boucle est : {resultat_boucle}")
print(f"Le temps pris par la boucle est : {temps_boucle:.4f} secondes") # .4f pour 4 décimales

print("\n--- Test rapide avec une fonction simple ---")
def addition_simple(a):
    return a + 100

resultat_add, temps_add = chronometreur_magique(addition_simple, 50)
print(f"Résultat de l'addition simple : {resultat_add}")
print(f"Temps pris pour l'addition simple : {temps_add:.6f} secondes") # Plus de décimales pour les courtes durées


Exercice 9 L’Adaptateur de Sortilèges ! 🧙‍♂️⚙️

Notre chronometreur_magique est génial, mais il a un petit défaut : il ne peut mesurer que les sortilèges qui prennent un seul argument ! Mais que faire si notre sortilège secret a besoin de plusieurs ingrédients (plusieurs arguments) ? Pas de panique, la programmation fonctionnelle a une astuce : les fonctions anonymes (lambda) peuvent venir à la rescousse pour « envelopper » notre sortilège complexe !

Exercice : Utilise ton chronometreur_magique de l’exercice précédent pour mesurer le temps d’exécution de la fonction boucle_geante_nichee suivante :

Python

def boucle_geante_nichee(longueur_x, longueur_y):
    # Imagine que tu comptes des étoiles dans deux constellations différentes
    etoiles_comptees = 0
    for x in range(longueur_x):
        for y in range(longueur_y):
            etoiles_comptees += 1
    return etoiles_comptees

Mesure le temps pris par un appel de boucle_geante_nichee(1000, 10000) sans modifier ni chronometreur_magique ni boucle_geante_nichee. Indique clairement comment tu utilises la lambda fonction pour faire le lien.

Connaissances nécessaires :

  • lambda fonctions pour capturer des arguments : Les fonctions lambda sont parfaites pour créer des petites fonctions « à la volée » qui peuvent fixer certains arguments d’une autre fonction.

Python

# Solution Exercice 9 : L'Adaptateur de Sortilèges

import time

# On réutilise notre chronometreur_magique de l'exercice précédent
def chronometreur_magique(sortilege, cible_magique):
  temps_avant = time.perf_counter()
  resultat = sortilege(cible_magique) # La fonction est appelée avec un SEUL argument
  temps_apres = time.perf_counter()
  temps_total = temps_apres - temps_avant
  return (resultat, temps_total)

# La fonction de notre "boucle_geante_nichee" qui a DEUX arguments
def boucle_geante_nichee(longueur_x, longueur_y):
    etoiles_comptees = 0
    for x in range(longueur_x):
        for y in range(longueur_y):
            etoiles_comptees += 1
    return etoiles_comptees

# --- Comment utiliser chronometreur_magique avec boucle_geante_nichee ? ---

# L'astuce est de créer une fonction lambda qui "enveloppe" boucle_geante_nichee
# et lui fournit les deux arguments nécessaires, tout en ne prenant qu'UN SEUL argument
# (qui sera ignoré par cette lambda, ou pourrait être un argument "factice" si nécessaire).

# Ici, la lambda est très simple : elle ne prend aucun argument réel (x)
# mais appelle boucle_geante_nichee avec les arguments fixes que nous voulons tester.
# Le 'cible_magique' de chronometreur_magique sera juste un placeholder ici.
# On aurait pu aussi faire : lambda _ : boucle_geante_nichee(1000, 10000)
# Le underscore _ est une convention pour un argument qu'on n'utilise pas.

# Méthode 1 : lambda qui ne prend pas vraiment d'argument "utile"
fonction_pour_chrono_1 = lambda x: boucle_geante_nichee(1000, 10000)
# L'argument 'cible_magique' de chronometreur_magique peut être n'importe quoi ici, par ex None
resultat_boucle_1, temps_boucle_1 = chronometreur_magique(fonction_pour_chrono_1, None)

print(f"--- Mesure de boucle_geante_nichee(1000, 10000) ---")
print(f"Le résultat de la boucle est : {resultat_boucle_1}")
print(f"Le temps pris par la boucle est : {temps_boucle_1:.4f} secondes")

print("\n--- Autre façon de penser la lambda (plus explicite) ---")
# On peut aussi créer une lambda qui "fixe" les arguments et ne prend plus qu'un argument vide
# pour satisfaire la signature de chronometreur_magique
def preparer_boucle_geante():
    return boucle_geante_nichee(1000, 10000)

resultat_boucle_2, temps_boucle_2 = chronometreur_magique(preparer_boucle_geante, None) # On passe None car preparer_boucle_geante n'a pas d'argument
print(f"Le résultat de la boucle est : {resultat_boucle_2}")
print(f"Le temps pris par la boucle est : {temps_boucle_2:.4f} secondes")

# Note : Dans des cas plus complexes, on utiliserait le concept de "currying"
# ou des outils comme `functools.partial` pour créer ces fonctions adaptées.


Exercice 10 : Le Compteur de Clics Magique ! 🖱️🔢

La programmation événementielle, c’est un peu comme si ton programme attendait que quelque chose de spécial se passe (un « événement », comme un clic de souris !) pour réagir. C’est le cœur de toutes les applications avec des boutons, des menus, etc. Et devine quoi ? Les fonctions pures et l’absence d’effets de bord peuvent rendre ces réactions super prévisibles !

Exercice : Reprends le squelette de code Tkinter (la bibliothèque pour créer des interfaces graphiques en Python). Ton objectif est de transformer le bouton en un compteur de clics interactif. À chaque fois que l’utilisateur cliquera sur le bouton, le nombre affiché sur le bouton devra s’incrémenter de 1.

Connaissances nécessaires :

  • Concepts de base de Tkinter : tk.Tk() pour la fenêtre, tk.Button() pour un bouton, b.pack() pour placer un widget, fenetre.mainloop() pour lancer la boucle d’événements.
  • Variable IntVar de Tkinter : Pour gérer des nombres qui peuvent changer et être affichés dans des widgets. C’est une variable spéciale qui « sait » quand elle est modifiée et met à jour automatiquement l’affichage du widget. C’est plus fonctionnel que de modifier directement le texte du bouton à la volée car Tkinter gère l’état pour nous.
  • command dans Tkinter : L’argument command d’un bouton prend une fonction. Cette fonction est appelée sans argument lorsque le bouton est cliqué.

# Solution Exercice 10 : Le Compteur de Clics Magique

import tkinter as tk

fenetre = tk.Tk()

# Connaissance nécessaire : Utiliser une IntVar pour que Tkinter gère l'affichage dynamique
# C'est une façon plus propre de gérer l'état pour les widgets graphiques.
compteur_clics = tk.IntVar() # On crée une variable Tkinter pour notre compteur
compteur_clics.set(0) # On initialise sa valeur à 0

# Le bouton : maintenant son texte est lié à notre IntVar
b = tk.Button(fenetre, textvariable=compteur_clics) # 'textvariable' lie le texte à l'IntVar
b.pack(pady=50) # Juste pour le centrer un peu

# --- Ici, le code demandé pour l'incrémentation ! ---

# La fonction qui sera appelée quand on clique sur le bouton
# Elle n'a pas besoin de prendre d'argument (l'événement est géré par Tkinter en interne)
def incrementer_compteur():
  # On récupère la valeur actuelle de l'IntVar, on l'incrémente, et on la met à jour
  valeur_actuelle = compteur_clics.get()
  compteur_clics.set(valeur_actuelle + 1)
  # Pas d'effet de bord sur une variable globale "normale" ici, on utilise l'IntVar de Tkinter
  # qui est conçue pour gérer l'état de l'interface de manière contrôlée.

# On associe notre fonction au bouton via l'argument 'command'
b.config(command=incrementer_compteur)

# --- Fin du code demandé ---

fenetre.title("Cliquer sur le bouton, magie !")
fenetre.geometry("400x200") # Fenêtre un peu plus petite
fenetre.mainloop()


Exercice 11 : La Symphonie des Chiffres ! 🎼

Dans ce défi, tu es le chef d’orchestre d’une symphonie de chiffres ! Tu as une étiquette qui doit afficher le « chiffre du jour », et 10 boutons, chacun représentant un chiffre de 0 à 9. Quand tu cliques sur un bouton, l’étiquette doit afficher le chiffre de ce bouton.

Exercice : Complète le code Tkinter pour que :

  1. L’étiquette chiffre affiche initialement « Cliquez sur un bouton ! ».
  2. Quand un bouton (de 0 à 9) est cliqué, l’étiquette affiche le chiffre correspondant à ce bouton.

Attention au piège classique de la boucle ! Souviens-toi que lorsque b.bind("<Button-1>", changer_chiffre(c)) est exécuté dans la boucle, changer_chiffre(c) est appelé immédiatement, et c’est le résultat de cet appel qui est lié à l’événement. Or, tu veux que la fonction soit appelée plus tard, quand le bouton est cliqué, avec la bonne valeur de c pour ce bouton spécifique.

Connaissances nécessaires :

  • tk.Label : Un widget pour afficher du texte ou des images.
  • label.config(text=...) : Pour changer le texte d’une étiquette après sa création.
  • grid() : Un gestionnaire de géométrie qui place les widgets dans une grille.
  • Piège de la closure dans les boucles : Quand tu définis une fonction (ou lambda) dans une boucle qui capture une variable de la boucle, cette variable est évaluée au moment où la fonction est appelée, pas au moment où elle est définie. Pour « fixer » la valeur de la variable pour chaque itération, tu dois la passer comme argument par défaut à une lambda ou à une fonction interne.

Explication du piège et de la solution : Si tu écris b.bind("<Button-1>", changer_chiffre(c)), la fonction changer_chiffre est appelée tout de suite dans la boucle for, avec la valeur de c de l’itération courante. Et le résultat de cet appel est ce qui est attaché au bouton. Ce n’est pas ce que tu veux. Tu veux que changer_chiffre soit appelée plus tard, quand le bouton est cliqué.

La solution typique est d’utiliser une lambda fonction qui capture la valeur correcte de c pour chaque bouton :

Python

# Mauvais (piège !) :
# b.bind("<Button-1>", changer_chiffre(c))
# Ici, changer_chiffre(c) est appelé immédiatement, et le résultat est lié.

# Bon (solution) :
# b.bind("<Button-1>", lambda event, arg=c: changer_chiffre(arg))
# Ou plus simple pour 'command' (mais ici c'est bind) :
# b.config(command=lambda arg=c: changer_chiffre(arg))
# La 'lambda' crée une petite fonction qui, elle, sera appelée plus tard.
# L'argument `event` est souvent passé par Tkinter aux fonctions liées à `bind`,
# d'où la nécessité de le capturer si la lambda est directement liée par `bind`.
# Si la lambda est utilisée avec 'command=', pas besoin de 'event'.

# Dans notre cas, avec 'bind', la fonction passée reçoit un objet 'event'.
# Donc si changer_chiffre(c) attend juste `c`, il faut ignorer `event`.
# La meilleure pratique pour `bind` quand on veut passer un argument spécifique :
# `b.bind("<Button-1>", lambda event, chiffre_a_afficher=c: changer_chiffre(chiffre_a_afficher))`
# L'argument par défaut `chiffre_a_afficher=c` "fixe" la valeur de `c` pour chaque lambda créée dans la boucle.


# Solution Exercice 11 : La Symphonie des Chiffres

import tkinter as tk

fenetre = tk.Tk()

# L'étiquette qui affichera le chiffre
chiffre_affiche = tk.Label(fenetre, text="Cliquez sur un bouton !", font=("Arial", 24))
chiffre_affiche.grid(row=1, column=1, columnspan=10, pady=20) # Mieux centré

# --- Ici, le code à compléter pour la fonction changer_chiffre ! ---

def changer_chiffre(chiffre_a_afficher):
  """
  Met à jour le texte de l'étiquette 'chiffre_affiche' avec le chiffre donné.
  Cette fonction est "pure" dans le sens où elle ne modifie pas de variables externes directes,
  mais agit sur un widget Tkinter via sa méthode config.
  """
  # On utilise la méthode 'config' de l'objet Label pour changer son texte
  chiffre_affiche.config(text=chiffre_a_afficher)

# --- Fin du code à compléter ---

# Création des 10 boutons
for i in range(10):
   chiffre_bouton = str(i) # Le chiffre à afficher sur le bouton et à passer à la fonction
   b = tk.Button(fenetre, text=chiffre_bouton, font=("Arial", 16), width=4, height=2)
   b.grid(row=2, column=i+1, padx=2, pady=10) # Placement en grille

   # LE PIÈGE ET SA SOLUTION !
   # On utilise une lambda pour "capturer" la valeur de chiffre_bouton pour chaque itération.
   # 'event=None' est souvent inclus car .bind() passe un objet événement.
   # 'chiffre_pour_callback=chiffre_bouton' crée un argument par défaut qui fixe la valeur de la variable
   # au moment de la définition de la lambda, évitant le problème de la closure.
   b.bind("<Button-1>", lambda event, chiffre_pour_callback=chiffre_bouton: changer_chiffre(chiffre_pour_callback))

fenetre.title("Symphonie des Chiffres")
fenetre.geometry("600x250") # Fenêtre adaptée
fenetre.mainloop()


Exercice 12 : L’Agence de Voyage Spatiale Immuable ! 🚀🌌


Bienvenue à l’Agence de Voyage Spatiale « Pure & Simple » ! Ici, les coordonnées des planètes (nos « points ») et les formations stellaires (nos « triangles ») sont immuables. Une fois que tu as défini une planète, elle ne change jamais de place ! Si tu veux la « déplacer », nous te donnons simplement une nouvelle planète à ses nouvelles coordonnées. C’est l’essence de l’immuabilité en programmation fonctionnelle : pas de modification en place, juste de nouvelles créations !

Exercice : Utilise la bibliothèque point_imm.py fournie ci-dessous pour :

  1. Crée 4 « planètes » : alpha (2,5), beta (7,1), gamma (3,8), et delta (9,4).
  2. Écris une fonction deplacer_formation_stellaire(formation_orig, deplacement_x, deplacement_y) qui prend une formation stellaire (formation_orig, qui est un triangle) et des valeurs de déplacement (deplacement_x, deplacement_y). Elle doit retourner une nouvelle formation stellaire où chaque planète a été déplacée. Rappelle-toi : utilise la fonction deplacer existante et crée toujours de nouvelles entités !
  3. Crée deux « constellations » :
    • constellation_matin : formée des planètes alpha, beta, gamma.
    • constellation_soir : formée des planètes beta, gamma, delta.
  4. Lance des « expéditions » pour créer de nouvelles constellations :
    • constellation_lointaine_1 : obtenue en déplaçant constellation_matin de (-1, -1).
    • constellation_lointaine_2 : obtenue en déplaçant constellation_soir de (2, 3).

Affiche toutes tes planètes et constellations pour voir la magie de l’immuabilité !

Connaissances nécessaires : Revoir la manipulation des tuples, et bien comprendre que les fonctions de cette bibliothèque ne modifient pas leurs arguments, mais en renvoient de nouveaux.


# Solution Exercice 12 : L'Agence de Voyage Spatiale Immuable

# fichier point_imm.py (reproduction pour l'exercice)
def point(x, y):
   """construire un point"""
   return (x, y)

def deplacer(p, dx, dy):
   """translation d’un point (renvoie un nouveau point !)"""
   return point(p[0]+dx, p[1]+dy)

def triangle(p1, p2, p3):
   """construction d’un triangle (tuple de points !)"""
   return (p1, p2, p3)

# une constante utile
origine = point(0, 0)

# --- Code Python pour l'exercice ---

# 1. Créer 4 planètes (points)
alpha = point(2, 5)
beta = point(7, 1)
gamma = point(3, 8)
delta = point(9, 4)

print(f"Planète Alpha : {alpha}")
print(f"Planète Beta : {beta}")
print(f"Planète Gamma : {gamma}")
print(f"Planète Delta : {delta}")
print("-" * 30)

# 2. Écrire la fonction deplacer_formation_stellaire
def deplacer_formation_stellaire(formation_orig, deplacement_x, deplacement_y):
  """
  Déplace chaque point d'une formation stellaire (triangle) et renvoie une NOUVELLE formation.
  """
  p1_deplace = deplacer(formation_orig[0], deplacement_x, deplacement_y)
  p2_deplace = deplacer(formation_orig[1], deplacement_x, deplacement_y)
  p3_deplace = deplacer(formation_orig[2], deplacement_x, deplacement_y)
  
  # On crée un nouveau triangle avec les points déplacés
  return triangle(p1_deplace, p2_deplace, p3_deplace)

# 3. Créer deux constellations (triangles)
constellation_matin = triangle(alpha, beta, gamma)
constellation_soir = triangle(beta, gamma, delta)

print(f"Constellation du Matin : {constellation_matin}")
print(f"Constellation du Soir : {constellation_soir}")
print("-" * 30)

# 4. Lancer des expéditions pour créer de nouvelles constellations
deplacement_1_x, deplacement_1_y = -1, -1
deplacement_2_x, deplacement_2_y = 2, 3

constellation_lointaine_1 = deplacer_formation_stellaire(
    constellation_matin, deplacement_1_x, deplacement_1_y
)
constellation_lointaine_2 = deplacer_formation_stellaire(
    constellation_soir, deplacement_2_x, deplacement_2_y
)

print(f"Constellation lointaine 1 (déplacée de {deplacement_1_x},{deplacement_1_y}) : {constellation_lointaine_1}")
print(f"Constellation lointaine 2 (déplacée de {deplacement_2_x},{deplacement_2_y}) : {constellation_lointaine_2}")
print("-" * 30)

# Vérifions que les originales sont intactes (le principe de l'immuabilité !)
print(f"Constellation du Matin (originale, inchangée) : {constellation_matin}")
print(f"Constellation du Soir (originale, inchangée) : {constellation_soir}")


Exercice 13 L’Agence de Voyage Spatiale « Mutante » ! 👾🌌


Bienvenue à l’Agence de Voyage Spatiale « Mutante » ! Ici, les planètes (nos « points ») et les formations stellaires (nos « triangles ») sont… eh bien, mutables ! Ça veut dire que quand tu « déplaces » une planète, c’est la planète elle-même qui change de coordonnées. C’est pratique, mais ça peut créer des surprises et des « effets de bord » imprévus si tu n’y prends pas garde !

Exercice : Utilise la bibliothèque point_mut.py fournie ci-dessous pour explorer les conséquences de la mutabilité.

  1. Question de philosophie : Est-ce une bonne idée d’avoir une variable globale origine = point(0, 0) dans ce système « mutant » ? Explique pourquoi. (Réfléchis à ce qui se passerait si tu déplaces origine par erreur !)
  2. Crée 4 « planètes » : alpha (2,5), beta (7,1), gamma (3,8), et delta (9,4).
  3. Crée deux « constellations » :
    • constellation_matin_mut : formée des planètes alpha, beta, gamma.
    • constellation_soir_mut : formée des planètes beta, gamma, delta.
  4. Lance des « expéditions » pour déplacer les constellations :
    • Déplace constellation_matin_mut de (-1, -1).
    • Déplace constellation_soir_mut de (2, 3). Observe bien les coordonnées de toutes les planètes et constellations après ces opérations. Obtiens-tu les mêmes résultats que dans l’exercice précédent avec l’approche « immuable » pour constellation_lointaine_1 et constellation_lointaine_2 ? Qu’est-ce qui se passe avec beta et gamma ?
  5. Défi de l’ingénieur spatial : Propose une modification (une « mise à jour logicielle ») à la classe triangle dans point_mut.py pour qu’elle puisse déplacer un triangle sans modifier les points originaux qui le composent, mais plutôt en créant un nouveau triangle avec des copies déplacées des points. Fais-la ressembler à la fonction deplacer_formation_stellaire de l’exercice précédent.

Connaissances nécessaires :

  • Objets mutables vs. immuables : Comprendre la différence fondamentale. Les objets mutables peuvent être modifiés après leur création (listes, dictionnaires, instances de classes par défaut). Les objets immuables ne peuvent pas (tuples, nombres, chaînes de caractères).
  • Copie d’objets : Pour éviter les effets de bord avec des objets mutables, il faut parfois faire des copies explicites (ex: point_copy = point(p.x, p.y) ou import copy; copy.deepcopy(obj)).

Python

# Solution Exercice 13 : L'Agence de Voyage Spatiale "Mutante"

# fichier point_mut.py (reproduction pour l'exercice)
class point:
   def __init__(self, x, y):
      self.x = x
      self.y = y

   def deplacer(self, dx, dy):
      # Modifie l'instance du point EN PLACE
      self.x += dx
      self.y += dy
   
   def __repr__(self): # Pour un affichage plus clair du point
       return f"Point({self.x}, {self.y})"

class triangle:
   def __init__(self, p1, p2, p3):
      self.p1 = p1
      self.p2 = p2
      self.p3 = p3

   def deplacer(self, dx, dy):
      # Modifie les points DEJA EXISTANTS du triangle EN PLACE
      self.p1.deplacer(dx, dy)
      self.p2.deplacer(dx, dy)
      self.p3.deplacer(dx, dy)

   def __repr__(self): # Pour un affichage plus clair du triangle
       return f"Triangle({self.p1}, {self.p2}, {self.p3})"

# --- Code Python pour l'exercice ---

# 1. Question de philosophie : Est-il pertinent de rajouter une variable globale `origine` ?
# Réponse : NON ! Ce serait une TRÈS mauvaise idée.
# Si 'origine' était un `point` (objet mutable), et qu'une fonction déplaçait `origine`
# pour une raison quelconque (par exemple, pour un calcul temporaire), alors TOUS
# les autres modules qui utiliseraient `origine` se retrouveraient avec un point déplacé
# sans le savoir. Cela créerait des effets de bord extrêmement difficiles à débugger.
# En programmation orientée objet/impérative avec des objets mutables, les globales sont à éviter,
# surtout si elles représentent un état qui pourrait être modifié.

print("--- Analyse de la mutabilité ---")
print("1. Rajouter une variable globale 'origine' mutable est une mauvaise idée.")
print("   Si l'objet 'origine' est modifié par erreur, cela aurait des effets de bord imprévisibles")
print("   sur toutes les parties du code qui s'attendent à ce qu'il reste (0,0).")
print("-" * 30)

# 2. Créer 4 points a, b, c et d.
alpha_mut = point(2, 5)
beta_mut = point(7, 1)
gamma_mut = point(3, 8)
delta_mut = point(9, 4)

print(f"Planète Alpha (mut) : {alpha_mut}")
print(f"Planète Beta (mut) : {beta_mut}")
print(f"Planète Gamma (mut) : {gamma_mut}")
print(f"Planète Delta (mut) : {delta_mut}")
print("-" * 30)

# 3. Créer deux triangles t1 et t2 constitué de a, b, c et b, c, d.
constellation_matin_mut = triangle(alpha_mut, beta_mut, gamma_mut)
constellation_soir_mut = triangle(beta_mut, gamma_mut, delta_mut) # PARTAGE DES POINTS BETA ET GAMMA !

print(f"Constellation du Matin (mut) AVANT déplacement : {constellation_matin_mut}")
print(f"Constellation du Soir (mut) AVANT déplacement : {constellation_soir_mut}")
print(f"Points individuels AVANT : Beta={beta_mut}, Gamma={gamma_mut}")
print("-" * 30)

# 4. Déplacer t1 de (-1,-1) et t2 de (2,3).
print("Déplacement des constellations...")
constellation_matin_mut.deplacer(-1, -1)
constellation_soir_mut.deplacer(2, 3)

print(f"Constellation du Matin (mut) APRÈS déplacement (-1,-1) : {constellation_matin_mut}")
print(f"Constellation du Soir (mut) APRÈS déplacement (2,3) : {constellation_soir_mut}")
print(f"Points individuels APRÈS : Beta={beta_mut}, Gamma={gamma_mut}")
print("-" * 30)

# Comparaison avec l'Exercice 12 (Partie 1)
# Non, on n'obtient PAS des triangles équivalents aux triangles t3 et t4 de la Partie 1.
# Le problème ici est que beta_mut et gamma_mut sont des objets partagés entre
# constellation_matin_mut et constellation_soir_mut.
# Quand constellation_matin_mut.deplacer(-1, -1) est appelé, beta_mut et gamma_mut sont modifiés.
# Puis, quand constellation_soir_mut.deplacer(2, 3) est appelé, beta_mut et gamma_mut sont à nouveau modifiés,
# mais à partir de LEURS NOUVELLES POSITIONS, et cela affecte aussi constellation_matin_mut
# car elle pointe toujours vers les MÊMES objets beta_mut et gamma_mut.
# C'est l'effet de bord classique de la mutabilité !

print("4. Obtient-on des triangles équivalents à ceux de la Partie 1 ? NON.")
print("   Les points Beta et Gamma sont PARTAGÉS entre les deux triangles. ")
print("   Le déplacement d'un triangle affecte aussi l'autre si les points sont partagés,")
print("   ce qui est une conséquence des effets de bord de la mutabilité.")
print("   Les coordonnées de Beta et Gamma sont le résultat d'une double modification !")
print("-" * 30)


# 5. Proposer une modification de l’implémentation mutable qui corrige le problème.
# L'objectif est de rendre la méthode 'deplacer' de 'triangle' non destructive,
# c'est-à-dire qu'elle retourne un NOUVEAU triangle avec des points déplacés,
# au lieu de modifier les points du triangle original.

import copy # Pour copier les points proprement

# Nouvelle implémentation (fichier point_mut_corrige.py)
class point_corrige:
   def __init__(self, x, y):
      self.x = x
      self.y = y

   def deplacer(self, dx, dy):
      # Cette méthode continue de modifier le point en place,
      # mais la classe triangle_corrige va l'utiliser différemment.
      self.x += dx
      self.y += dy
   
   def __repr__(self):
       return f"Point({self.x}, {self.y})"

class triangle_corrige:
   def __init__(self, p1, p2, p3):
      # Il est crucial que ces points soient des COPIES si on veut éviter les partages implicites
      # lorsque les points originaux sont passés comme arguments.
      # Pour cet exercice, on va se concentrer sur la méthode deplacer pour créer de nouveaux points.
      self.p1 = p1
      self.p2 = p2
      self.p3 = p3

   def deplacer_non_destructif(self, dx, dy):
      """
      Déplace le triangle en renvoyant un NOUVEAU triangle avec les points déplacés.
      Les points du triangle original NE SONT PAS MODIFIÉS.
      """
      # On crée de NOUVEAUX points pour le nouveau triangle
      # On doit copier les points originaux avant de les déplacer pour ne pas modifier les originaux
      new_p1 = point_corrige(self.p1.x, self.p1.y) # Crée une copie du point 1
      new_p2 = point_corrige(self.p2.x, self.p2.y) # Crée une copie du point 2
      new_p3 = point_corrige(self.p3.x, self.p3.y) # Crée une copie du point 3

      new_p1.deplacer(dx, dy) # Déplace la COPIE
      new_p2.deplacer(dx, dy) # Déplace la COPIE
      new_p3.deplacer(dx, dy) # Déplace la COPIE
      
      # On retourne un nouveau triangle avec ces nouveaux points déplacés
      return triangle_corrige(new_p1, new_p2, new_p3)

   def __repr__(self):
       return f"Triangle({self.p1}, {self.p2}, {self.p3})"

print("5. Proposition de modification pour corriger le problème (méthode non-destructive) :")

# Recréons les points originaux pour le test de la version corrigée
alpha_corrige = point_corrige(2, 5)
beta_corrige = point_corrige(7, 1)
gamma_corrige = point_corrige(3, 8)
delta_corrige = point_corrige(9, 4)

constellation_matin_corrige = triangle_corrige(alpha_corrige, beta_corrige, gamma_corrige)
constellation_soir_corrige = triangle_corrige(beta_corrige, gamma_corrige, delta_corrige)

print(f"Constellation du Matin (corrige) AVANT déplacement : {constellation_matin_corrige}")
print(f"Constellation du Soir (corrige) AVANT déplacement : {constellation_soir_corrige}")
print(f"Points individuels AVANT : Beta={beta_corrige}, Gamma={gamma_corrige}")
print("-" * 30)

print("Déplacement des constellations via la nouvelle méthode non-destructive...")
# Les variables _t1 et _t2 sont les NOUVEAUX triangles résultants des déplacements
constellation_lointaine_1_corrige = constellation_matin_corrige.deplacer_non_destructif(-1, -1)
constellation_lointaine_2_corrige = constellation_soir_corrige.deplacer_non_destructif(2, 3)

print(f"Constellation du Matin (corrige) APRÈS déplacement (originale inchangée) : {constellation_matin_corrige}")
print(f"Constellation du Soir (corrige) APRÈS déplacement (originale inchangée) : {constellation_soir_corrige}")
print(f"Points individuels APRÈS (originaux inchangés) : Beta={beta_corrige}, Gamma={gamma_corrige}")
print("-" * 30)

print(f"Nouvelle Constellation lointaine 1 (obtenue du Matin) : {constellation_lointaine_1_corrige}")
print(f"Nouvelle Constellation lointaine 2 (obtenue du Soir) : {constellation_lointaine_2_corrige}")

# Maintenant, les résultats pour constellation_lointaine_1_corrige et _2_corrige
# devraient correspondre à t3 et t4 de la partie 1, car les points originaux
# n'ont pas été modifiés.

Une réflexion sur “ Programmation fonctionnelle ”

Les commentaires sont fermés