Up.oN
#24/03/20225 Update: #10/04/2025

L'Algorithmique

Dans cette partie, on va aborder les opérateurs, les conditions et les boucles. Enfin, nous verrons quelques mots clés.

Les opérateurs de comparaison

Rien de particulier ici:

  • < inférieur à
  • <= inférieur ou égale à
  • > supérieur à
  • >= supérieur ou égale à
  • == égalité
  • != différent

Concernant les chaînes de caractères:

"hello" == "hello" # True
"hello" == "world" # False

"hello" >= "hello" # True
"auth"  >= "world"  # True - compare la première lettre - ordre lexicographique (table ASCII)

Les opérateurs logiques

Au nombre de trois: and, or et not .

L'ordre de priorité est le suivant: not, and et or .

And

a b a and b
True True True
True False False
False False False
False True False

Or

a b a or b
True True True
True False True
False True True
False False False

Not

a not a
True False
False True

Instruction SI

Rappel: Comme expliqué dans les fondamentaux, les indentations en Python ont une signification et indique un 'block' de code. C'est l'équivalent des { } en php.

# instruction SI 
if condition:
    ...

# instruction SI ... SINON 
if condition:
    ...
else:
    ...

# instruction SI ... SINON SI ... SINON ... 
if condition:
    ...
elif condition:
    ...
else:
   ...

Opérateur ternaire

Le sucre syntaxique n'est pas aussi sexy qu'en php ici (pas de ?? non plus).

Toutefois, voici l'opérateur ternaire en Python:

# opérateur ternaire 
valeur_si_vrai if condition else valeur_si_faux

# exemple
age = 20
message = "Majeur" if age >= 18 else "Mineur"
print(message)  # Affiche "Majeur"

Le cas de

Le Python ne dispose pas de switch comme en php. Par contre, il existe un match ... case, qui est "similaire" au match de php.

# exemple de match

def choix_action(action):
  match action:
      case "start":
          return "Démarrage en cours..."
      case "stop":
          return "Arrêt en cours..."
      case "pause":
          return "Pause activée..."
      case _:
          return "Action inconnue !"
          
print(choix_action("pause"))  # Affiche "Pause activée..."

Les boucles

Python, utilise une combinaison de for et de la fonction range(). On retrouve également la traditionnelle boucle while.

For ... range()

range([start], stop, [step]) permet de générer une séquence de nombre:

  • start optionnel: valeur de départ (par défaut 0)
  • stop obligatoire: valeur d'arrêt
  • step optionnel: le pas (par défaut 1)
Combiné avec le mot clé for, on obtient la boucle for:

# exemple de boucle for
for i in range(5):  # Equivalent à range(0, 5, 1)
    print(i)
            
# affiche 0,1,2,3,4

(do) While - "faire ... jusqu'à"

La boucle universelle.

# syntaxe de la boucle while
while condition:
    # Code à exécuter

#exemple 
i = 1
while i <= 5:
    print(i)
    i += 1
            
# i vaut à chaque tour de boucle : 1,2,3,4,5,6
# arrivé à 6 la boucle s'arrête

While - "tant que"

En Python, il n'existe pas de structure native « do…while » comme en JavaScript ou PHP. Cependant, on peut en simuler le comportement de manière condensée en utilisant une boucle infinie avec une condition de sortie placée après le bloc d'instructions.

# Syntaxe simulée do…while
while True:
# Bloc d'instructions à exécuter (exécuté au moins une fois)
    # ...

    # Vérification de la condition après l'exécution du bloc
    if condition_de_sortie:
        break

# Exemple simple
i = 0
while True:
    print(i)
    i += 1
    if i >= 3:  # Condition de sortie, simulant "faire... jusqu'à"
        break
  • La boucle while True garantit que le bloc s'exécute au moins une fois.
  • La condition de sortie (if i >= 3) est vérifiée à la fin de chaque itération.
  • Lorsque la condition est vraie, break interrompt la boucle, simulant ainsi le comportement d'un do…while.

For ... else

En Python, vous pouvez associer un bloc else à une boucle for (ou à une boucle while). Le bloc else s'exécute uniquement lorsque la boucle se termine normalement, c'est-à-dire lorsque l'itération sur l'objet (liste, chaîne, etc.) est complètement effectuée, sans que l'instruction break ait été rencontrée. Autrement dit, si l'exécution de la boucle est interrompue prématurément par un break, le code contenu dans le else ne sera pas exécuté.

Syntaxe:

for élément in collection:
# instructions pour chaque élément
    if condition_de_sortie:
        break
else:
# instructions à exécuter si aucun break n'a été rencontré

Quelques points importants :

  • Le bloc else doit être indenté au même niveau que le for.
  • Il est exécuté uniquement après la boucle, si celle-ci s'est déroulée sans interruption par break.

Utilités et cas d'usage

Recherche d'un élément

L'un des usages les plus courants du for ... else est dans la recherche d'un élément qui satisfait une condition. Plutôt que d'utiliser une variable booléenne pour savoir si l'élément a été trouvé, le bloc else gère automatiquement le cas « pas trouvé » après l'itération.

# Exemple avec une recherche dans une chaîne de caractères
mot = "bonjour"
recherche = "z"
for lettre in mot:
    if lettre == recherche:
        print("Lettre trouvée!")
        break
else:
    print("Lettre non trouvée dans le mot.")

Validation de conditions sur tous les éléments

Si vous avez besoin de vérifier qu'un ensemble d'éléments respecte une condition et de réagir différemment en fonction de la présence d'une exception (via break), le for ... else vous permet de bien séparer ces deux cas.

Rappel des points importants

  • Exécution du else: Se déclenche uniquement si la boucle se termine sans interruption par break.
  • Lecture séquentielle: Le else est associé directement à la boucle et non au if.
  • Utilisation judicieuse: Bien que l'usage du for ... else puisse prêter à confusion, il s'avère très élégant pour les cas de recherche ou de vérification intégrale dans une collection.

Conseils et bonnes pratiques

  • Clarté du code: Comme cette construction est moins utilisée que le simple for, ajoutez un commentaire si nécessaire pour expliquer son comportement dans le contexte de votre code.
  • Tests: Lors du débogage, vérifiez bien que le break se comporte comme attendu pour éviter d'exécuter le bloc else par inadvertance.

La boucle for ... else en Python est un outil puissant pour gérer des cas de boucles où la complétion normale (sans interruption) doit déclencher une action spécifique. Que ce soit pour la recherche d'un élément ou la validation complète d'un critère, cette syntaxe offre une solution élégante et lisible dès lors qu'elle est bien documentée dans votre code.

While ... else

La boucle while s'exécute tant qu'une condition donnée est vraie. On peut ajouter une clause else à cette boucle. La particularité est la suivante:

  • Le bloc else s'exécute uniquement si la boucle while se termine "normalement", c'est-à-dire lorsque la condition devient fausse.
  • Si l'exécution de la boucle est interrompue par un break, le bloc else n'est pas exécuté.
Syntaxe:

while condition:
# bloc d'instructions exécuté tant que condition est vraie
    if condition_de_sortie:
        break
    # d'autres instructions
else:
# bloc d'instructions exécuté si la boucle s'est terminée sans rencontrer de break

Points clés à retenir :

  • Le while vérifie la condition avant chaque itération.
  • Le break est utilisé pour interrompre la boucle prématurément.
  • Le bloc >else permet de traiter la situation où la condition n'a plus de raison de rester vraie (fin normale de la boucle).

Exemple pratique

Recherche d'un élément dans une séquence
Imaginons que nous voulons parcourir une liste pour chercher si un élément particulier y figure. Si l'élément est trouvé, on interrompt la boucle avec un break. Sinon, si la boucle se termine sans interruption, le bloc else nous informe que l'élément n'existe pas dans la séquence.

# Exemple sans trouver l'élément
valeurs = [2, 4, 6, 8]

i = 0
while i < len(valeurs):
# On cherche un nombre impair
    if valeurs[i] % 2 != 0:
        print(f"Nombre impair trouvé : {valeurs[i]}")
        break
    i += 1
else:
    print("Aucun nombre impair n'a été trouvé dans la liste.")

Explications:

  • La boucle while parcourt les indices de la liste tant que i est inférieur à la longueur de la liste.
  • Pour chaque élément, la condition vérifie si le nombre est impair.
  • Puisque tous les nombres de la liste sont pairs, la condition if n'est jamais satisfaite et le break n'est jamais exécuté.
  • À la fin de la boucle, la clause else s'exécute et affiche que aucun nombre impair n'a été trouvé.

# Exemple en trouvant l'élément
valeurs = [2, 4, 5, 8]

i = 0
while i < len(valeurs):
# On recherche un nombre impair
    if valeurs[i] % 2 != 0:
        print(f"Nombre impair trouvé : {valeurs[i]}")
        break
    i += 1
else:
    print("Aucun nombre impair n'a été trouvé dans la liste.")

Explications:

  • Dans ce cas, le nombre 5 est présent dans la liste.
  • Lorsque i atteint l'indice correspondant à 5, la condition est vraie, le message est affiché et le break interrompt la boucle.
  • Comme le break est exécuté, le bloc else n'est pas atteint.

Applications et avantages

Traitement de recherches ou validations

Le couple while ... else est très utile dans des situations de recherche ou de validation de conditions sur une collection d'éléments. Il permet de distinguer clairement le cas où :

  • La condition d'arrêt est atteinte parce qu'on a trouvé l'élément recherché (utilisation de break).
  • La boucle a parcouru toutes les possibilités sans trouver ce qui était recherché (exécution du bloc else).

Amélioration de la lisibilité du code

L'utilisation de else avec les boucles permet de simplifier la gestion d'un cas particulier. Au lieu de devoir utiliser une variable flag pour déterminer si une interruption par break est intervenue, la syntaxe intégrée assure que l'action voulue (traiter le cas "non trouvé") est clairement associée à la fin normale de la boucle.

Cas particuliers et conseils
Le cas où la condition de la boucle est fausse dès le départ

Si la condition du while est fausse dès le début, le bloc while ne s'exécute même pas, et le bloc else s'exécute immédiatement. Cela permet de gérer automatiquement le cas d'une collection vide ou d'une situation où l'état initial n'exige pas d'itération.
Exemple:

nombre = 0
while nombre > 0:
# Cette boucle ne s'exécutera pas car la condition est déjà fausse
    print("Cette instruction ne sera jamais affichée.")
    nombre -= 1
else:
    print("La boucle while ne s'est jamais exécutée, bloc else exécuté.")

Bonnes pratiques:

  • Commenter le code: Le comportement du else avec la boucle while n'est pas évident pour tous les développeurs. Ajoutez des commentaires pour clarifier son rôle et éviter toute confusion.
  • Utiliser dans des contextes précis: Privilégiez cet usage lorsque vous traitez des cas où l'absence d'un élément ou d'une condition spécifique doit être explicitement gérée.
  • Tester les deux chemins: Veillez à vérifier que votre code fonctionne correctement à la fois dans le cas d'une interruption via break et dans le cas d'une fin "naturelle" de la boucle.


La structure while ... else en Python permet de différencier clairement la fin naturelle d'une boucle de son interruption par un break. C'est un outil très utile pour améliorer la clarté du code, notamment dans des scénarios de recherche ou de validation d'éléments dans une séquence. Bien que moins utilisé que la boucle for ... else, il s'agit d'une fonctionnalité puissante qui, une fois bien comprise, permet d'écrire un code plus lisible et mieux structuré.

Iterable - Iterator - iter()

En Python, un itérable (iterable) est tout objet sur lequel on peut itérer, c'est-à-dire parcourir ses éléments un par un. Cela inclut des objets comme les listes, les tuples, les chaînes de caractères, les dictionnaires, etc. (voir les structures de données). Concrètement, un objet est itérable s'il implémente la méthode spéciale __iter__(), qui renvoie un itérateur.

Un itérateur (iterator), de son côté, est un objet qui représente une séquence de données et qui permet d'accéder à ses éléments un à un, généralement à l'aide de la méthode __next__(). Lorsqu'on appelle __next__(), l'itérateur renvoie l'élément suivant de la séquence. Lorsque la séquence est épuisée, un appel à __next__() déclenche l'exception StopIteration.

# Un itérable : une liste
ma_liste = [1, 2, 3, 4]

# Obtenir un itérateur à partir de l'itérable
iterateur = iter(ma_liste)

# Utilisation de l'itérateur avec la fonction next()
print(next(iterateur))  # Affiche 1
print(next(iterateur))  # Affiche 2
print(next(iterateur))  # Affiche 3
print(next(iterateur))  # Affiche 4

# La prochaine instruction lèvera une exception StopIteration
# print(next(iterateur))

En pratique, les boucles for utilisent ces concepts en interne pour parcourir automatiquement les éléments d'un itérable :

for element in ma_liste:
    print(element)

Ici, Python appelle implicitement iter(ma_liste) pour obtenir un itérateur, puis utilise next() à chaque itération jusqu'à ce que l'itérateur soit épuisé.

Pourquoi ces concepts sont-ils utiles ?

  • Gestion de la mémoire: Les itérateurs permettent de générer des séquences sans avoir à stocker tous les éléments en mémoire simultanément (par exemple, avec des générateurs).
  • Flexibilité: Ils oVous pouvez créer vos propres classes itérables en implémentant les méthodes __iter__() et __next__(), ce qui est très utile pour des besoins spécifiques de parcours de données.
En résumé, les itérables et les itérateurs sont des piliers du paradigme de l'itération en Python, rendant le traitement des séquences de données à la fois simple et puissant.

map()

La fonction intégrée map() permet d'appliquer une fonction à chaque élément d'un ou plusieurs itérables (comme des listes, tuples, etc.) et retourne un itérateur contenant les résultats. Elle est particulièrement utile pour transformer rapidement des collections de données sans avoir à écrire de boucles explicites. Cette approche est souvent plus concise et peut améliorer la lisibilité du code.

La syntaxe générale de map() est :

map(fonction, itérable, ...)

  • fonction: La fonction à appliquer à chaque élément. Elle peut être une fonction définie, ou une fonction lambda.
  • itérable: Un ou plusieurs objets sur lesquels on souhaite itérer. Si plusieurs itérables sont fournis, la fonction doit accepter autant d'arguments qu'il y a d'itérables.
Le résultat de map() est un objet de type map, qui est un itérateur. Pour obtenir une liste ou un tuple des résultats, il faut le convertir explicitement avec list() ou tuple().

Application d'une fonction simple:

def carre(nombre):
    return nombre ** 2

nombres = [1, 2, 3, 4, 5]
resultat = map(carre, nombres)

# Conversion de l'objet map en liste pour afficher le résultat
print(list(resultat))  # Affiche : [1, 4, 9, 16, 25]

Ici, la fonction carre est appliquée à chaque élément de la liste nombres.

Utilisation avec une fonction lambda.
Pour des opérations simples, on peut utiliser une fonction lambda directement dans map():

nombres = [1, 2, 3, 4, 5]
resultat = map(lambda x: x ** 2, nombres)
print(list(resultat))  # Affiche : [1, 4, 9, 16, 25]

L'utilisation d'une lambda permet d'écrire le code de manière plus concise, surtout pour des transformations simples.

Traitement de plusieurs itérables.
map() peut aussi travailler sur plusieurs itérables simultanément. Par exemple, additionner les éléments correspondants de deux listes :

liste1 = [1, 2, 3]
liste2 = [4, 5, 6]
resultat = map(lambda x, y: x + y, liste1, liste2)
print(list(resultat))  # Affiche : [5, 7, 9]

Dans ce cas, la lambda reçoit deux arguments (un de chaque liste) et renvoie leur somme.

Points importants:

  • Évaluation paresseuse: L'objet retourné par map() est un itérateur, ce qui signifie que les valeurs ne sont calculées que lorsque vous itérez dessus. Cela permet d'économiser de la mémoire, surtout lorsqu'on travaille avec de grandes collections.
  • Conversion explicite: En Python 3, pour voir les résultats de map(), il faut convertir l'itérateur en liste ou en tuple.
  • Utilisation de plusieurs itérables: Si les itérables n'ont pas la même longueur, l'itération s'arrête dès que le plus court est épuisé.
  • Lisibilité et concision: map() peut rendre votre code plus compact, mais il faut veiller à ne pas trop compliquer la compréhension si la fonction appliquée devient trop complexe. Dans ce cas, une boucle for explicite pourrait être préférable.

filter()

La fonction intégrée filter() permet de filtrer un itérable en ne retenant que les éléments qui satisfont une condition. Concrètement, elle prend en argument :

  • Une fonction: qui reçoit un élément et retourne une valeur évaluée en booléen (vrai ou faux)
  • Un itérable: (liste, tuple, chaîne, etc.) sur lequel la fonction sera appliquée.
Si la fonction retourne une valeur "vraie" (truthy) pour un élément, celui-ci est conservé dans l'itérateur résultant. Sinon, il est ignoré.
Si vous passez None comme fonction, Python utilise implicitement l'identité (c'est-à-dire qu'il ne garde que les éléments "vrais", en excluant par exemple 0, False, None, ou des chaînes vides).

filter(function, iterable)

  • function: Une fonction (ou lambda) qui reçoit un élément et retourne un booléen.
  • iterable: Un objet sur lequel on peut itérer (liste, tuple, chaîne, dictionnaire, etc.).
Le résultat est un objet de type filter, qui est un itérateur. Pour obtenir une liste ou un tuple, il faudra le convertir explicitement avec list() ou tuple().

Exemple avec une liste, filtrer les nombres pairs:

nombres = [1, 2, 3, 4, 5, 6, 7, 8]

# Utilisation d'une fonction lambda pour ne garder que les nombres pairs
nombres_pairs = filter(lambda x: x % 2 == 0, nombres)

# Conversion en liste pour afficher le résultat
print(list(nombres_pairs))  # Affiche : [2, 4, 6, 8]

Explication : La lambda retourne True pour les nombres divisibles par 2 (pairs). Seuls ces nombres sont conservés dans l'itérateur.

map() != filter()

Bien que map() et filter() soient toutes deux des fonctions d'ordre supérieur utilisées pour traiter des itérables, elles remplissent des rôles différents :

  • map(): Applique une fonction à tous les éléments de l’itérable et renvoie un nouvel itérable contenant les résultats.
  • filter(): Applique une fonction (qui doit retourner un booléen) à chaque élément, mais ne conserve que ceux pour lesquels la fonction renvoie True.

# map exemple
# Calculer le carré de chaque nombre
carres = map(lambda x: x ** 2, [1, 2, 3, 4])
print(list(carres))  # Affiche : [1, 4, 9, 16]

# filter exemple
# Ne garder que les nombres pairs
pairs = filter(lambda x: x % 2 == 0, [1, 2, 3, 4])
print(list(pairs))  # Affiche : [2, 4]

En résumé :

  • map() transforme chaque élément (même si le résultat peut être identique à l'élément d'origine);
  • filter() sélectionne uniquement les éléments répondant à une condition donnée, sans les transformer.

reduce()

La fonction reduce() de la bibliothèque functools est un outil de programmation fonctionnelle qui permet de réduire un itérable à une seule valeur en appliquant de manière cumulative une fonction binaire à ses éléments.

from functools import reduce

Le but: Transformer une séquence (liste, tuple, etc.) en une unique valeur (par exemple, la somme, le produit, la concaténation, etc.).
Via une fonction binaire: Une fonction qui prend deux arguments. La fonction est d'abord appliquée aux deux premiers éléments de l'itérable, puis le résultat est combiné avec le troisième élément, et ainsi de suite jusqu'à obtenir une valeur finale.

Syntaxe:

from functools import reduce

reduce(function, iterable[, initializer])

  • function: La fonction à appliquer. Elle doit accepter deux arguments.
  • iterable: L’itérable sur lequel réduire (liste, tuple, etc.).
  • initializer (optionnel): Une valeur initiale qui est utilisée comme premier argument pour la réduction. Si elle est fournie, la réduction commence avec cette valeur et le premier élément de l’itérable.

# exemple Calcul de la somme d'une liste de nombres
nombres = [1, 2, 3, 4]
somme = 0
for n in nombres:
    somme += n
print(somme)  # Affiche : 10

# avec reduce()
from functools import reduce

nombres = [1, 2, 3, 4]
somme = reduce(lambda x, y: x + y, nombres)
print(somme)  # Affiche : 10

Explication:
La lambda reçoit deux nombres à chaque appel :

  • D'abord : x = 1, y = 2 → renvoie 3
  • Ensuite : x = 3, y = 3 → renvoie 6
  • Puis : x = 6, y = 4 → renvoie 10

Autre exemple, Calcul du produit des éléments d'une liste:

from functools import reduce

nombres = [2, 3, 4]
produit = reduce(lambda x, y: x * y, nombres)
print(produit)  # Affiche : 24

Explication:
La lambda reçoit deux nombres à chaque appel :

  • 2 x 3 = 6
  • 6 x 4 = 24

Avec l'utilisation d'un initializer:

from functools import reduce

nombres = [1, 2, 3, 4]
# On commence la somme avec 10
somme = reduce(lambda x, y: x + y, nombres, 10)
print(somme)  # Affiche : 20 (10 + 1 + 2 + 3 + 4)

Quand utiliser reduce() ?

  • Avantages
    • Permet de condenser un itérable en une seule valeur en une seule ligne.
    • Utile pour appliquer une opération cumulative sur une séquence (par exemple, la réduction de données, le calcul de produits, ou d'autres agrégations).
  • Attention
    • Le code utilisant reduce() peut parfois être moins lisible pour ceux qui ne sont pas familiers avec la programmation fonctionnelle.
    • Parfois, des boucles explicites ou d'autres fonctions (comme sum() pour la somme ou math.prod() pour le produit en Python 3.8+) peuvent être plus simples et clairs.

La fonction reduce() est un outil puissant pour réduire un itérable à une seule valeur en appliquant une fonction binaire de manière cumulative. Elle fait partie de la programmation fonctionnelle en Python et se trouve dans le module functools.

Que ce soit pour calculer une somme, un produit ou pour combiner des chaînes, reduce() offre une syntaxe concise qui peut simplifier votre code lorsque l'opération se prête bien à une réduction.

sorted()

La fonction intégrée sorted() permet de trier un itérable et retourne toujours une nouvelle liste triée sans modifier l’itérable d’origine. Elle accepte tout type d’itérable : listes, tuples, chaînes de caractères, dictionnaires, ensembles, etc.

Syntaxe générale:

sorted(iterable, key=None, reverse=False)
  • iterable: L’objet à trier.
  • key (optionnel): Une fonction qui permet de transformer chaque élément avant de comparer. Elle est souvent utilisée pour personnaliser le tri.
  • reverse (optionnel): Si True, le tri se fait dans l’ordre décroissant.
# exemple Tri d'une liste en ordre décroissant

nombres = [3, 1, 4, 2]
resultat = sorted(nombres, reverse=True)
print(resultat)  # Affiche : [4, 3, 2, 1]

Points à retenir:

  • sorted() retourne une nouvelle liste triée et ne modifie pas l'itérable original.
  • Vous pouvez personnaliser le tri grâce à l'argument key qui permet d"appliquer une fonction de transformation sur chaque élément avant comparaison.
  • L'argument reverse=True permet de trier dans l'ordre décroissant.
  • sorted() fonctionne avec tout itérable (listes, tuples, chaînes, dictionnaires, ensembles…).

La fonction sorted() est très puissante et flexible, vous permettant de trier des données provenant de différents types d'itérables tout en offrant la possibilité de personnaliser le tri via l'argument key et d'inverser l'ordre avec reverse. Elle favorise un code clair et non destructif puisque l'itérable d'origine reste inchangé.

Les mots clés: break, continue, pass

Trois mots clés à connaître:

  • break sert à interrompre une boucle immédiatement
  • continue sert à sauter une itération, ("passer" un tour de boucle)
  • pass est un simple "placeholder", pratique quand on écrit une signature de fonction ou méthode, mais que cette dernière n'est pas encore implémentée

Les fonctions

Comment écrire une fonction? Via le mot clé def:

# exemple de fonction
def bonjour():
    print("Hello, monde !")
    return "Coucou"

# Appel de la fonction
bonjour() # affiche Hello, monde !

On peut bien entendu définir une valeur par défaut pour un argument (def saluer(nom="Inconnu"):). Et utilisé les arguments nommés, ce qui permet une flexibilité au niveau de l'ordre:

# exemple d'utilisation des arguments nommés
def infos(nom, age):
    print(f"Nom : {nom}, Âge : {age}")

infos(age=25, nom="Alice")  

Les fonctions anonymes

Une fonction lambda en Python est une fonction anonyme, c'est-à-dire une fonction sans nom, définie en une seule expression à l'aide du mot-clé lambda. Elle permet de créer rapidement des fonctions simples qui retournent le résultat d'une expression.

Syntaxe: lambda param1, param2, ..., paramN: expression

  • L'expression après : peut être un calcul, une opération, une transformation ou une condition.
  • Elle doit être unique et ne peut pas contenir plusieurs instructions (if, for, while, etc.).

Exemple avec un expression représentant une opération simple:

# Addition
addition = lambda x, y: x + y
print(addition(4, 5))  # ➝ 9

# Multiplier par 10
fois_dix = lambda x: x * 10
print(fois_dix(3))  # ➝ 30

# Mettre une chaîne en majuscules
majuscule = lambda s: s.upper()
print(majuscule("hello"))  # ➝ "HELLO"

Exemple avec un expression représentant une condition (avec opérateur ternaire x if condition else y):

# Vérifier si un nombre est pair ou impair
pair_ou_impair = lambda x: "Pair" if x % 2 == 0 else "Impair"
print(pair_ou_impair(7))  # ➝ "Impair"

# Trouver le plus grand de deux nombres
maximum = lambda a, b: a if a > b else b
print(maximum(10, 5))  # ➝ 10

# Retourner un message en fonction de l'âge
age_message = lambda age: "Adulte" if age >= 18 else "Mineur"
print(age_message(16))  # ➝ "Mineur"

Une lambda est souvent utilisée dans des fonctions comme map(), filter(), sorted(), etc.

Les fonctions variadiques

En Python, une fonction variadique est une fonction qui accepte un nombre variable d'arguments positionnels. Au lieu de déclarer explicitement un nombre fixe de paramètres, vous pouvez définir un paramètre spécial préfixé par l'astérisque (*). Ce paramètre capte tous les arguments supplémentaires passés lors de l'appel de la fonction et les regroupe dans un tuple.

L'utilisation de *args présente plusieurs avantages :

  • Flexibilité: Vous pouvez créer des fonctions qui se comportent de manière plus générique, sans avoir à connaître à l'avance le nombre d'arguments.
  • Clarté: Cela permet de centraliser la gestion d'un ensemble d'arguments et de les itérer facilement.
  • Compatibilité avec le déballage de tuples: Le même mécanisme est utilisé pour « déballer » des séquences lors d'un appel de fonction.

Définition de *args

Pour définir une fonction qui accepte un nombre variable d'arguments positionnels, on utilise la syntaxe suivante :

# Syntaxe de base
def ma_fonction(*args):
    print(args)  # args est un tuple contenant tous les arguments supplémentaires

Lorsque vous appelez cette fonction, tous les arguments passés en plus des paramètres positionnels obligatoires (s'il y en a) sont collectés dans le tuple args.

# Exemple simple
def afficher_elements(*args):
    for arg in args:
        print(arg)

afficher_elements(1, 2, 3) # Affiche 1, 2, 3

Explication du fonctionnement:
Lorsque vous appelez une fonction variadique, Python fait la répartition suivante :

  • Les arguments positionnels (s'ils sont définis) sont affectés aux paramètres définis en premier.
  • Tous les arguments supplémentaires sont regroupés dans un tuple qui est affecté au paramètre précédé de *.

# Exemple avec paramètres fixes et args :
def additionner(a, b, *args):
    total = a + b
    for nombre in args:
        total += nombre
    return total

print(additionner(10, 20))         # Pas d'arguments supplémentaires → 30
print(additionner(10, 20, 30, 40))   # 10+20+30+40 = 100

Utilisation de *args pour déballer des séquences

L"opérateur * sert également lors des appels de fonction pour "déballer" une séquence (comme une liste ou un tuple) en arguments positionnels.

def point(x, y):
    return f"({x}, {y})"

coordonnees = (3, 7)
# Sans déballage, Python pense que l'on passe un seul argument.
# point(coordonnees)  --> Erreur !
# Avec déballage :
print(point(*coordonnees))  # Affecte 3 à x et 7 à y → affiche (3, 7)

Ainsi, l'astérisque permet de convertir un tuple ou une liste en plusieurs arguments positionnels pour la fonction.

Combinaison de paramètres normaux, *args et **kwargs

En plus de *args pour les arguments positionnels, Python offre également **kwargs pour capturer des arguments nommés supplémentaires dans un dictionnaire. Même si le sujet porte sur *args, il est intéressant de savoir qu'ils peuvent être utilisés ensemble.

def fonction_mixte(a, b, *args, **kwargs):
    print(f"a = {a}, b = {b}")
    print("args =", args)
    print("kwargs =", kwargs)

fonction_mixte(1, 2, 3, 4, 5, nom="Alice", age=30)

Affiche:

a = 1, b = 2
args = (3, 4, 5)
kwargs = {'nom': 'Alice', 'age': 30}

Bonnes pratiques et points importants

Ordre des paramètres
La déclaration des paramètres d'une fonction doit respecter l'ordre suivant :

  1. Paramètres positionnels obligatoires
  2. Paramètres optionnels avec valeurs par défaut
  3. *Paramètre args (pour capturer le reste des arguments positionnels)
  4. Paramètres nommés obligatoires
  5. **Paramètre kwargs

# exemple
def exemple(a, b=10, *args, c, **kwargs):
# ici, c est un paramètre "mot-clé uniquement"
    print(a, b, args, c, kwargs)
                
# Appel correct :
exemple(1, 2, 3, 4, c=5, d=6)

Itération et accès aux éléments
Puisque args est un tuple, vous pouvez :

  • Itérer dessus dans une boucle for.
  • Accéder à un élément précis par son index (args[0], args[1], …).

Aucun argument supplémentaire n'est obligatoire

Le paramètre *args peut être vide s'il n'y a pas d'arguments supplémentaires. Cela permet donc d'appeler la fonction sans se soucier d'erreurs liées au nombre d'arguments.

def afficher_message(*args):
    if args:
        print("Messages :", args)
    else:
        print("Aucun message reçu.")

afficher_message()                 # Affiche "Aucun message reçu."
afficher_message("Bonjour", "Hi")  # Affiche le tuple de messages

Avantages d'une fonction variadique

  • Création d'interfaces flexibles: Permet de créer des fonctions ou des wrappers qui peuvent accepter des entrées diverses sans avoir à redéfinir toute l'interface
  • Facilité de transmission: Lorsqu'on veut passer directement un tuple d'arguments, il suffit de le déballer avec * dans l'appel de fonction.
  • Simplicité du code: Le code reste lisible et permet d'éviter la répétition de nombreux paramètres.

Exemples pratiques

# Exemple d'une fonction d'addition
def additionner(*nombres):
    total = sum(nombres)
    return total

print(additionner(5, 10))              # Affiche 15
print(additionner(1, 2, 3, 4, 5))      # Affiche 15
# Exemple d'une fonction qui concatène des chaînes
def concatener(*mots):
# Les arguments sont des chaînes de caractères
    return " ".join(mots)

resultat = concatener("Ceci", "est", "un", "exemple")
print(resultat)   # Affiche "Ceci est un exemple"
# Exemple avec paramètres positionnels obligatoires
def rapport(nom, *points):
    print(f"Rapport pour {nom}:")
    for point in points:
        print("-", point)

rapport("Alice", "Observation 1", "Observation 2", "Observation 3")
# sortie
Rapport pour Alice:
  - Observation 1
  - Observation 2
  - Observation 3

Conclusion

L'utilisation du paramètre *args en Python est une technique puissante pour créer des fonctions pouvant accepter un nombre variable d'arguments. Elle repose sur le concept de déballage des arguments dans un tuple, ce qui permet d'itérer sur ceux-ci et de les utiliser de manière flexible. Ce mécanisme, complété par l'usage de **kwargs pour les arguments nommés, contribue à écrire un code Python plus générique et adaptable à différentes situations.

En résumé :

  • *args permet de collecter tous les arguments positionnels supplémentaires dans un tuple.
  • On peut décomposer (déballer) des séquences lors de l'appel de fonctions grâce à l'opérateur *.
  • L'ordre de déclaration dans les paramètres est important pour éviter les erreurs.
  • Ces fonctionnalités rendent votre code plus souple et évitent de devoir redéfinir des fonctions pour différents nombres d'arguments.

Les fonctions partielles

Le principe

En programmation fonctionnelle, la partial application consiste à fixer partiellement certains arguments d'une fonction pour créer une nouvelle fonction ayant une signature réduite. En Python, le module functools fournit la fonction partial qui permet de "prédéfinir" certains paramètres d'une fonction de sorte que, lors de l'appel ultérieur, vous n'ayez plus qu'à fournir les arguments restants.

Cela offre plusieurs avantages :

  • Simplification du code: Vous pouvez créer des versions spécialisées de fonctions générales.
  • Réutilisabilité: Vous évitez de répéter les mêmes paramètres à chaque appel.
  • Intention déclarée: Un nom explicite pour la fonction partielle peut améliorer la lisibilité du code.

La fonction functools.partial

La signature est la suivante : functools.partial(func, /, *args, **kwargs)

  • func: La fonction de base à laquelle vous souhaitez fixer certains arguments.
  • *args: Les arguments positionnels à fixer (les « préfixes » des arguments).
  • **kwargs: Les arguments nommés à fixer.

Lorsque la fonction partielle est appelée, les arguments fournis lors de l'appel sont ajoutés (concaténés) aux arguments déjà fixés. Ainsi, l'appel effectif est équivalent à:
func(*args, *new_args, **{**kwargs, **new_kwargs})

Les objets partiels créés possèdent trois attributs en lecture seule :

  • func: la fonction originale.
  • args: les arguments positionnels pré-fixés.
  • keywords: le dictionnaire des arguments nommés pré-fixés.
Ces attributs permettent également l'introspection sur l'objet partiel.

Exemples pratiques

Supposons que nous ayons une fonction multiply qui multiplie deux nombres. Nous allons créer une fonction partielle qui fixe le premier facteur à 2 pour obtenir une fonction qui double un nombre.

from functools import partial

def multiply(x, y):
    return x * y

# Crée une fonction partielle qui fixe x à 2
double = partial(multiply, 2)
print(double(4))  # Affiche 8

Ici, l'appel double(4) revient à appeler multiply(2, 4).

Exemple 1 : Une fonction de multiplication

Supposons que nous ayons une fonction multiply qui multiplie deux nombres. Nous allons créer une fonction partielle qui fixe le premier facteur à 2 pour obtenir une fonction qui double un nombre.

from functools import partial

def multiply(x, y):
    return x * y

# Crée une fonction partielle qui fixe x à 2
double = partial(multiply, 2)
print(double(4))  # Affiche 8

Ici, l'appel double(4) revient à appeler multiply(2, 4).

Exemple 2 : Manipulation d'arguments positionnels et nommés

Considérons une fonction avec plusieurs paramètres et des arguments nommés :

from functools import partial

def func(u, v, w, x):
    return u * 4 + v * 3 + w * 2 + x

# Création d'une fonction partielle fixant les trois premiers paramètres
p = partial(func, 5, 6, 7)
print(p(8))  # Calcule 5*4 + 6*3 + 7*2 + 8 = 60

Vous voyez ici que les arguments 5, 6, 7 sont « enrobés » dans la fonction partielle. L'appel de p(8) fournit la valeur pour le dernier paramètre.

Exemple 3 : Utilisation avancée des attributs d'un objet partial

Nous pouvons examiner les attributs de l'objet partiel:

from functools import partial

def add(a, b):
    return a + b

add_five = partial(add, 5)
print(add_five.func)      # Affiche la fonction 'add'
print(add_five.args)      # Affiche (5,)
print(add_five.keywords)  # Affiche {} (vide dans ce cas)

Ces attributs permettent de connaître quels arguments ont été "baked" lors de la création de l'objet partiel.

Exemple 4 : Fonctions partielles et objets mutables

Une chose importante à noter est que les arguments passés à partial sont « baked in » au moment de la création. Si vous passez un objet mutable, la référence est conservée:

from functools import partial

def multiply(item, multiplier):
    return item * multiplier

liste = ["a"]
p_multiply = partial(multiply, liste, 2)
print(p_multiply())  # ['a', 'a']

# Même si on réaffecte la variable 'liste', le partial conserve la référence initiale :
liste = ["a", "b"]
print(p_multiply())  # Toujours ['a', 'a']

# En revanche, si on modifie l'objet mutable, le changement sera reflété
liste = ["a"]
p_multiply = partial(multiply, liste, 2)
print(p_multiply())  # ['a', 'a']
liste.append("b")
print(p_multiply())  # ['a', 'b', 'a', 'b']

Cela montre que, pour un objet mutable, on ne prend pas une copie, mais la référence (source: devcuriosity.com - python-partial-function).

Exemple 5 : (POO) Utiliser partial avec des méthodes - le cas de partialmethod

Lorsque vous souhaitez fixer des arguments pour des méthodes de classe, il faut prendre en compte que la première variable (habituellement self) est automatiquement passée. Un simple usage de partial ne fonctionnera pas comme attendu. Pour cela, Python offre partialmethod.

from functools import partialmethod

class Article:
    def set_platform(self, platform):
        self.platform = platform
            
    # Création d'une méthode partielle qui fixe platform à "substack"
    set_substack = partialmethod(set_platform, "substack")
            
article = Article()
article.set_substack()
print(article.platform)  # Affiche 'substack'

Ici, partialmethod gère correctement le passage du self et fixe l'argument platform.

Quand et pourquoi utiliser les fonctions partielles

Les fonctions partielles sont particulièrement utiles dans les situations suivantes.

Création de callbacks

Par exemple, pour des interfaces graphiques (Tkinter, PyQt) ou d'autres frameworks où vous devez fournir une fonction à appeler lors d'un événement et que certains paramètres restent constants.

Exemple : fixer la valeur d'un paramètre lors de l'appel d'une fonction de gestion d'événement avec partial.

Spécialisation de fonctions générales

Lorsque vous avez une fonction générique à laquelle vous appliquez fréquemment certains arguments constants (par exemple, la conversion d'une chaîne en entier avec une base fixe, voir l'exemple avec int et base=2).

from functools import partial
from math import prod  # ou une fonction multiply personnalisée

# Créer une fonction qui élève toujours un nombre au carré
square = partial(lambda x, exponent: x ** exponent, exponent=2)
Simplification de chaînes de fonctions (pipeline)

Dans des cas où le résultat d'une fonction doit être transformé avant d'être passé à une autre, la fonction partielle peut fixer des paramètres dans des fonctions intermédiaires.


Les fonctions partielles représentent un puissant outil de programmation fonctionnelle en Python. Grâce à functools.partial et partialmethod vous pouvez "figer" une partie des arguments d'une fonction ou méthode pour obtenir de nouvelles fonctions plus spécifiques et simples à utiliser. Cela conduit à du code plus lisible et à la réduction de la répétition de valeurs constantes.

Les fonctions privées

En Python, tout est accessible. Il n'existe pas de mot-clé comme private ou protected que l'on retrouve dans d'autres langages (par exemple, Php ou C++).
Les développeurs se fient aux conventions de nommage pour signaler les intentions d'usage du code.

Placer un underscore (_) devant le nom d'une fonction ou d'une variable indique qu'elle est destinée à être « privée » ou pour un usage interne uniquement.

Cette convention est largement reconnue et respectée par la communauté, mais elle n'empêche pas l'accès direct à la fonction depuis l'extérieur.

Dans une classe, on utilisera le __ (double underscore), on parle de name mangling. La POO n'est pas détaillée ici.

Dans un module, en plus de _, on peut utiliser __all__ pour ne pas exporter certaines fonctions

Les Docstrings

Il est important de bien documenté son code. Voici, un exemple en Python:

def addition(a, b):
    """
    Cette fonction retourne la somme de deux nombres.
                
    Paramètres:
    a (int, float) : Premier nombre
    b (int, float) : Deuxième nombre
            
    Retourne:
    int, float : La somme de a et b
    """          
    return a + b            

Remarque: la documentation est ainsi accessible via: print(addition.__doc__) ou help(addition).