8 Fonctions

La plupart du temps, on utilise les fonctions de base ou contenues dans des modules. Cela dit, lorsque l’on récupère des données en ligne ou qu’on doit mettre en forme des données importées depuis diverses sources, il arrive qu’il soit nécessaire de créer ses propres fonctions. L’avantage de créer ses fonctions se révèle dès lors qu’on doit effectuer une suite d’instruction de manière répétée, avec quelques légères différences (on peut alors appliquer les fonctions au sein d’une boucle, comme nous l’avons abordé dans le Chapitre 7).

8.1 Définition

Une fonction est déclarée à l’aide du mot clé keyword. Ce qu’elle renvoie est retourné à l’aide du mot clé return.

La syntaxe est la suivante :

def nom_fonction(parametres):
  corps_de_la_fonction

Une fois que la fonction est définie, on l’appelle en faisant référence à son nom :

nom_fonction()

Il suffit donc de rajouter des parenthèses au nom de la fonction pour l’appeler. En effet, nom_fonction désigne l’objet qui contient la fonction qui est appelée à l’aide de l’expression nom_fonction(). Par exemple, si on souhaite définir la fonction qui calcule le carré d’un nombre, voici ce que l’on peut écrire :

def carre(x):
  return x**2

On peut ensuite l’appeler :

print(carre(2))
## 4
print(carre(-3))
## 9

8.1.1 Ajout d’une description

Il est possible (et fortement recommandé) d’ajouter une description de ce que la fonction fait, en adoptant des conventions (c.f. https://www.python.org/dev/peps/pep-0257/) =

def carre(x):
  """retourne le carré de x"""
  return x**2

De fait, quand on évalue ensuite l’instruction suivante, la description de la fonction s’affiche :

`?`(carre)

Dans Jupyter Notebook, après avoir écrit le nom de la fonction, on peut aussi afficher la description en appuyant sur les touches du clavier Shift et Tabulation.

8.1.2 Paramètres d’une fonction

Dans l’exemple de la fonction carre() que nous avons créée, nous avons renseigné un seul paramètre, appelé x. Si la fonction que l’on souhaite créer nécessite plusieurs paramètres, il faut les séparer par une virgule.

Considérons par exemple le problème suivant. Nous disposons d’une fonction de production \(Y(L, K, M)\), qui dépend du nombre de travailleurs \(L\) et de la quantité de capital \(K\), et du matériel \(M\), telle que \(Y(L, K, M) = L^{0.3} K^{0.5}M^2\). Cette fonction pourra s’écrire en Python de la manière suivante :

def production(l, k, m):
  """
  Retourne la valeur de la production en fonction
  du travail, du capital et du matériel.
  
  Keyword arguments:
  l -- travail (float)
  k -- capital (float)
  m -- matériel (float)
  """
  return l**0.3 * k**0.5 * m**(0.2)

8.1.2.1 Appel sans noms de paramètres

En reprenant l’exemple précédent, si on nous donne \(L = 60\) et \(K = 42\) et \(M = 40\), on peut en déduire la production :

prod_val = production(60, 42, 40)
print(prod_val)
## 46.289449781254994

On peut noter que le nom des paramètres n’a pas été mentionné ici. Lors de l’appel de la fonction, la valeur du premier paramètre a été attribué au paramètre défini en premier (l), celle du second au second paramètre (k) et enfin celle du troisième au troisième paramètre (m).

8.1.2.2 Paramètres positionnels paramètres par mots-clés

Il existe deux types de paramètres que l’on peut donner à une fonction en Python :

  • les paramètres positionnels ;
  • les paramètres par mots-clés.

Contrairement aux paramètres positionnels, les paramètres par mot clé ont une valeur attribuée par défaut. On parle de paramètre formel pour désigner les paramètres de la fonction (les variables utilisées dans le corps de la fonction) et de paramètre effectif pour désigner la valeur que l’on souhaite donner au paramètre formel. Pour définir la valeur à donner à un paramètre formel, on utilise le symbol d’égalité. Lors de l’appel de la fonction, si l’utilisateur ne définit pas explicitement une valeur, celle par défaut sera affectée. Ainsi, il n’est pas forcément nécessaire de préciser les paramètres par mots-clés lors de l’appel de la fonction.

Il est important de noter que les arguments positionnels (ceux qui n’ont pas de valeur par défaut) doivent apparaître en premier dans la liste des paramètres.

Prenons un exemple avec deux paramètres positionnels (l et m) et un paramètre par mot-clé (k) :

def production_2(l, m, k=42):
  """
  Retourne la valeur de la production en fonction
  du travail, du capital et du matériel.
  
  Keyword arguments:
  l -- travail (float)
  m -- matériel (float)
  k -- capital (float) (default 42)
  """
  return l**0.3 * k**0.5 * m**(0.2)

La fonction production_2() peut s’appeler, pour donner le même résultat, des trois manières suivantes :

# En nommant tous les paramètres, en ommettant k
prod_val_1 = production_2(l = 42, m = 40)
# En nommant tous les paramètres et en précisant k
prod_val_2 = production_2(l = 42, m = 40, k = 42)
# En nommant uniquement le paramètre mot-clé k
prod_val_3 = production_2(42, 40, k = 42)
# En ne nommant aucun paramètre
prod_val_4 = production_2(42, 40, 42)

res = [prod_val_1, prod_val_2, prod_val_3, prod_val_4]
print(res)
## [41.59215573604822, 41.59215573604822, 41.59215573604822, 41.59215573604822]

Si la fonction contient plusieurs paramètres positionnels ; lors de l’appel :

  • soit on nomme tous les paramètres positonnels par leur nom ;
  • soit aucun ;
  • il n’y a pas d’entre deux.

Du moment que tous les paramètres positionnels sont nommés lors de l’appel, on peut les faire figurer dans des ordres différents :

def production_3(a, l, m = 40, k=42):
  """
  Retourne la valeur de la production en fonction
  de la productivité, du travail, du capital et du matériel.
  
  Keyword arguments:
  a -- productivité totale des facteurs (float)
  l -- travail (float)
  m -- matériel (float) (default 40)
  k -- capital (float) (default 42)
  """
  return a * l**0.3 * k**0.5 * m**(0.2)
  
prod_val_1 = production_3(1, 42, m = 38)
prod_val_2 = production_3(a = 1, l = 42)
prod_val_3 = production_3(l = 42, a = 1)
prod_val_4 = production_3(m = 40, l = 42, a = 1)

res = [prod_val_1, prod_val_2, prod_val_3, prod_val_4]
print(res)
## [41.16765711449734, 41.59215573604822, 41.59215573604822, 41.59215573604822]

8.1.2.3 Fonction comme paramètre

Une fonction peut être fournie en paramètre à une autre fonction.

def carre(x):
  """Retourne le carré de x"""
  return x**2
  
def appliquer_carre_4(fun):
  """Applique la fonction `fun` à 4"""
  return fun(4)

print(appliquer_carre_4(carre))
## 16

8.2 Portée

Lorsque une fonction est appelée, le corps de cette fonction est interprété. Les variables ayant été définies dans le corps de la fonction sont assignées à un namespace local. C’est-à-dire qu’elles ne vivent qu’à l’intérieur ce cet espace local, qui est créé au moment de l’appel de la fonction et détruit à la fin de celui ci. On parle alors de portée des variables. Ainsi, une variable ayant une portée locale (assignée dans l’espace local) peut avoir le même nom qu’une variable globale (définie dans l’espace de travail global), sans pour autant désigner le même objet, ou écraser cet objet.

Regardons cela à travers un exemple.

# Définition d'une variable globale :
valeur = 1

# Définition d'une variable locale à la fonction f
def f(x):
  valeur = 2
  nouvelle_valeur = 3
  print("valeur vaut :", valeur)
  print("nouvelle_valeur vaut :", nouvelle_valeur)
  return x + valeur

Appelons la fonction f(), puis regardons la valeur de valeur et celle de nouvelle_valeur après l’exécution de la fonction.

res = f(3)
## valeur vaut : 2
## nouvelle_valeur vaut : 3
print("valeur vaut :", valeur)
## valeur vaut : 1
print("nouvelle_valeur vaut :", nouvelle_valeur)
## NameError: name 'nouvelle_valeur' is not defined
## 
## Detailed traceback: 
##   File "<string>", line 1, in <module>

Comme on peut le constater, durant l’appel, la variable locale du nom valeur valait 2. Cette variable ne faisait pas référence à la variable du même nom définie dans l’environnement global. À l’issue de l’exécution de la fonction f(), cette variable valeur locale est supprimée, et il en est de même pour la variable locale nouvelle_valeur, qui n’existe pas dans l’environnement gloabl (d’où l’erreur retournée).

Sans trop rentrer trop dans les détails, il semble important de connaître quelques principes à propos de la portée des variables. Les variables sont définies dans des environnements, qui sont embriqués les uns dans les autres. Si une variable n’est pas définie dans le corps d’une fonction, Python ira chercher dans un environnement parent.

valeur = 1
def f(x):
  return x + valeur
  
print(f(2))
## 3

Si on définit une fonction à l’intérieur d’une autre fonction, et qu’on appelle une variable non définie dans le corps de cette fonction, Python ira chercher dans l’environnement directement supérieur. S’il ne trouve pas, il ira chercher dans l’environnement encore supérieur, et ainsi de suite jusqu’à l’environnement global.

# La variable valeur n'est pas définie dans
# l'environnement local de g().
# Python va alors chercher dans f().
valeur = 1
def f():
  valeur = 2
  def g(x):
    return x + valeur

  return g(2)

print(f())
## 4
# La variable valeur n'est définie ni dans g() ni dans f()
# mais dans l'environnement supérieur (ici, global)
valeur = 1
def f():
  def g(x):
    return x + valeur

  return g(2)

print(f())
## 3

Si on définit une variable dans le corps d’une fonction et que l’on souhaite qu’elle soit accessible dans l’environnement global, on peut utiliser le mot-clé global :

def f(x):
  global y
  y = x+1

f(3)
print(y)
## 4

La variable que l’on souhaite définir de manière globale depuis un espace local de la fonction ne doit pas avoir le même nom d’un des paramètres.

8.3 Fonctions lambda

Python propose ce que l’on appelle des fonctions lambdas, ou encore des fonctions anonymes. Une fonction lambda ne possède qu’une seule instruction dont le résultat est celui de la fonction.

On les définit à l’aide du mot-clé lambda. La syntaxe est la suivante :

nom_fonction = lambda parametres : retour

Les paramètres sont à séparer par des virugles.

Reprenons la fonction carre() créée précédemment :

def carre(x):
  return x**2

La fonction lambda équivalent s’écrit :

carre_2 = lambda x: x**2
print(carre_2(4))
## 16

Avec plusieurs paramètres, regardons la fonction lambda équivalente à la fonction produduction() :

def production(l, k, m):
  """
  Retourne la valeur de la production en fonction
  du travail, du capital et du matériel.
  
  Keyword arguments:
  l -- travail (float)
  k -- capital (float)
  m -- matériel (float)
  """
  return l**0.3 * k**0.5 * m**(0.2)
production_2 = lambda l,k,m : l**0.3 * k**0.5 * m**(0.2)
print(production(42, 40, 42))
## 40.987803063838406
print(production_2(42, 40, 42))
## 40.987803063838406

8.4 Retour de plusieurs valeurs

Il peut parfois être pratique de retourner plusieurs éléments en retour d’une fonction. Bien que la liste se porte candidate à cette fonctionnalité, il peut-être plus avisé d’utiliser un dictionnaire, pour pouvoir accéder aux valeurs grâce à leur clé !

import statistics
def stat_des(x):
  """Retourne la moyenne et l'écart-type de `x`"""
  return {"moyenne": statistics.mean(x),
  "ecart_type": statistics.stdev(x)}

x = [1,3,2,6,4,1,8,9,3,2]
res = stat_des(x)
print(res)
## {'moyenne': 3.9, 'ecart_type': 2.8460498941515415}
message = "La moyenne vaut {} et l'écart-type vaut {}"
print(message.format(res["moyenne"], res["ecart_type"]))
## La moyenne vaut 3.9 et l'écart-type vaut 2.8460498941515415

8.5 Exercice

  1. Créer une fonction nommée somme_n_entiers qui retourne la somme des \(n\) premiers entiers. Son seul paramètre sera n.
  2. À l’aide d’une boucle, afficher la somme des 2 premiers entiers, puis 3 premiers entiers, puis 4 premiers entiers, etc. jusqu’à 10.
  3. Créer une fonction qui à partir de deux points représentés par des couples de coordonnées (\(x_1\), \(y_1\)) et (\(x_2\), \(y_2\)) retourne la distance euclidienne entre ces deux points. Proposer une seconde solution à l’aide d’une fonction lambda.