1Definir et appeler une fonction
Une fonction est l'outil d'abstraction fondamental de la programmation : elle permet de nommer un traitement, de le tester isolement et de le reutiliser sans le reecrire. Tout programme d'une certaine taille est un assemblage de fonctions courtes.
Une fonction associe a des parametres un traitement et, eventuellement, une valeur de retour. En Python :
def nom(param1, param2, ...):
"""Docstring : ce que fait la fonction, ses preconditions."""
corps
return valeurDefinition et appel
def aire_disque(r):
"""Aire d'un disque de rayon r >= 0."""
from math import pi
return pi * r * r
a = aire_disque(2) # APPEL : 2 est l'argument, r est le parametre
print(a) # 12.566370614359172
La definition (def) ne fait qu'associer un nom a un bloc de code : rien n'est
execute. Seul l'appel declenche l'execution.
- Parametre (ou parametre formel) : le nom qui figure dans la definition.
- Argument (ou parametre effectif) : la valeur passee lors de l'appel.
- Signature : le nom de la fonction et la liste de ses parametres.
- Specification : ce que la fonction promet de faire (docstring), par opposition a son implementation (le corps).
return : valeur de retour
def carre(x):
return x * x # renvoie une valeur, utilisable dans un calcul
def affiche_carre(x):
print(x * x) # affiche, mais ne renvoie RIEN
y = carre(5) # y vaut 25
z = affiche_carre(5) # affiche 25, et z vaut None
print(2 * carre(3)) # 18
print(2 * affiche_carre(3)) # TypeError : None n'est pas multipliable
print affiche, return renvoie. Une
fonction qui se contente d'afficher est inutilisable dans un autre calcul. Dans un devoir, sauf
mention explicite, une fonction doit toujours renvoyer son resultat.
Points de detail sur return :
- il termine immediatement la fonction : le code qui suit n'est pas execute ;
- une fonction sans
return(ou avec unreturnnu) renvoieNone; - on peut renvoyer plusieurs valeurs : c'est en realite un tuple.
def div_euclide(a, b):
"""Renvoie le couple (quotient, reste)."""
return a // b, a % b # tuple implicite
q, r = div_euclide(17, 5) # depaquetage : q vaut 3, r vaut 2
t = div_euclide(17, 5) # t vaut (3, 2)
Parametres par defaut et arguments nommes
def puissance(x, n=2):
"""x^n ; n vaut 2 par defaut."""
return x ** n
puissance(5) # 25 : n prend sa valeur par defaut
puissance(5, 3) # 125 : argument positionnel
puissance(n=3, x=5) # 125 : arguments nommes, l'ordre est libre
Les parametres a valeur par defaut doivent figurer apres ceux qui n'en ont pas.
def ajoute(x, t=[]): # NE JAMAIS FAIRE CELA
t.append(x)
return t
ajoute(1) # [1]
ajoute(2) # [1, 2] !!! la meme liste est reutilisee
La valeur par defaut est evaluee une seule fois, a la definition. Si elle est mutable, elle est partagee par tous les appels. La forme correcte :
def ajoute(x, t=None):
if t is None:
t = []
t.append(x)
return t2Portee des variables et passage des arguments
Comprendre ou vit une variable est indispensable pour ecrire des fonctions correctes. C'est la source d'erreur numero un chez les debutants.
La portee d'une variable est la region du programme ou son nom est visible. Une variable creee dans une fonction est locale : elle nait a l'appel, meurt au retour, et est invisible de l'exterieur. Une variable creee au niveau du module est globale.
def f():
a = 10 # a est LOCALE a f
print(a)
f() # 10
print(a) # NameError : a n'existe pas ici
La regle LEGB
Pour resoudre un nom, Python cherche dans l'ordre :
- Local : le corps de la fonction courante ;
- Enclosing : les fonctions englobantes ;
- Global : le niveau du module ;
- Built-in : les fonctions natives (
len,max...).
La premiere trouvee gagne. C'est pourquoi definir max = 3 au niveau global masque
la fonction native.
x = "global"
def g():
print(x) # lecture : Python remonte au niveau global -> "global"
def h():
x = "local" # AFFECTATION : x devient local a h
print(x) # "local"
g(); h(); print(x) # global / local / global
c = 0
def incremente():
c = c + 1 # UnboundLocalError
incremente()
Des qu'un nom est affecte quelque part dans une fonction, Python le considere comme
local dans toute la fonction, y compris avant l'affectation. La lecture
c + 1 porte donc sur un c local pas encore initialise.
global : a fuir
c = 0
def incremente():
global c # techniquement correct...
c = c + 1
Cela fonctionne, mais rend la fonction dependante d'un etat exterieur : elle n'est plus testable isolement, son comportement depend de l'ordre des appels. La bonne solution est de passer la valeur en parametre et de renvoyer le resultat :
def incremente(c):
return c + 1
c = incremente(c)
Une fonction est pure si (i) sa valeur de retour ne depend que de ses arguments et (ii) elle ne modifie rien en dehors d'elle-meme (pas d'effet de bord). Les fonctions pures sont plus faciles a tester, a prouver et a reutiliser. C'est le style a viser en CPGE.
Passage des arguments : le point delicat
Python passe les arguments par reference d'objet. La fonction recoit une nouvelle etiquette collee sur le meme objet. Les consequences dependent de la mutabilite.
def modifie_entier(n):
n = n + 1 # rattache le nom LOCAL n a un nouvel objet
return n
a = 5
modifie_entier(a)
print(a) # 5 : inchange, un int est IMMUABLE
def modifie_liste(t):
t.append(99) # MODIFIE l'objet lui-meme
u = [1, 2]
modifie_liste(u)
print(u) # [1, 2, 99] : la liste appelante est modifiee !
def remplace_liste(t):
t = [0, 0] # rattache le nom LOCAL a une nouvelle liste
u = [1, 2]
remplace_liste(u)
print(u) # [1, 2] : inchange
| Type | Mutable ? | Une modification dans la fonction est-elle visible ? |
|---|---|---|
int, float, bool, str,
tuple | non | non |
list, dict, set | oui | oui, si l'on modifie l'objet (append, t[i] = ...) |
def trie_en_place(t):
"""Modifie t. Renvoie None. (Effet de bord assume.)"""
t.sort()
def trie_copie(t):
"""Renvoie une NOUVELLE liste triee. t est inchangee."""
return sorted(t)
Choisir l'un ou l'autre, jamais les deux : une fonction qui modifie et renvoie induit
en erreur. La convention Python est qu'une fonction modifiant en place renvoie
None.
3Pile d'appels, fonctions natives et modularite
Ce qui se passe a l'appel : la pile d'execution
A chaque appel, Python empile un bloc d'activation contenant les parametres
et les variables locales. Au return, ce bloc est depile et sa memoire liberee.
def cube(x):
return x * carre(x)
def carre(x):
return x * x
print(cube(3))
cube(3)Deux blocs distincts peuvent contenir une variable de meme nom sans interference : les variables locales sont vraiment locales. C'est ce mecanisme qui rend la recursivite possible (chapitre 14) : chaque appel recursif possede son propre jeu de variables.
Fonctions natives utiles
| Fonction | Role | Exemple |
|---|---|---|
len(s) | longueur | len([1,2,3]) → 3 |
abs(x) | valeur absolue | abs(-3) → 3 |
min, max | extremums | max(3, 7), max(t) |
sum(t) | somme | sum([1,2,3]) → 6 |
round(x, n) | arrondi | round(3.14159, 2) → 3.14 |
divmod(a,b) | (quotient, reste) | divmod(17,5) → (3,2) |
sorted(t) | copie triee | sorted([3,1,2]) → [1,2,3] |
enumerate(t) | couples (indice, valeur) | voir chapitre 3 |
zip(t, u) | appariement | list(zip([1,2],"ab")) |
Fonctions anonymes
f = lambda x: x**2 + 1 # equivaut a def f(x): return x**2 + 1
f(3) # 10
points = [(1, 5), (2, 3), (3, 9)]
points.sort(key=lambda p: p[1]) # tri selon la 2e coordonnee
Une lambda ne contient qu'une expression, pas d'instruction. On ne
l'utilise que comme argument (cle de tri, filtre) ; l'affecter a un nom est un contresens : autant
ecrire un def.
Modularite : concevoir avec des fonctions
- Identifier les sous-problemes independants : chacun devient une fonction.
- Ecrire pour chacun sa specification avant son code.
- Ecrire les tests (
assert) juste apres la fonction. - Assembler dans une fonction principale qui n'orchestre que les appels.
Regle empirique : si une fonction depasse une vingtaine de lignes ou comporte plus de trois niveaux d'indentation, elle en cache probablement deux.
def moyenne(t):
"""Moyenne arithmetique de la liste non vide t."""
return sum(t) / len(t)
def variance(t):
"""Variance (population) de la liste non vide t."""
m = moyenne(t)
return sum((x - m)**2 for x in t) / len(t)
def ecart_type(t):
"""Ecart-type de la liste non vide t."""
return variance(t) ** 0.5
def mediane(t):
"""Mediane de la liste non vide t (t n'est pas modifiee)."""
u = sorted(t)
n = len(u)
if n % 2 == 1:
return u[n // 2]
return (u[n//2 - 1] + u[n//2]) / 2
def resume(t):
"""Renvoie un dictionnaire de statistiques descriptives."""
return {"n": len(t), "moyenne": moyenne(t), "mediane": mediane(t),
"ecart_type": ecart_type(t), "min": min(t), "max": max(t)}
s = [12, 15, 9, 18, 11]
assert moyenne(s) == 13.0
assert mediane(s) == 12
assert abs(ecart_type(s) - 3.1622776601683795) < 1e-12
Chaque fonction est courte, testable, et ecart_type reutilise
variance qui reutilise moyenne : c'est la composition qui fait la
puissance de la decomposition.
L'essentiel du chapitre
returnrenvoie,printaffiche : ne jamais confondre.- Une variable affectee dans une fonction est locale ;
globalest a proscrire. - Les objets immuables (
int,str,tuple) ne peuvent pas etre modifies par une fonction ; les listes et dictionnaires, si. - Ne jamais utiliser une valeur par defaut mutable (
def f(t=[])). - Une fonction pure ne depend que de ses arguments et ne produit aucun effet de bord.
- Chaque appel possede son propre bloc d'activation dans la pile.
- Specifier, puis coder, puis tester avec des
assert.