-
- 1.1. Introduction
- 1.2. Définition d'une fonction Python
- 1.3. Typage dynamique des paramètres
- 1.4. Passage d'arguments
- 1.4.1. Arguments positionnels
- 1.4.2. Arguments nommés
- 1.4.3. Arguments optionnels
- 1.4.4. Arguments arbitraires
- 1.5. Fonctions imbriquées
- 1.5.1. Exemple 1 - Nombres premiers
- 1.5.2. Exemple 2 - Hauteur d'eau
- 1.5.3. La fonction usine (factory)
- 1.6. Fonction anonyme (lambda)
- 1.6.1. Définition
- 1.6.2. Assignation à une variable
- 1.6.3. Applications courantes
- 1.6.3.1. Utilisation avec map()
- 1.6.3.2. Utilisation avec sorted()
- 1.6.3.3. Utilisation avec filter()
- 1.7. Portée des variables
- 1.7.1. Exemple de variable globale
- 1.7.2. Exemple de variable non locale
- 1.8. Fonctions récursives
- 1.8.1. Récursivité directe
- 1.8.2. Récursivité indirecte
- 1.9. Mémoïsation
- 1.10. Exercices - TP N°4
- 1.10.1. Exercice 1
- 1.10.2. Exercice 2
1.1. Introduction¶
Au chapitres précédents, nous avons vu les structures de base et de contrôle qui constituent le squelette d'un programme Python. Nous abordons maintenant les structures d'organisation du code, qui transforment un script linéaire en un système modulaire et réutilisable.
Les fonctions fond partie des structures d'organisation de code, elles sont très importantes en programmation. Elles permettent d'encapsuler des blocs de code réutilisables, de donner du sens aux opérations par un nom explicite, et de réduire la redondance. Elles constituent également la base de la modularité et facilitent la programmation orientée objet et l'usage de modules en Python.
Une fonction Python est un concept similaire à la fonction mathématique. C'est un bloc de code nommé qui reçoit en entrée des arguments et qui produit en sortie un résultat.
Cette section couvre successivement :
- Les fonctions de base (définition, valeur de retour, fonctions anonymes)
- Les modules pour structurer le code à grande échelle (importations)
- Les fonctions avancées (générateurs, décorateurs)
- Les espaces de noms qui régissent la visibilité des variables (global, nonlocal)
Ces concepts sont la base de la modularité en Python et préparent à la programmation orientée objet.
1.2. Définition d'une fonction Python¶
En Python, on définit une fonction à l'aide du mot-clé def suivi d'un nom de la fonction, puis de parenthèses contenant une suite de paramètres (qui peut être vide), et enfin d'un deux-points (:).
Le bloc d'instructions qui constitue le corps de la fonction commence à la ligne suivante et doit être indenté.
La forme générale est:
def nom_de_la_fonction(arg1, arg2, ..., argn):
"""Docstring optionnelle décrivant la fonction."""
# bloc d'énoncés indentés
return expression # optionnel
Les paramètres (arg1, arg2, etc.) sont séparés par des virgules.
Le mot-clé return permet à la fonction de retourner la valeur d'une expression ou d'un résultat d'un calcul.
Si return est absent, la fonction retourne automatiquement l'objet spécial None, qui représente l'absence de résultat.
La chaîne de caractères """ Docstring ...""" placée juste après la définition de la fonction, entre triples guillemets, est appelée une Docstring ou une chaîne de documentation. Elle permet de documenter la fonction, le texte contenu est :
- Affiché par la fonction
help(ma_fonction) - Utilisé par les éditeurs de code (VS Code, PyCharm, IDLE) pour l'auto-complétion
- Accessible via la commande
ma_fonction.__doc__
Une docstring brève et complète est une bonne pratique. Elle décrit ce que fait la fonction, ses paramètres, son retour et les erreurs potentielles.
Remarque
Dans l'usage courant, les termes « paramètre » et « argument » sont parfois employés comme synonymes, bien qu'ils aient une signification précise.
- Un paramètre est une variable placée dans la définition d'une fonction.
- Un argument est la valeur effectivement passée à la fonction lors de son appel.
Dans les sous sections suivantes on donne quelques exemples simples qui montrent des définitions de fonctions avec différents arguments et retours.
1.2.1. Fonction à un seul paramètre¶
Voici un exemple de fonction simple avec un paramètre en entrée et un résultat en sortie :
# Exemple 1
# Fonction simple avec un paramètre et un retour
def cube(x):
""" retoune x^3 v1.0"""
return x ** 3
c2 = cube(2)
c3 = cube(3)
print('2^3 =', c2)
print('3^3 =', c3)
help(cube)
2^3 = 8
3^3 = 27
Help on function cube in module __main__:
cube(x)
retoune x^3 v1.0
Dans l'exemple précédent, on a défini une fonction nommée cube qui prend en argument un paramètre x et retourne le résultat de l'opération x ** 3 (x à la puissance 3)
Dans le premier appel avec l'expression c2 = cube(2) :
- x vaut 2 pendant l'exécution de la fonction
- la fonction retourne 8 (2 ** 3)
- c2 reçoit la valeur 8
Dans le premier appel avec l'expression c3 = cube(3) :
- x vaut 3 pendant l'exécution de la fonction
- la fonction retourne 27 (3 ** 3)
- c3 reçoit la valeur 7
Dans l'affiche les résultats
- Le 1er print montre que c2 contient bien 8 :
print('2^3 =', c2)Affiche :2^3 = 8 - Le 2em print montre que c3 contient bien 27 :
print('3^3 =', c3)Affiche :3^3 = 27
On peut ajouter une docsting à la fonction précédente pour la documenter:
# Exemple de fonction simple
def cube(x):
"""
calcule x^3
x : entier, réel ou complexe
"""
return x ** 3
help(cube)
Help on function cube in module __main__:
cube(x)
calcule x^3
x : entier, réel ou complexe
1.2.2. Fonction à plusieurs paramètres¶
Voici un autre exemple d'une fonction de x qui retourne la valeur d'un polynôme du 2nd degré :
# Exemple 2
# on défit une fonction de type polynôme : f(x) = 5*x^2 + 2*x - 1
# puis on affiche quelques résultats de calcul avec f(x)
def f(x):
return 5 * x**2 + 2*x - 1
print('f(0) =', f(0))
print('f(0.5) =', f(0.5))
print('f(1) =', f(1))
f(0) = -1 f(0.5) = 1.25 f(1) = 6
Dans l'exemple précédent $$f(x) = 5 x^2 + 2 x - 1 $$ les coefficients du polynôme sont constants et fixés à (5,2,-1).
Si on veux généraliser à tout polynôme du second degré :
$$ {p_2}(x) = a x^2 + b x + c $$
les coefficients (a, b, c) doivent êtres des paramètres dans la définition de la fonction.
Dans ce cas, on doit donc définir une fonction avec 4 paramètres (x, a, b, et c), c'est l'équivalent d'une fonction à plusieurs variables en mathématiques.
# Exemple 3
# On généralise la fonction précédente en prenant
# les coefficients a,b,c comme paramètres
def p2(x, a, b , c):
return a * x**2 + b * x + c
# équivalent à f(x) de l'exemple 2
print('résultat :', p2(1.2, 5, 2,-1))
# autre polynôme (on change les paramètres)
# on peut stocker le calcul dans une variable y
y = p2(0.5, 4, 7 ,-2)
print(f'{y = }')
résultat : 8.6 y = 2.5
Voici d'autres exemples d'une fonction à deux paramètres
# Exemple 4
# Le symbole de Kronecker est un bon exemple
# d'une fonction à deux paramètres
# def Kronecker_delta(i, j):
# """ Retourne δij (1 si i=j, 0 sinon). """
# return int(i == j)
def Kronecker_delta(i, j):
return 1 if i == j else 0
print(Kronecker_delta(1,1), Kronecker_delta(100,100))
1 1
# on utilise la fonction précédente pout calculer
# le produit delta_ij A_ij = trace(A)
A = [[1, 3, 4],
[2, 5, 6],
[0, 2 ,9]]
print(A)
s = 0
for i in range(3):
for j in range(3):
s += Kronecker_delta(i,j) * A[i][j]
print('detal_ij A_ij = ', s)
[[1, 3, 4], [2, 5, 6], [0, 2, 9]] detal_ij A_ij = 15
Une fonction sans paramètres qui retourne une résultat de type str
# Exemple 5
# Fonction sans paramètres
def menu_options():
while True:
print("\n---- Choisir une option ----")
print("1. Ajouter un élément")
print("2. Supprimer un élément")
print("0. Quitter")
choix = input("Votre choix : ")
if choix in ["0", "1", "2"]:
return choix
else:
print('Choix incorrecte')
# Utilisation
option = menu_options()
print(f"Vous avez choisi l'option {option}")
print(type(option))
---- Choisir une option ---- 1. Ajouter un élément 2. Supprimer un élément 0. Quitter Vous avez choisi l'option 1 <class 'str'>
1.2.3. Fonction avec un retour de plusieurs valeurs¶
Il es possible de retourner plusieurs valeurs à partir d'une fonction en faisant suivre return par la liste des résultats à retourner séparés par des virgules.
Dans l'exemple suivant on cherche les deux racines réelles d'une équation du second degré.
Si le discriminant est nul ou positif on retourne les deux racines, sinon (négatif) on retourne None (rien).
# Solutions dans R d'une équation du 2nd degré.
def sol_eq_2deg(a,b,c):
d = b**2 - 4*a*c
if d < 0:
print("pas de solutions réelles")
return None
x1 = (-b + d**0.5)/(2*a)
x2 = (-b - d**0.5)/(2*a)
return x1,x2
print('solution :', sol_eq_2deg(1,4,4))
#sol1, sol2 = sol_eq_2deg(1,-8,15)
sol1, sol2 = sol_eq_2deg(1,4,4)
print(f'Les solutions sont : {sol1} et {sol2}')
solution : (-2.0, -2.0) Les solutions sont : -2.0 et -2.0
Dans return x1,x2 de la fonction sol_eq_2deg on a mit les deux solutions séparée par un virgule.
Le 1er affichage montre que la fonction renvoie un seul résultat qui est par défaut un tuple de deux éléments.
Le 2ème affichage montre qu'on peut déballer (unpacking) le résultat renvoyé par la fonction comme n'import quel tuple avec la commande sol1, sol2 = sol_eq_2deg(1,-8,15)
Remarques
Noter le
returndans le bloc deif, ce qui rendelsepas nécessaire.On peut modifier le comportement pas défaut de
return x1,x2qui produit un tuple, en précisant le type du résultat explicitement. Par exemplesreturn [x1, x2]retourne les deux solutions sous forme d'une listereturn {x1, x2}retourne les deux solutions sous forme d'un ensemblereturn {'sol1' : x1, 'sol2' : x2}retourne les deux solutions sous forme d'un dictionnaire
Dans tous les cas, on pourra déballer les résultats après appel.
Dans le cas d'une solution double, une retour sous forme d'un ensemble convient mieux car il ne retourne qu'une seule solution.Il faut faire attention lors du déballage des deux solutions,tester les cas suivants :
# Affichage valable pour un return set ou list sol1, sol2 = sol_eq_2deg(1,-8,15) print(f'Les solutions sont : {sol1 = } et {sol2 = }') # Attention ! Lève une erreur pour set en cas de solution double sol1, sol2 = sol_eq_2deg(1, -4, 4) print(f'Les solutions sont : {sol1 = } et {sol2 = }') # Valable pour le de dict seulement (fonction values) #sol1, sol2 = sol_eq_2deg(1, -4, 4).values() #print(f'Les solutions sont : {sol1 = } et {sol2 = }')
# retour de résultat sous forme d'une collection
def sol_eq_2deg(a,b,c):
if a == 0:
return (-c/b)
d = b**2 - 4*a*c
if d < 0:
print("pas de solutions réelles")
return None
x1 = (-b + d**0.5)/(2*a)
x2 = (-b - d**0.5)/(2*a)
# activer l'un des retours suivant
#return x1, x2 # retour par défaut
#return [x1,x2] # retour en liste
return {x1,x2} # retour en set
#return {'sol1' : x1, 'sol2' : x2} # en dictionnaire
# Affichage valable pour les 3 cas de return
# même avec solution double (x1=x2)
print('solution :', sol_eq_2deg(1,-8,15))
print('solution :', sol_eq_2deg(1,-4,4))
print('solution :', sol_eq_2deg(0,1,-1))
solution : {3.0, 5.0}
solution : {2.0}
solution : 1.0
1.2.4. Fonction sans retour de valeur¶
Il existe des cas où on définit une fonction qui n'est destinée qu'à réaliser quelques tâches spécifiques, sans avoir besoin d'un retour de résultats. Une telle fonction elle revoie quand-même None même s'il elle ne contint pas de commande return
Exemple : Si on reprend le cas de l'exemple 5 et on veut séparer les prints du traitement, on pourra créer une fonction affiche_menu qui ne prendra aucun argument comme suit:
# Fonction affichage de menu
def affiche_menu():
print("\n---- Choisir une option ----")
print("1. Ajouter un élément")
print("2. Supprimer un élément")
print("0. Quitter")
#Utilisation normale de la fonction:
affiche_menu()
---- Choisir une option ---- 1. Ajouter un élément 2. Supprimer un élément 0. Quitter
Si on cherche ce que la fonction précédente renvoie comme résultat, il faut l'affecter à une variable.
Avec une commande de type : res = affiche_menu(), la fonction va executer les prints (affiche le menu) puis retourne None qui sera affiché si on fait print(f'{res = }')
res = affiche_menu()
print(f'{res = }')
---- Choisir une option ---- 1. Ajouter un élément 2. Supprimer un élément 0. Quitter res = None
1.3. Typage dynamique des paramètres¶
Une particularité des fonctions en Python est qu'elles ne requièrent pas de déclarer les types des paramètres. Tant que les opérations sur les arguments fournis sont valides, l'interpréteur ne soulève pas d'erreur. Python est en effet connu comme étant un langage au « typage dynamique » (duck typing), c'est-à-dire qu'il reconnaît le type des variables au moment de l'exécution.
Par exemple :
print(f(2), type(f(2))) # ici l'argument 2 est un int
print(f(2.0), type(f(2.0))) # ici l'argument 2.0 est un float
affichera :
23 <class 'int'>
23.0 <class 'float'>
Il faut tout de même se méfier de cette grande flexibilité qui pourrait conduire à des surprises dans les résultats retournés par les fonctions. Il convient donc de prévoir les tests nécessaires au bon déroulement des opérations sur les arguments en entrée.
L'exemple suivant illustre une possible situation de confusion :
# Exemple de fonction qui multiplie deux nombres
# ATTENTION :
# Le comportement change selon le type des arguments en entrée.
# Le résultat peut être un nombres, chaînes ou liste.
def mult(a, b):
if not isinstance(b,int):
raise ValueError("b n'est pas entier")
return a * b
# float et int comme arguments, retourne un float
res = mult(1.2, 4 )
print(f'{res = } : {type(res)}')
# str et int comme argument : retourne un str
res = mult('1.2', 4)
print(f'{res = } : {type(res)}')
# list et int comme argument : retourne une liste dupliquée
res = mult([0,1,2], 4)
print(f'{res = } : {type(res)}')
res = 4.8 : <class 'float'> res = '1.21.21.21.2' : <class 'str'> res = [0, 1, 2, 0, 1, 2, 0, 1, 2, 0, 1, 2] : <class 'list'>
On voit à travers cet exemple que la fonction mult(a, b) peut être conçue au debut pour prendre deux arguments de type numérique et retourner le résultat de leur multiplication.
Cependant, les différents cas d'utilisation montrent qu'elle accepte aussi d'autres types d'arguments et réalise l'opération * sans lever d'erreur.
L'exemple illustre bien la flexibilité du typage dynamique : la fonction mult() acceptera des argument de n'importe quel type supportant l'opérateur *.
Cependant, le type du résultat peut être inattendu car * change de sens selon les types (multiplication pour le type numérique , répétition pour le type str, duplication pour les list, tuple, ...)
Si l'on souhaite garantir un résultat numérique, la fonction ne doit accepter que des arguments de type numérique, sinon on risque un bug silencieux (pas d'erreur, mais un résultat faux).
Cette situation doit être gérée à l'aide d'une structure conditionnelle (if) sur les arguments d'entrée.
Par exemple :
# fonction mult avec contrôle des types des arguments
def mult(a, b):
#arg_valid = all(isinstance(x, (int, float,complex)) for x in (a, b))
arg_valid = isinstance(a, (int, float,complex)) and isinstance(b, (int, float,complex))
if not arg_valid :
raise TypeError("Les arguments doivent être des nombres (int ou float)")
#print('Les arguments doivent être des nombres (int ou float)')
return
return a * b
#res = mult('2.5', 4) # dé-commenter pour tester un cas d'erreur
#res = mult(2.5+2J, 4+2J) # accepté par la fonction
print(f'{res = } : {type(res)}')
res = [0, 1, 2, 0, 1, 2, 0, 1, 2, 0, 1, 2] : <class 'list'>
1.4. Passage d'arguments¶
Comme on vient de voir à partir des exemples précédents, le nombre de paramètres que l'on peut définir dans une fonction n'est pas fixe; nous avons défini les fonctions :
cube(x)etf(x)avec un paramètrep2(x, a, b, c)avec 4 paramètresKronecker_delta(i, j)avec 2 paramètresmenu_options()sans aucun paramètre
Lors des appels, chacune des fonctions précédentes est appelée avec le même nombre d'arguments que le nombre de ses paramètres et dans le même ordre. Cependant, python offre trois mécanismes principaux de la définition des fonctions :
- Arguments positionnels : Arguments fournis dans l'ordre des paramètres
- Arguments nommés : On associe explicitement un nom à chaque argument, ce qui permet de changer l'ordre.
- Arguments optionnels : On peut fournir une valeur par défaut à un paramètre, il devient optionnel.
- Nombre variable d'arguments : On peut définir une fonction qui accepte un nombre arbitraire d’arguments.
1.4.1. Arguments positionnels¶
Dans les exemples précédents, nous avons fait appel à des fonctions en leur fournissant des arguments dans le même ordre que leurs paramètres.
Lorsqu'on appelle une fonction de cette manière, chaque valeur est associée à un paramètre en fonction de sa position dans la liste fournie : on parle alors d'arguments positionnels.
Reprenons l'exemple :
def sol_eq_2deg(a, b, c):
...
sol_eq_2deg(1, -8, 15)
Les paramètres reçoivent les valeurs dans cet ordre : a = 1, b = -8, c = 15.
Ainsi, l'ordre des arguments dans l'appel doit correspondre strictement à l'ordre des paramètres tels qu'ils ont été définis.
Si l'ordre est modifié, l'association des valeurs change, ce qui peut conduire à un résultat incorrect, voire à une erreur.
Cependant, il ne faut pas en déduire que la fonction elle-même impose l'usage d'arguments positionnels. En Python, c'est l'appel qui détermine le mode utilisé.
1.4.2. Arguments nommés¶
La même fonction précédente peut tout aussi bien être appelée avec des arguments nommés, ce qui rend l'ordre sans importance ; Dans ce cas, les valeurs sont associées aux paramètres grâce à leur nom, et non à leur position.
Les appels suivants sont tous équivalents et fournissent le même résultat
print(sol_eq_2deg(a = 1 , b = -8, c = 15))
print(sol_eq_2deg(a = 1 , c = 15, b =-8 ))
print(sol_eq_2deg(b = -8, c = 15, a = 1 ))
{3.0, 5.0}
{3.0, 5.0}
{3.0, 5.0}
Avec les arguments nommés, il suffit de préciser explicitement le nom de chaque paramètre.
Remarque Il est possible de mélanger entre les deux modes d'appels avec arguments positionnels et arguments nommés. La seule règle à observer c'est qu'il faut commencer par les arguments positionnels avant les arguments nommés.
Les commandes suivantes sont équivalentes :
print(sol_eq_2deg(1, b=-8, c=15))
print(sol_eq_2deg(1, c=15, b=-8))
print(sol_eq_2deg(1, -8, c=15))
#print(sol_eq_2deg(a=1, -8, c=15)) # erreur
{3.0, 5.0}
{3.0, 5.0}
{3.0, 5.0}
1.4.3. Arguments optionnels¶
Lors de la définition d'une fonction, certains paramètres peuvent avoir des valeurs par défaut. Ces paramètres sont dits optionnels.
Un paramètre optionnel se déclare lors de la définition d'une fonction. Par exemple on si on défini la fonction précédente comme suit:
def sol_eq_2deg(a, b, c=15):
...
alors si aucun argument n'est fourni pour le paramètre optionnel c, il prend la valeur par défaut 15
# argument par défaut
def sol_eq_2deg(a, b, c=15):
d = b**2 - 4*a*c
if d < 0:
print("pas de solutions réelles")
return None
x1 = (-b + d**0.5)/(2*a)
x2 = (-b - d**0.5)/(2*a)
return x1, x2
print(sol_eq_2deg(1, -8) ) # c = 15 par défaut
print(sol_eq_2deg(a=1 , b=-8) ) # c = 15 par défaut
print(sol_eq_2deg(a=1, c=15, b=-8)) # c = 15 par défaut
print(sol_eq_2deg(1 , -8 , 12 ) ) # c = 12
print(sol_eq_2deg(1 , -8 , c=12 )) # c = 12
print(sol_eq_2deg(1, c=12, b=-8) ) # c = 12
(5.0, 3.0) (5.0, 3.0) (5.0, 3.0) (6.0, 2.0) (6.0, 2.0) (6.0, 2.0)
Lors de la définition d'une fonction avec arguments par défaut, il faut placer paramètres positionnels en premiers. Par exemple
def sol_eq_2deg(a=1, b, c):
. . .
provoquera une erreur de syntax : e
Cell In[58], line 2
def sol_eq_2deg(a=1, b, c):
^
SyntaxError: parameter without a default follows parameter with a default
L'exemple suivant illustre le cas d'une fonction avec deux arguments optionnels.
Il s'agit de définir une fonction qui permet de calculer le nombre de Froude $F_r = V^2 /(2g) $ et de l'arrondir à 1.0 avec une tolérance fixée par défaut à 1e-3.
Le paramètre g étant la gravité, il sera aussi pris par défaut g = 9.81.
L'exemple montre différentes façons d'appeler la fonction.
# Exemple de fonction avec deux arguments par défaut
def FroudeNumber(V, h, g=9.81, tol=1e-3):
Fr = V/(g*h)**0.5
return 1.0 if abs(Fr-1) <= tol else Fr
#if abs(Fr-1) <= tol : Fr = 1.0 # arrondi à 1.0
#return Fr
print('g=9.81, tol = 0.001 par défaut, V et h positionnels')
print('Fr(2.18 m/s, 0.48 m) :', FroudeNumber(2.18 ,0.48 ) )
print('Fr(1.918 m/s, 0.375 m) :', FroudeNumber(1.918,0.375) )
print('\ng=9.81 par défaut, tol = 0.01, V et h positionnels')
print('Fr(2.18 m/s, 0.48 m) :', FroudeNumber(2.18 ,0.48 , tol=0.01))
print('Fr(1.918 m/s, 0.375 m) :', FroudeNumber(1.918,0.375, tol=0.01))
print('\ntol = 0.01 par défaut, g = 10, V et h positionnels')
print('Fr(2.18 m/s, 0.48 m) :', FroudeNumber(2.18 ,0.48 , g = 10))
print('Fr(1.918 m/s, 0.375 m) :', FroudeNumber(1.918,0.375, g = 10))
print('\ng=9.81, tol = 0.001 par défaut, V et h nommés')
print('Fr(2.18 m/s, 0.48 m) :', FroudeNumber(h=0.48 , V=2.18 ))
print('Fr(1.918 m/s, 0.375 m) :', FroudeNumber(V=1.918, h=0.375))
g=9.81, tol = 0.001 par défaut, V et h positionnels Fr(2.18 m/s, 0.48 m) : 1.0046189622236181 Fr(1.918 m/s, 0.375 m) : 1.0 g=9.81 par défaut, tol = 0.01, V et h positionnels Fr(2.18 m/s, 0.48 m) : 1.0 Fr(1.918 m/s, 0.375 m) : 1.0 tol = 0.01 par défaut, g = 10, V et h positionnels Fr(2.18 m/s, 0.48 m) : 0.9950293128010519 Fr(1.918 m/s, 0.375 m) : 0.99045094107011 g=9.81, tol = 0.001 par défaut, V et h nommés Fr(2.18 m/s, 0.48 m) : 1.0046189622236181 Fr(1.918 m/s, 0.375 m) : 1.0
Remarque :
Il est possible de définir la fonction FroudeNumber avec d'autres versions, en voici deux :
# version 1. inversion de condition dans l'expression retournée
def FroudeNumber(V, h, g=9.81, tol=1e-3):
Fr = V/(g*h)**0.5
return Fr if abs(Fr-1) > tol else 1.0
print('Fr(2.18 m/s, 0.48 m) :', FroudeNumber(2.18 ,0.48 ) )
print('Fr(1.918 m/s, 0.375 m) :', FroudeNumber(1.918,0.375) )
Fr(2.18 m/s, 0.48 m) : 1.0046189622236181 Fr(1.918 m/s, 0.375 m) : 1.0
# version 2. test if de l'arrondi à part
def FroudeNumber(V, h, g=9.81, tol=1e-3):
Fr = V/(g*h)**0.5
if abs(Fr-1) <= tol : Fr = 1.0 # arrondi à 1.0
return Fr
print('Fr(2.18 m/s, 0.48 m) :', FroudeNumber(2.18 ,0.48 ) )
print('Fr(1.918 m/s, 0.375 m) :', FroudeNumber(1.918,0.375) )
Fr(2.18 m/s, 0.48 m) : 1.0046189622236181 Fr(1.918 m/s, 0.375 m) : 1.0
1.4.4. Arguments arbitraires¶
La flexibilité de Python dans la définition et l'appel des fonctions va au-delà des arguments positionnels, nommés ou optionnels.
Grâce aux opérateurs de paquetage * et **, il est possible de créer des fonctions capables de recevoir un nombre arbitraire d'arguments.
Ce mécanisme est particulièrement utile lorsque l'on ne connaît pas à l'avance la quantité d'informations que l'utilisateur devra fournir à la fonction.
Syntaxe générale
def fonction(*args, **kwargs): ... # corps de la fonction
*argsregroupe tous les arguments positionnels dans untuple.**kwargsregroupe tous les arguments nommés dans undict.
Ces deux mécanismes peuvent être utilisés séparément ou ensemble, selon les besoins.
1.4.4.1. Exemple d'arguments positionnels arbitraires¶
Le code suivant définit une fonction moyenne(*args) qui calcule la moyenne d'un nombre indéfini de valeurs numériques.
Les valeurs passées à moyenne() sont collectées dans le tuple args, ce qui permet d'additionner autant de nombres que nécessaire.
# Exemple 1 : arguments positionnels arbitraires
# Utilisation de `*args`
def moyenne(*args):
total = 0
for valeur in args:
total += valeur
return total/len(args)
print(moyenne(3, 5))
print(moyenne(1, 2, 3, 4, 5))
4.0 3.0
Remarques:
Il est possible d'utiliser directement une somme sur un générateur dans le corps de la fonction
moyenne(*args):def moyenne(*args): total = sum(valeur for valeur in args) return total / len(args)
ou même une version encore plus compacte :
def moyenne(*args): return sum(args) / len(args)
Encore une autre version avec un test si aucun argument n'est fourni, on retourne
None.def moyenne(*args): return sum(args) /len(args) if args else None print(moyenne())
Si l'on souhaite calculer la moyenne à partir d'un objet itérable (liste, range, générateur…), il faut d'abord le dépaqueter avant de le passer à la fonction.
Voici quelques exemples :
def moyenne(*args):
#return None if not args else sum(args) /len(args)
return sum(args) /len(args) if args else None
print(moyenne())
print(moyenne(*range(1,6))) # un range
print(moyenne(*[1, 4, 9, 16, 25])) # une liste
print(moyenne(*(i*i for i in range(1,6)))) # un générateur
print(moyenne(), moyenne(*[]))
None 3.0 11.0 11.0 None None
Lorsque l'opérateur * est appliqué à un argument de type objet itérable, ile permet de le dépaqueter en plusieurs variables. Cependant; lorsqu'il est appliqué à un paramètre d'une fonction, il permet de l'empaqueter en un tuple.
1.4.4.2. Exemples d'arguments nommés arbitraires¶
Exemple 1¶
On reprend l'exemple précédent du calcul du nombre de Froude. Ici, g et tol seront fournis via des arguments nommés arbitraires, afin d'illustrer l'usage de **kwargs.
# FroudeNumber version **kwargs
# kwargs va contenir g et tol
def FroudeNumber(V, h, **kwargs):
if h == 0:
return "Erreur : h ne peut pas être nul."
g = kwargs.get('g',9.81) # par défaut 9.81 m/s^2
tol = kwargs.get('tol',0.001) # par défaut 0.001
Fr = V/(g*h)**0.5
return Fr if abs(Fr-1) > tol else 1.0
print(FroudeNumber(2.18 ,0.48))
print(FroudeNumber(2.18 ,0.48, g = 10) )
print(FroudeNumber(2.18 ,0.48, tol = 0.005, g = 10) )
print(FroudeNumber(2.18 ,0.48, tol = 0.005) )
1.0046189622236181 0.9950293128010519 1.0 1.0
Comparativement aux versions précédentes de FroudeNumber utilisant des arguments optionnels, la version basée sur kwargs peut recevoir n'importe quel argument nommé, pas seulement g et tol.
Par exemple :
FroudeNumber(2.18, 0.48, tol=0.005, troisieme_param=..., quatrieme_param=...)
Les arguments supplémentaires ne provoquent aucune erreur, même s'ils ne sont pas utilisés dans la fonction. Ils seront simplement ignorés.
C'est un comportement qui constitue à la fois un avantage et un inconvénient :
Avantage : la fonction devient plus flexible et tolérante à des options inconnues. Elle continue de fonctionner même si des versions plus anciennes du code appellent la fonction avec des paramètres qui n'existent plus.
Inconvénient : une faute de frappe ou un argument mal nommé passe inaperçu, ce qui peut masquer une erreur de l'utilisateur.
Un autre bénéfice de kwargs est qu'il permet de faire évoluer la fonction sans casser l'ancien code.
Supposons que FroudeNumber admettait dans une version initiale un argument optionnel g.
Si, dans une nouvelle version, on retire g des paramètres et on fixe sa valeur dans la fonction, alors un appel ancien comme :
FroudeNumber(2.18, 0.48, g=10)
provoquerait une erreur (argument inattendu).
Avec kwargs, cet appel reste valide : g est reçu dans kwargs mais simplement ignoré.
Exemple 2¶
Voici un autre exemple où l'on ne connaît pas à l'avance quels arguments seront transmis dans kwargs.
Il s'agit du calcul de quelques statistiques sur plusieurs séries de données.
Dans le code suivant, chaque série est fournie sous forme d'un élément du dictionnaire passé via kwargs.
def statistiques(**kwargs):
stats = {}
for nom, valeurs in kwargs.items():
n = len(valeurs)
if n == 0:
stats[nom] = {None}
continue
stats[nom] = {'moyenne': sum(valeurs) / n,
'min': min(valeurs),
'max': max(valeurs) }
return stats
#Dictionnaire de données
data = {'temperature': [20 , 22 , 21 , 23, 22],
'pression' : [101, 102, 100, 99, 98, 100],
'vitesse' : [5 , 6 , 5.5, 6.2],
'humidité' : []
}
# appel de la fonction avec **data
stats = statistiques(**data)
# affichage des résultats
for c, v in stats.items():
print(f'{c:12s} : {v}')
temperature : {'moyenne': 21.6, 'min': 20, 'max': 23}
pression : {'moyenne': 100.0, 'min': 98, 'max': 102}
vitesse : {'moyenne': 5.675, 'min': 5, 'max': 6.2}
humidité : {None}
Grâce à la définition
statistiques(**kwargs), la fonction peut recevoir un nombre quelconque d’arguments nommés. Tous ces arguments sont rassemblés automatiquement dans le dictionnairekwargs, dont les clés sont les noms des séries (ex."temperature") et les valeurs sont les listes de données associées.La fonction parcourt ensuite
kwargsavec une boucle. Pour chaque série, elle récupère son nom (nom) et sa liste de valeurs (valeurs), calcule la moyenne, le minimum et le maximum, puis range ces résultats dans un sous-dictionnaire. Ce sous-dictionnaire est enfin stocké dans le dictionnairestatssous la même clé que celle fournie par l’utilisateur.Lors de l’appel
statistiques(**data), le dictionnairedataest dépaqueté : chaque paire clé–valeur dedatadevient un argument nommé de la fonction. C’est le même principe que pour le dépaquetage d’une liste ou d’un tuple avec*args, mais ici appliqué aux dictionnaires.Au lieu d’écrire
**data, on pourrait aussi fournir chaque série séparément en arguments nommés, par exemple :
statistiques(temperature=[20, 22, 21, 23, 22],
pression=[101, 102, 100, 99, 98, 100],
vitesse=[5, 6, 5.5, 6.2],
humidité=[]
)
1.4.4.3. Exemple d'arguments nommés et positionnels arbitraires¶
Exemple supplémentaire
Dans cet exemple, on reprend la fonction de calcul de la moyenne moyenne(*args) et on y ajoute deux options via **kwargs :
- Une option pour calculer une moyenne pondérée en fournissant une liste de poids.
- Une option pour retourner une valeur normalisée selon la méthode min–max.
Le code ci-dessous illustre la fonction et différentes manières de l'appeler.
def moyenne(*args, **kwargs):
data = list(args)
if len(data) == 0: return None
weights = kwargs.get('poids', None)
# Si pas de poids alors moyenne arithmétique
if weights is None:
moy = sum(data) /len(data)
else:
moy = sum(x * w for x, w in zip(data, weights)) / sum(weights)
# Normalisation de la moyenne à la demande
if kwargs.get('normaliser', False):
min_val, max_val = min(data), max(data)
moy = (moy - min_val) / (max_val - min_val)
return moy
print('Moyenne arithmétique : ')
print(moyenne(2,4,9))
print('\nMoyenne arithmétique normalisée: ')
print(moyenne(2,4,9, normaliser = True ))
print('\nMoyenne pondérée')
print(moyenne(2,4,9, poids = [3,2,0]))
print(moyenne(2,4,9, poids = [3,2])) # w3 = 0
print('\nMoyenne pondérée normalisée')
print(moyenne(2,4,9, poids = [3,2,0], normaliser = True))
Moyenne arithmétique : 5.0 Moyenne arithmétique normalisée: 0.42857142857142855 Moyenne pondérée 2.8 2.8 Moyenne pondérée normalisée 0.11428571428571425
Le corps de la fonction se compose des étapes suivantes :
On commence par extraire la liste des données :
data = list(args)
Si la liste est vide, on retourne
None:if len(data) == 0: return None
On extrait dans une variable
weightsla liste des poids à partir du dictionnairekwargsà l'aide de la méthodeget()(avecNonepar défaut) :weights = kwargs.get('poids', None)
On teste la valeur de
weights:- Si
weights is NoneestTrue(la clé'poids'n'existe pas danskwargs), on calcule la moyenne simple - Sinon, on calcule la moyenne pondérée.
if weights is None: moy = sum(data) /len(data) else: moy = sum(x * w for x, w in zip(data, weights)) / sum(weights)
- Si
Avant de retourner
moy, on vérifie sikwargscontient la clé'normaliser'et si elle vautTruealors on normalise la moyenne selon min–max:if kwargs.get('normaliser', False): min_val, max_val = min(data), max(data) moy = (moy - min_val) / (max_val - min_val)
Remarque :
Nous avons choisi de normaliser la moyenne plutôt que les données data elles-mêmes. Cela évite de traiter chaque valeur individuellement entraînant un gain de temps d'execution. Cette approche est correcte pour le cas de la moyenne arithmétique (simple ou pondérée).
Cependant, cette approche n'est pas correcte les cas de la moyenne géométrique ou harmonique, il faudrait appliquer la normalisation min–max à chaque élément de data avant le calcul de la moyenne.
On écrirait alors :
data = [(x - min_val) / (max_val - min_val) for x in data]
1.5. Fonctions imbriquées¶
En Python, une fonction imbriquée (ou fonction interne ou nested function) est une fonction définie à l'intérieur du corps d'une autre fonction, appelée fonction englobante ou parente.
La fonction imbriquée n'est accessible que depuis la fonction qui la contient. Elle ne peut être appelée directement depuis l'extérieur de la fonction parente, ce qui en fait un outil d'encapsulation
Ce mécanisme permet principalement :
- D'organiser le code en sous-blocs logiques au sein d'une fonction complexe
- D'isoler des traitements spécialisés réutilisés plusieurs fois dans la fonction parente
- D'éviter la pollution de l'espace de noms global en gardant les fonctions auxiliaires locales
- De créer des fermetures (closures) qui capturent et retiennent l'état de la fonction parente
1.5.1. Exemple 1 - Nombres premiers¶
Comme premiere illustration des fonctions imbriquées, on défini une fonction nombres_premier(n) qui prend en argument un nombre $n$ et retourne un tuple contenant tous les nombres premiers jusqu'à $n$.
Pour cela, nous avons besoin de determiner si tout nombre $k\le n$ n'est divisible que par $1$ et lui même.
On définit alors une autre fonction divisible(k) qui cherche s'il existe au moins un diviseur $d$ de $k$ dans l'intervalle $[2, \ \sqrt{k}]$. Dès que la fonction trouve un diviseur, elle retourne True, sinon elle retourne False.
On utilise cette fonction pour construire un générateur p qu'on retourne sous forme de tuple.
# nombres_premier(n) : Retourne tous les nombres premiers <= n
# divisible(k) : Retourne True si k a un diviseur non trivial
def nombres_premiers(n):
if not isinstance(n,int):
print("n doit être un entier positif")
return tuple()
def divisible(k):
limite = int(k**0.5)
for d in range(2, limite + 1):
if k % d == 0:
return True
return False
p = (k for k in range(2, n+1) if not divisible(k))
return tuple(p)
print(nombres_premiers(50))
(2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47)
1.5.2. Exemple 2 - Hauteur d'eau¶
Comme seconde illustration pratique des fonctions imbriquées, on reprend l'exemple 2.2 - calcul du tirant d'eau du chapitre 2. On utilise ici la méthode de Newton-Raphson pour les calculs itératifs. Pour cela, nous allons définir une fonction qui effectue les calculs et retourne la hauteur comme résultat.
La fonction est définie avec des arguments positionnels et des arguments optionnels, et composée de 3 fonctions internes : qui définit l'équation à résoudre, une pour le calcul de la dérivée et une 3ème pour les itérations.
On montre le code ci-dessous, puis on le commente après :
def tirant_eau(b, Q, S, n, g=9.81, tol=1e-10, itmax=25, detail=False):
"""Calcule le tirant d'eau d'un canal rectangulaire par Newton-Raphson."""
# Expression à résoudre F(h) (imbriquée)
def F(h):
A = b * h
R = A / (b + 2*h)
return A * (1/n) * R**(2/3) * S**0.5 - Q
# Dérivée dF par rapport à h (imbriquée )
def dFdh(h):
A = b * h
R = A / (b + 2*h)
dA = b
dR = (b / (b + 2*h))**2
return (1/n) * (S**0.5) * (dA * R**(2/3) + (2/3) * A * dR / (R**(1/3)))
# Boucle Newton-Raphson (imbriquée)
def newton_raphson(h0):
h = h0
Fh = F(h)
it = 0
while abs(Fh) > tol and it < itmax:
h = h - Fh / dFdh(h)
Fh = F(h)
it += 1
if detail:
print(f"It {it:2d}: h = {h:.8f} m, F(h) = {Fh:.3e}")
return h, it
# Hauteur initiale
h0 = (Q**2 / (g * b**2))**(1/3)
if detail :
print("Point initial (hauteur critique) :")
print(f"h0 = {h0:.6f} m, F(h0) = {F(h0):.3e}")
print('\nIterations : ')
h_final, iterations = newton_raphson(h0)
if iterations >= itmax:
print("Nombre maximal d'itérations atteint : pas de convergence\n")
else:
if detail : print("Fin des calculs avec convergence\n")
return h_final
# programme principal
b = 1.0 # largeur (m)
Q = 0.5 # débit (m3/s)
S = 0.0005 # pente
n = 0.015 # coefficient Manning
h = tirant_eau(b, Q, S, n)
h = tirant_eau(b, Q, S, n, tol = 2e-12, detail = True)
print(h)
Point initial (hauteur critique) : h0 = 0.294277 m, F(h0) = -3.574e-01 Iterations : It 1: h = 0.81402217 m, F(h) = 5.552e-02 It 2: h = 0.74912958 m, F(h) = 2.998e-04 It 3: h = 0.74877523 m, F(h) = 9.967e-09 It 4: h = 0.74877522 m, F(h) = 1.110e-16 Fin des calculs avec convergence 0.7487752211100159
La fonction tirant_eau(. . .) est définie avec des arguments positionnels et des arguments optionnels:
Les argument positionnels concernent les données variables du problème:
b(m) : largeur du canal,Q(m^3/s) : le débit (mesuré)S(-) : la pente du fondnle coefficient de Manning
Les arguments optionnels concernent les paramètres physiques et les options de résolution :
g(m/s^2) : accélération gravitationnelle (paramètre physique par défaut g = 9.81 m/s^2)tol: tolérance des calculs itératifs (par défaut tol = 1e-10)itmaxnombre maximal d'itérations pour éviter une boucle infinie en cas de non convergence (par défaut itmax = 20)detail: paramètre supplémentaire qui contrôle l'affichage des détails de calculs à l'intérieur de la fonction (par défaut detail = False).
Ce paramètre est parfois utile pour vérifier les étapes des calculs et des itérations notamment pendant la phase du développement.
Le corps de la fonction est composé principalement de trois fonctions imbriquées :
La fonction
F(h)implémente l'équation à résoudre $F(h) = 0$ (voir 2.2 - chapitre 2)$$F(h) =\frac{1}{n} A(h) \left(R_H(h)\right)^{2/3} \sqrt{S} - Q.$$
La fonction
dFdhpour la dérivée de $F'(h)$ :$$F'(h) =\frac{1}{n}\sqrt{S} \left(A'(h) R(h)^{2/3} + \frac{2}{3} A(h) R'(h)/ R(h)^{1/3} \right) $$ $$A'(h) = b \quad\text{et}\quad R'(h) = b^2/(b+2h)^2$$
La fonction
newton_raphson(h0)qui permet de réaliser les itérations.
Elle prend comme argument h0, fais appel àF(h)etdFdh(h)et retourne la hauteur calculée ainsi que le nombre d'iterations effectuées.
Le reste du corps de la fonction contient l'initialisation de h0, l'appel de la fonction newton_raphson et le test si les calculs ont convergé ou pas.
1.5.3. La fonction usine (factory)¶
Les fonctions imbriquées sont souvent utilisées pour créer des fonctions sur mesure (pattern factory).
# Une fonction qui permet de fabriquer d'autres fonctions.
# Elle retourne une fonction qui applique l'opération
# indiquée par l'argument `symbole`.
def operateur(symbole):
def operation(a, b): #Fonction imbriquée
match symbole:
case '+':
resultat = a + b
case "*":
resultat = a * b
case "-":
resultat = a - b
case _ :
raise NotImplementedError(f"Opérateur '{symbole}' non pris en charge.")
return f"{a} {symbole} {b} = {resultat}"
return operation
# Création de différentes opérations
additionner = operateur("+")
multiplier = operateur("*")
soustraire = operateur("-")
# applications
print(additionner(5, 3))
print(multiplier(5, 3))
print(soustraire(5, 3))
5 + 3 = 8 5 * 3 = 15 5 - 3 = 2
1.6. Fonction anonyme (lambda)¶
1.6.1. Définition¶
En Python, la fonction anonyme appelée fonction lambda est conçue pour être courte et utilisée de manière ponctuelle.
Contrairement aux fonctions définies avec le mot-clé def, une fonction lambda est écrite sur une seule ligne et n'a pas forcément de nom explicite.
Sa syntaxe suit la structure :
lambda arguments: expression
Ses caractéristiques principales sont :
- Elle est souvent utilisée "à la volée", sans être assignée à une variable.
- Elle ne peut contenir qu'une seule expression (pas d'instructions multiples ni de blocs de code).
- Elle n'a pas de
returnexplicite ; la valeur de l'expression est automatiquement renvoyée. - Elle ne peut pas inclure d'instructions complexes comme
for,whileou des blocsif/elsemultiples (sauf sous forme d'expression ternaire).
Exemple d'utilisation à la volée :
c = (lambda x : x*x)(5.0)
d = (lambda v : 2*v)(3)
print(c,d)
def carre(x):
return(x*x)
carre(5.0)
25.0 6
25.0
# Avec mémoïsation (rapide)
def fibo_memo(n, cache={}):
#print(cache)
if n in cache: # Résultat déjà calculé ?
return cache[n]
if n <= 1:
result = n
else:
result = fibo_memo(n-1, cache) + fibo_memo(n-2, cache)
cache[n] = result # stocke le résultat
return result
fibo_memo(45)
#meilleure
1134903170
L'expression (lambda x : x*x) définit la fonction qui prend x comme argument et retourne son carré x*x.
La valeur entre parenthèse (5.0) est l'appel de la fonction qui revoie 25.0 puis l'affecte à c.
Un print de la fonction affiche l'objet function et l'adresse mémoire où il est stocké.
print((lambda x : x*x))
<function <lambda> at 0x0000015782CFAA20>
Voici deux autres exemples où lambda est appliquée à une liste.
# liste des carrés d'une liste de nombres
nombres = [1,2,3,5,8,13,21]
carres = [(lambda x: x**2)(n) for n in nombres ]
print(carres)
print([x*x for x in nombres])
[1, 4, 9, 25, 64, 169, 441] [1, 4, 9, 25, 64, 169, 441]
# liste des produits des éléments d'une liste de nombres
# la somme donne le produit scalaire.
U = [2, 3, 5]
V = [4, 2, 1]
T = [(lambda x,y: x*y)(u,v) for u,v in zip(U,V)]
print(f'some({T}) = {sum(T)}')
some([8, 6, 5]) = 19
1.6.2. Assignation à une variable¶
Une expression lambda peut être assignée à une variable, de la même manière qu’une fonction définie avec def.
Cette variable pourra ensuite être utilisée comme n'importe quelle autre fonction.
Exemples :
racine = lambda x: x**0.5 # racine(x) avec une variable
somme = lambda x, y : x + y # somme(x,y) avec deux variables
#utilisations multiples des deux fonctions
print('racine(9) =', racine(9))
print('racine(2) =', racine(2))
print('2 + 5 =', somme(2,5))
print('5 - 2 =', somme(5,-2))
print('type de lambda :', type(lambda x: x * 2))
print('type de racine :', type(racine))
racine(9) = 3.0 racine(2) = 1.4142135623730951 2 + 5 = 7 5 - 2 = 3 type de lambda : <class 'function'> type de racine : <class 'function'>
Même si l'assignation d'un fonction lambda à une variable est possible, cette pratique est toutefois déconseillée. Si une fonction lambda s'écrit en une ligne, c’est bien pour qu'on puisse la lire quand elle est utilisée.
1.6.3. Applications courantes¶
La fonction lambda est souvent utilisée dans les opérations de transformation map(), de filtrage filter() et de tri sorted().
On rappelle que :
map(): applique une expression pour transformer les éléments d'un itérable (list, tuple, …)sorted(): trie un itérable selon un critèrefilter(): sélectionne les éléments d'un itérable selon un critère
1.6.3.1. Utilisation avec map()¶
# Utilisation avec map()
nombres = [1,2,3,5,8,13,21]
carres = list(map(lambda x: x**2, nombres))
print(carres)
[1, 4, 9, 25, 64, 169, 441]
La commande précédente est équivalente à une compréhension de liste contenant un appel de fonction :
# commande 2
carres = [(lambda x: x**2)(n) for n in nombres]
Elle est également équivalente à une compréhension de liste simple :
# commande 3
carres = [n**2 for n in nombres]
Les trois commandes produisent le même résultat pour la variable carres.
Dans la commande 2, la fonction lambda est une expression simple appliquée aux éléments de la liste nombres, un à un, grâce à la boucle for intégrée à la compréhension de liste.
Avec map(), en revanche, il n'y a pas de boucle explicite : la fonction lambda est transmise à map, qui l'applique elle-même à l'ensemble des éléments de nombres.
Remarque
map(lambda x: x**2, nombres) renvoie un objet de type map.
Il faut le convertir (par exemple en liste) ou parcourir ses éléments pour les afficher.
#affiche l'objet map et son adresse mémoire
print(map(lambda x: x**2, nombres))
#affiche les éléments de map()
for i in map(lambda x: x**2, nombres):
print(i,end=' ')
<map object at 0x0000015782D375E0> 1 4 9 25 64 169 441
1.6.3.2. Utilisation avec sorted()¶
Voici quelques exemple de tris simples d'une liste de nombres pas ordre croissant (par défaut) ou par ordre décroissant.
nombres = [-3, 0, 1,-5, 8,-4, 7, 2]
print(sorted(nombres)) # tri croissant (par défaut reverse = False)
print(sorted(nombres, reverse=True)) # tri décroissant
[-5, -4, -3, 0, 1, 2, 7, 8] [8, 7, 2, 1, 0, -3, -4, -5]
La fonction sorted() offre la possibilité de trier un itérable selon une clé de tri.
Si on veut par exemple trier la liste précédente par valeurs absolues croissantes alors on utilise la fonction abs comme clé :
print(sorted(nombres, key=abs)) # tri croissant par valeurs absolue
[0, 1, 2, -3, -4, -5, 7, 8]
Lorsqu'on veut trier avec une clé de tri personnalisée, on définit une fonction à appliquer comme clé.
Cette fonction doit prendre un argument et renvoyer une valeur sur laquelle Python peut effectuer
des comparaisons (<, >, ==), afin de déterminer l'ordre des éléments.
L'exemple suivant montre comment trier la liste [-3, 0, 1,-5, 8,-4, 7, 2] en fonction de leurs distance par rapport à 5.
On défini d'abord la fonction dist5(x) qui retourne abs(x-5) qui est la mesure de la distance entre x et 5.
def dist5(x):
return abs(x-5)
print(sorted(nombres, key=dist5)) # tri paires en premiers
[7, 8, 2, 1, 0, -3, -4, -5]
Dans la liste des nombres qu'on vient de trier, 7 est le plus proche de 5 (distance 2) et -5 est le plus éloigné (distance 10).
On peut afficher chaque nombre avec sa distance de 5 sous forme d'une liste de tuples par exemple:
print([(x, abs(x-5)) for x in nombres])
[(-3, 8), (0, 5), (1, 4), (-5, 10), (8, 3), (-4, 9), (7, 2), (2, 3)]
Dans le tri précédent, la fonction dist5(x) se limite à retourner une seule expression et ne fait pas de calculs complexes.
Grâce à une fonction lambda, l'expression abs(x-5) peut être directement utilisée comme clé de tri dans sorted :
print(sorted(nombres, key = lambda x: abs(x-5)))
[7, 8, 2, 1, 0, -3, -4, -5]
De cette façon, en une seule commande, la fonction lambda remplace entièrement la définition de dist5.
Elle sert de clé de tri et n'a pas besoin d'être nommée.
Voici un autre exemple d'illustration de l'utilité de lambda : tri des conduites selon la valeur de la perte de charge qu'elles engendrent.
pertes = [
("Conduite A", 3.2), # m de pertes
("Conduite B", 1.8),
("Conduite C", 2.5)
]
pertes_triees = sorted(pertes, key = lambda val: val[1])
print(pertes_triees)
[('Conduite B', 1.8), ('Conduite C', 2.5), ('Conduite A', 3.2)]
Dans ce ca, la variable pertes est une liste de tuples (print(type(pertes), type(pertes[0])) affichera <class 'list'> <class 'tuple'>). Avec key = lambda val: val[1]), on indique à sorted que le tri doit se faire selon les valeurs ayant l'indice 1 (doc selon les valeurs 3.2, 1.8 et 2.5).
Remarque :
L'utilisation de la fonction sorted avec la fonction lambda permet de réaliser un tri avec une clé multiple. Il suffit de retourner un tuple de plusieurs critères en sortie de lambda. La syntaxe générale est :
sorted(liste, key=lambda x: (critère1, critère2, ...))
L'exemple suivant permet de trier une liste de points $(x,y)$ en géométrie bidimensionnelle selon leurs distances $d$ par rapport à l'origine ($d^2 = x^2 + y^2$), et selon leur abscisses $x$.
# Liste de points (x, y)
points = [(2, 5), (1, 3), (5, 2), (1, 7), (3, 1), (2, 1) ]
# Tri selon la distance à l'origine (0, 0) d'abord,
# puis par coordonnée x en cas d'égalité
points_triés = sorted(points, key=lambda p: (p[0]**2 + p[1]**2, p[0]))
print("Points triés par distance à l'origine puis par x:")
for point in points_triés:
x, y = point
d2 = (x**2 + y**2)
print(f" {point = } : d^2 = {d2}")
Points triés par distance à l'origine puis par x: point = (2, 1) : d^2 = 5 point = (1, 3) : d^2 = 10 point = (3, 1) : d^2 = 10 point = (2, 5) : d^2 = 29 point = (5, 2) : d^2 = 29 point = (1, 7) : d^2 = 50
1.6.3.3. Utilisation avec filter()¶
filter(function, iterable) attend une fonction qui renvoie True ou False.
positifs = list(filter(bool, [x*(x>5) for x in nombres]))
print(positifs)
print(list(filter(lambda x: x>5, nombres)))
[8, 7] [8, 7]
1.7. Portée des variables¶
Python utilise une hiérarchie de portée des variables, appelée règle LEGB, qui détermine :
- leur visibilité : où une variable est accessible,
- leur durée de vie : pendant combien de temps l'objet (la variable) référencé existe.
Lorsqu'une variable est utilisée, Python la cherche dans l'ordre LEGB, qui signifie :
L - Local : les variables définies à l'intérieur d'une fonction sont locales par défaut.
- Visibilité : uniquement dans la fonction où elles sont définies.
- Durée de vie : pendant l'exécution de la fonction.
E - Enclosing (non local) : les variables définies dans une fonction englobante sont non locales pour les fonctions imbriquées.
- Visibilité : accessible dans la fonction interne.
- Durée de vie : tant que la fonction englobante existe.
- Pour modifier une variable non locale depuis la fonction interne, il faut utiliser le mot-clé
nonlocal.
G - Global : les variables définies au niveau principal du programme sont globales.
- Visibilité : accessibles dans toutes les fonctions, y compris les sous-fonctions.
- Durée de vie : pendant toute l'exécution du programme.
- Pour modifier une variable globale depuis une fonction, il faut la déclarer avec
global.
Pour la lecture seule, aucune déclarationglobaln'est nécessaire.
B - Built-in (intégré) : noms des objets prédéfinis par Python.
- Ces noms peuvent désigner des fonctions, des types ou des constantes intégrées.
- Visibilité : accessibles partout dans le programme.
- Durée de vie : tant que l'interpréteur Python est actif.
- Les constantes intégrées sont :
True,False,None,Ellipsis,NotImplemented
- Les constantes intégrées sont :
1.7.1. Exemple de variable globale¶
# Retourne A+x sans changer A
def f1(x):
var_interne = A + x
return var_interne
# Chance A en A+x puis retourne sa valeur
def f2(x):
global A
A = A + x
return A
# Crée une variable locale A différente de celle
# du programme, lui ajoute x puis retourne sa valeur
def f3(x):
A = 5.5 + x
return A
# cette fonction va provoquer une erreur à l'appel
def f4(x):
A = A + x
return A
# Variable définie dans le programme principal
A = 5.5
# retourne la bonne valeur et ne modifie pas A
y = f1(4.5)
print(f"sans 'global A' : {y = }, {A = }")
# retourne la bonne valeur mais modifie A
y = f2(4.5)
print(f"avec 'global A' : {y = }, {A = }")
# retourne la bonne 5.5 + x
y = f3(4.5)
print(f"variable A locale : {y = }, {A = }")
#sans global A, une tentative de modification
# à l'intérieure de la fonction provoque
# une erreur : UnboundLocalError
#print(f4(4.5))
sans 'global A' : y = 10.0, A = 5.5 avec 'global A' : y = 10.0, A = 10.0 variable A locale : y = 10.0, A = 10.0
Dans cet exemple, on a défini A = 5.5 dans le programme principal.
Elle est visible en lecture dans les quatres fonctions.
La fonction
f1(x)
Elle lit la valeur globale deApour calculerA + x, stockée dans la variable localevar_interne, puis retourne ce résultat.
La variable globaleAn'est pas modifiée.La fonction
f2(x)
DéclareAcomme variable globale (global A) pour y avoir accès total et la modifier en l'incrémentant dex.
La valeur deAest modifiée aussi dans le programme principal.La fonction
f3(x)
Crée une variable localeAavec l'instructionA = 5.5 + x. Elle est du même nom mais distincte de la variable globale.
Cette variable locale prend la valeur dex(argument) et lui ajoute 5.5 et retourne lé résultat.
Elle est indépendante de la variable globaleA. Elle la masque à l'intérieur de la fonction et n'a aucun effet sur elle.La fonction
f4(x)
Utilise l'instructionA = A + xsans déclarerAcomme variable globale. Python considère doncAcomme locale dans toute la fonction. Cependant, dans la partie droite de l'affectation(A + x), il tente de lire la valeur de A avant qu'elle n'ait reçu une valeur locale. Il lève donc uneUnboundLocalError, une erreur qui se produit lorsqu'on essaie d'accéder à une variable locale avant qu'elle n'ait été initialisée dans la fonction.
1.7.2. Exemple de variable non locale¶
Une variable non locale, partagée entre une fonction et une sous-fonction imbriquée, se comporte de manière similaire à une variable globale partagée entre le programme principal et une fonction.
Toute variable d'une fonction est visible en lecture seule dans ses sous-fonctions imbriquées. Si on a besoin de modifier une variable locale à une fonction dans une de ses sous-fonctions, il faut la déclarer dans la sous-fonction avec le mot-clé nonlocal.
Dans l'exemple suivant, on calcule la racine carrée d'un nombre a avec la fonction racine(a, tol=1e-6) qui prend a comme argument positionnel et tol comme argument optionnel, et renvoie x comme solution de l'équation $x^2 - a = 0$.
La fonction initialise la solution x et appelle la fonction iterationsNewton() qui a accès à x, a et tol. Comme on a besoin de modifier x dans la sous-fonction, on le déclare comme variable non locale avec nonlocal x.
def racine(a, tol = 1e-6):
#sous-fonction iterations()
def iterationsNewton():
nonlocal x
while abs(x**2 - a) > tol:
x = 0.5 * (x + a/x)
x = 0.5*a # initialisation
iterationsNewton() # itérations
return x
racine(16)
4.000000000000004
Remarqu :
Bien que parfois utile, l'utilisation de variables globales doit être limitée car cela peut :
- Créer des effets de bord : Modification imprévue de l'état du programme depuis n'importe quelle fonction
- Réduire la maintenabilité : Code plus difficile à comprendre, tester et déboguer
- Introduire des dépendances cachées : Relations implicites entre fonctions qui ne sont pas visibles dans leurs paramètres.
- Limiter la réutilisabilité : Fonctions dépendantes du contexte global
- Provoquer des conflits : Risque d'écrasement accidentel par d'autres parties du programme.
Alternatives préférables
- Passer les valeurs en paramètres
- Retourner explicitement les résultats
- Réserver les variables globales aux constantes, écrites en UPPER_CASE, et éviter toute modification.
1.8. Fonctions récursives¶
Une fonction récursive est une fonction qui s'appelle elle-même, directement à partir de son propre corp ou indirectement par l'intermédiaire d'une autre fonction qu'elle appelle.
Les fonctions récursives sont particulièrement adaptées à la résolution de problèmes définis de manière récursive, c'est-à-dire lorsque la solution d'un problème peut s'exprimer en fonction de la même solution des sous-problèmes plus simples.
Les fonctions récursives sont particulièrement adaptées à la résolution de problèmes dont la solution peut se déduire de la solution de sous-problèmes plus simples de même nature.
Une fonction récursive doit toujours comporter :
- un cas de base (condition d'arrêt),
- et un appel récursif qui progresse vers ce cas de base.
Les fonctions récursives sont souvent claires et naturelles pour exprimer certains algorithmes (par exemple sur des structures arborescentes), mais elles ne sont pas toujours plus efficaces que les solutions itératives et peuvent entraîner un surcoût en mémoire si elles sont mal utilisées.
1.8.1. Récursivité directe¶
Une fonction utilise la récursivité directe lorsqu'elle s'appelle elle-même directement à l'intérieur de son propre corps.
L'exemple le plus typique est la fonction factorielle : $n! = n (n-1)!$ avec $0! = 1$.
Le code suivant illustre bien ce principe. Il définit la fonction factorielle(n) qui reçoit n comme argument:
- Si
n <= 1, elle retourne 1 (cas de base) - Sinon elle retourne
n*factorielle(n-1), s'appelle ainsi elle-même avecn-1jusqu'à atteindre le cas de base (n=1).
Pour plus de précision, on donne un code équivalent qui utilise une boucle for.
def factorielle(n):
if n <= 1:
return 1
return n*factorielle(n-1)
n = 7
print(f'récursivité : {n}! = {factorielle(n)}')
# Fonction équivalente avec une boucle
def factorielle_boucle(n):
fact = 1
for i in range(1,n+1):
fact *= i
return fact
print(f'boucle for : {n}! = {factorielle_boucle(n)}')
récursivité : 7! = 5040 boucle for : 7! = 5040
Comme second exemple, on définit une suite récurrente : $ u_n = \displaystyle \frac{n}{1+ u_{n-1}}$ avec $u_0 = 1$
Le codage de fonction récursive (ci-dessous) est similaire à l'écriture mathématique de la suite, contrairement à la fonction équivalente itérative avec boucle for.
def u(n):
if n==0 : return 1 # cas de base
return n/(1 + u(n-1)) # apple récursif
# test
n = 1000
print(f'u({n}) = {u(n)}')
# Fonction équivalente itérative
def u_boucle(n):
val_u = 1
for i in range(1,n+1):
val_u = i/(1 + val_u)
return val_u
print(f'u({n}) = {u_boucle(n)}')
u(1000) = 31.13450998698424 u(1000) = 31.13450998698424
1.8.2. Récursivité indirecte¶
On parle de récursivité indirecte lorsqu'une fonction ne s'appelle pas elle-même directement, mais passe par une ou plusieurs autres fonctions qui, à leur tour, rappellent la fonction initiale.
Si une fonction A appelle une fonction B, et B finit par appeler A à nouveau, les deux forment ainsi un cycle d'appels récursifs indirects.
Comme exemple illustratif, on définit deux suites $u_n$ et $v_n$ par :
$$ u_n = \begin{cases} 1 & \text{si } n = 0 \\ 2v_{n-1} & \text{si } n \ge 1 \end{cases} % \qquad, \qquad % v_n = \begin{cases} 1 & \text{si } n = 0 \\ 1 + u_{n-1} & \text{si } n \ge 1 \end{cases} $$
La suite $u$ dépend de $v$, et la suite $v$ dépend de $u$ : la définition est indirectement récursive.
Le code suivant montre
def u(n):
if n == 0: # cas de base
return 1
return 2*v(n-1) # appel de v
def v(n):
if n == 0: # cas de base
return 1
return 1+ u(n - 1) # appel de u
n = 2
print(f'u({n}) = {u(n)} , v({n}) = {v(n)}')
u(2) = 4 , v(2) = 3
Si on veut comment les deux fonctions s'appellent et se transmettent des valeur, il suffit d'insérer des commandes print dans chacune d'elles.
def u(n):
if n == 0: # cas de base
print(f'u({n}) = {1}', end=' , ')
return 1
val = 2 * v(n-1)
print(f'u({n}) = {val}', end=' , ')
return val
def v(n):
if n == 0:
print(f'v({n}) = {1}', end=' , ')
return 1
val = 1 + u(n - 1)
print(f'v({n}) = {val}',end=(' , '))
return val
n = 7
print(f'\nu({n}) = {u(n)}\n')
print(f'\nv({n}) = {v(n)}')
v(0) = 1 , u(1) = 2 , v(2) = 3 , u(3) = 6 , v(4) = 7 , u(5) = 14 , v(6) = 15 , u(7) = 30 , u(7) = 30 u(0) = 1 , v(1) = 2 , u(2) = 4 , v(3) = 5 , u(4) = 10 , v(5) = 11 , u(6) = 22 , v(7) = 23 , v(7) = 23 , u(1) = 2 , v(2) = 3 , u(3) = 6 , v(4) = 7 , u(5) = 14 , v(6) = 15 , u(7) = 30 , u(7) = 30 u(0) = 1 , v(1) = 2 , u(2) = 4 , v(3) = 5 , u(4) = 10 , v(5) = 11 , u(6) = 22 , v(7) = 23 , v(7) = 23
Enfin, on présente une version itérative équivalente afin de comparer de nouveau les deux approches : fonctions récursive et fonction avec boucle.
# version sans récursivité:
def uv(n):
u, v = 1, 1 # u_0 et v_0
print(f"u(0) = {u} \t v(0) = {v}")
for i in range(1, n+1):
u_new = 2 * v
v_new = 1 + u
u, v = u_new, v_new
print(f"u({i}) = {u} \t v({i}) = {v}")
return u,v
n = 7 # nombre de termes
u,v = uv(n)
#print(f"u({n}) = {u} \t v({n}) = {v}")
u(0) = 1 v(0) = 1 u(1) = 2 v(1) = 2 u(2) = 4 v(2) = 3 u(3) = 6 v(3) = 5 u(4) = 10 v(4) = 7 u(5) = 14 v(5) = 11 u(6) = 22 v(6) = 15 u(7) = 30 v(7) = 23
1.9. Mémoïsation¶
La mémoïsation est une technique qui consiste à mémoriser dans une variable, les résultats de calculs déjà effectués par une fonction afin d'éviter de les recalculer lorsqu'on rencontre les mêmes entrées.
Le but de cette technique d'optimisation de code est de diminuer le temps d'exécution d'un programme au prix d'un peu de mémoire supplémentaire.
Comme exemple illustratif, on prend la suite de Fibonacci
$$ \begin{cases} F_0 = 0 \\ F_1 = 1 \\ F_n = F_{n-1} + F_{n-2}, \quad n \ge 2 \end{cases} $$
Premiers termes : $ 0,\ 1,\ 1,\ 2,\ 3,\ 5,\ 8,\ 13,\ 21,\ \dots $
Le code suivant montre la mise en œuvre de la suite de Fibonacci à l'aide d'une fonction récursive comportant un double appel interne. La fonction reçoit un une nombre $n$ et renvoie $F_n$
def fibo(n):
if n <= 1: # cas de base
return n
return fibo(n-1) + fibo(n-2) # recursion
fibo(40) # calculs lent pour n grand (> 35)
102334155
Le problème de ce code est que le double appel récursif entraîne un très grand nombre de calculs redondants.
Les mêmes valeurs de fibo(n-1) et fibo(n-2) sont recalculées de nombreuses fois, et chacun des deux fait encore deux autres appels. Ceci provoque une explosion du temps d'exécution, en particulier pour les grandes valeurs de n.
La mémoïsation permet de réduire considérablement ce temps de calcul en introduisant une variable cache, généralement un dictionnaire, qui mémorise les résultats déjà calculés.
À chaque appel de la fonction, la valeur de n est utilisée comme clé, et le résultat correspondant est stocké comme valeur.
- Si
nest déjà présent danscache, la fonction renvoie immédiatement la valeur mémorisée. - Sinon, la valeur est calculée récursivement, stockée dans
cache, puis retournée.
Sans mémoïsation, la complexité temporelle de cet algorithme est exponentielle.
Avec la mémoïsation, elle devient linéaire, ce qui rend le calcul efficace même pour des valeurs de n élevées.
Pour n = 40 par exemple, le temps de calcul passe de 9.5 secondes à quelques millisecondes.
def fibo_memo(n, cache=None):
# Initialisation du cache lors du premier appel
if cache is None:
cache = {}
# Si la valeur a déjà été calculée, on la renvoie directement
if n in cache:
return cache[n]
# Cas de base
if n <= 1:
result = n
else:
# Appels récursifs avec mémoïsation
result = fibo_memo(n - 1, cache) + fibo_memo(n - 2, cache)
# Stockage du résultat pour les appels futurs
cache[n] = result
return result
fibo_memo(41)
165580141
1.10. Exercices - TP N°4¶
1.10.1. Exercice 1¶
Ecrire une fonction, nommée
reynols, permettant de calculer le nombre de Reynolds d'un écoulement en charge dans une conduite.La fonction doit recevoir
Deux arguments positionnels :
- Le diamètre $D$ de la conduite
- La vitesse $V$ de l'écoulement
Un argument optionnel :
- La viscosité cinématique $\nu$ du liquide, dont la valeur par défaut est $\nu = 1.003\,10^{-6}\ \text{m}^2/\text{s}$
La fonction devra également vérifier la validité des arguments fournis (valeurs strictement positives, types numériques, etc.).
Ecrire différentes commandes d'appel de cette fonction afin de vérifier que le code s'exécute correctement et que les résultats obtenus sont cohérents.
Écrire une autre fonction, nommée
regime, qui utilise la fonctionreynoldspour déterminer le type de régime d'écoulement.Elle doit recevoir, en plus des mêmes arguments que
reynolds, deux arguments optionnels supplémentaires définissant les plages du nombre de Reynolds associées aux différents régimes d'écoulement.Elle doit retourner une chaîne de caractères désignant le type d'écoulement :
'laminaire','transitoire', outurbulent.
Modifier
regimepour quelle renvoie en même temps la valeur du nombre de ReynoldsReavec le type d'écoulement
1.10.2. Exercice 2¶
La fonction définie dans l'exemple de la section 1.10 renvoie le terme $F_n$ de la suite de Fibonacci.
Dans cet exercice on demande d'utiliser cette fonction pour :
- Construire une liste des $n$ premiers termes de la suite : $u_k = F_{k+1}/F_k$ , $ 1 < k \leq n$.
- Verifier que $u_k$ converge vers le nombre d'or $\phi = (1+\sqrt{5})/2\quad : \quad$ $\displaystyle \lim_{k \to +\infty} \frac{F_{k+1}}{F_k} = \phi$
Comparer entre $\phi$ et $u_k$ pour $k$ grand
Voici l’écriture correcte et rigoureuse de la limite :
C’est cette notation qu’il convient d’utiliser pour exprimer formellement la convergence vers le nombre d’or.
Aucun commentaire:
Enregistrer un commentaire