Fonctions

Une fonction est un objet qui permet d’effectuer des opérations en fonction de données qu’on lui transmet. L’intérêt premier d’une fonction est de permettre d’appliquer un même algorithme à différentes données. Une fonction permet en outre d’isoler un code de manière à le rendre indépendant de l’espace de noms courant. Les fonctions peuvent être définies dans un fichier à part, ce qui permet de constituer un module.

Définition d’une fonction

La définition d’une fonction se fait avec le mot clé def, suivi du nom de la fonction, de la liste des paramètres entre parenthèses, suivie du symbole : (deux points). Le code de la fonction est décalé d’une indentation par rapport au mot def.

Voici une fonction qui prend un nombre en argument et renvoie ce nombre multiplié par deux :

1
2
3
def double(x):
    y = x*2
    return y

La variable nommée x figurant dans l’entête de la fonction est un paramètre, permettant de transmettre un argument au code de la fonction. En principe, le paramètre désigne la variable (c.a.d. le nom) alors que l’argument désigne l’objet effectivement transmis lors de l’appel de la fonction. Voici comment utiliser cette fonction :

>>> f(5)
10

Dans ce cas, l’argument transmis à la fonction est un entier de valeur 5, qui est créé au moment de l’appel. Comme on le voit sur cet exemple, le mot-clé return permet à la fonction de renvoyer un résultat.

Le nom de la fonction et la liste de ses paramètres constituent la signature de la fonction. Les seules informations apportées par la signature sont le nom de la fonction et les noms de ses paramètres (et bien sûr leur nombre), mais aucune information n’est apportée ni sur les types des arguments, ni sur le type de l’objet renvoyé par return.

Il faut noter qu’une fonction est un objet (au même titre qu’un entier ou une liste par exemple) :

>>> type(double)
<class 'function'>

Le nom de la fonction est un nom de variable. Il est donc possible de définir une autre variable faisant référence à la même fonction :

1
d = double
>>> d(5)
10

Il s’en suit qu’une fonction peut être transmise en argument d’une autre fonction.

Variables locales et paramètres

Le bloc de code qui se trouve dans la fonction utilise un espace de noms (pour les variables) indépendant de l’espace de noms du code qui appelle la fonction. La variable y qui est utilisée dans la fonction est une variable locale, c’est-à-dire une variable définie dans l’espace de noms local à la fonction. Cette variable disparaît lorsque la fonction se termine. On dit aussi que la portée de la variable se limite à la fonction. Si le code qui appelle la fonction possède une variable de même nom, il n’y a pas interférence entre les deux :

>>> y = 0
>>> f(5)
10
>>> y
0

L’unique paramètre de la fonction a le nom x. Ce nom devient automatiquement le nom d’une variable locale dans la fonction. Le mot clé return permet à la fonction de renvoyer un objet. Si la fonction se termine sans rencontrer return, l’objet None est renvoyé. Lors de l’appel de la fonction ci-dessus, un entier de valeur 5 est créé et une variable locale nommée x est créée, faisant référence à cet entier. Il est tout à fait possible de modifier cette variable locale :

1
2
3
def double(x):
    x = x*2
    return x
>>> x = 10
>>> double(x)
20
>>> x
10

On constate que l’appel de la fonction ne modifie par l’objet référencé par la variable globale x. Au moment où elle est créée, la variable locale x fait bien référence au même objet que la variable globale x. Cependant, la ligne 2 de la fonction a pour effet de créer un nouvel objet de type int (car ce type d’objet est non mutable), contenant le résultat de l’opération de multiplication. Il s’en suit que l’objet initial n’est pas modifié.

Il en est tout autrement lorsque l’objet passé en argument est un objet mutable, par exemple une liste :

1
2
3
def changer_element(L,i,e):
    L[i] = e
    return(L)
>>> X = [1,2]
>>> changer_element(X,i,0)
>>> X
[0,2]

Dans ce cas, la liste référencée par la variable globale X devient référencée par la variable locale L, mais la ligne 2 modifie le contenu de la liste. La liste référencée par la variable globale X est donc bien modifiée. Cette différence de comportement est due au fait que les listes sont des objets mutables alors que les entiers ne le sont pas. L’effet précédent (la modification de la liste passée en argument) peut être recherché mais, si on souhaite absolument l’éviter, il est prudent de convertir la liste en n-uplet avant de la transmettre.

Voici une fonction qui a deux paramètres, permettant de lui transmettre deux arguments :

1
2
def puissance(x,n):
    return x**n

En Python, les variables ne sont pas typées. En particulier, les variables locales définies comme paramètres de la fonction ne sont pas typées. Cela signifie que les types des objets auxquels ces variables font référence sont a priori quelconque, ce qui permet une grande souplesse dans l’utilisation de la fonction :

>>> puissance(2,2) # les arguments sont des entiers
4
>>> puissance(2.5,2) # le premier argument est un flottant, l'autre un entier
6.25

Il peut arriver que le type d’un objet transmis en argument ne soit pas un type prévu par le concepteur de la fonction, par exemple :

>>> puissance("2",2)
Traceback (most recent call last):
  File "<pyshell#371>", line 1, in <module>
    puissance("2",2)
  File "<pyshell#370>", line 2, in puissance
    return x**n
TypeError: unsupported operand type(s) for ** or pow(): 'str' and 'int'

On voit sur cet exemple l’inconvénient des variables de paramètre non typées : le type des variables n’est pas vérifié par l’interpréteur. Dans le meilleur des cas, cela conduit à une erreur qui se déclenche lorsqu’on tente une opération non permise avec ce type, ce qui interrompt le cours d’exécution (comme ci-dessus) mais dans d’autres cas, le problème peut passer inaperçu et provoquer une erreur à un autre endroit du code, ou même un résultat faux mais sans erreur.

Il est toujours possible d’ajouter une vérification du type de l’objet :

1
2
3
4
def puissance(x,n):
    if (type(x)!=float and type(x)!=int) or (type(n)!=float and type(n)!=int) :
        return None
    return x**n

Certains paramètres peuvent avoir une valeur par défaut, mais ils doivent être placés après les autres paramètres :

1
2
3
def volume_gaz_parfait(P,T,n=1):
    R = 8.31
    return n*R*T/P

Dans ce cas, on peut faire les deux appels suivants :

>>> volume_gaz_parfait(1e5,298,2)
>>> volume_gaz_parfait(1e5,298) # n a la valeur par défaut (=1)

Il est possible d’utiliser une variable globale à l’intérieur d’une fonction :

1
2
3
4
R = 8.31

def volume_gaz_parfait(P,T,n=1):
    return n*R*T/P

Cette pratique est cependant déconseillée car elle rend la fonction inutilisable en dehors du contexte particulier où elle figure. Il est même possible de définir une variable globale au sein d’une fonction, en la faisant précéder du mot clé global, mais cette possibilité est à proscrire lors de la conception de la fonction.

Objet renvoyé

Comme déjà mentionné, le mot-clé return permet à la fonction de renvoyer un objet au code qui fait l’appel de la fonction. Les paramètres constituent ainsi les entrées de la fonction alors que l’objet renvoyé constitue la sortie. Plus précisément, lorsque l’appel de la fonction avec des arguments est rencontré dans une expression, le code de la fonction est exécuté puis l’objet renvoyé prend la place de l’appel dans l’expression.

Il faut noter que l’instruction return interrompt l’exécution de la fonction. Dans le cas de retours conditionnels, il peut donc y avoir plusieurs return dans la fonction, comme dans l’exemple de la fonction puissance ci-dessus.

Si la fin du bloc de code de la fonction se termine sans return, alors l’objet None est renvoyé.

Il est bien sûr possible de renvoyer un n-uplet, ce que l’on fait lorsqu’on a besoin de renvoyer plusieurs objets, par exemple plusieurs nombres (rappelons que le n-uplet est lui-même un objet). Voici un exemple :

1
2
3
4
def barycentre(x1,x2,y1,y2):
    xG = (x1+x2)/2
    yG = (y1+y2)/2
    return xG,yG

L’appel de la fonction donne un n-uplet :

>>> barycentre(0,0,1,2)
(0.0, 1.5)

Il est possible de définir deux variables à partir de ce n-uplet :

>>> x,y = barycentre(0,0,1,2)

Annotations

Comme nous l’avons déjà mentionné, les paramètres d’une fonction et son objet renvoyé sont non typés. En conséquence, le type d’objet transmis en argument est quelconque. Par exemple, la fonction suivante :

def type_objet(objet):
    print(type(objet))

fonctionne pour tout type d’objet transmis en argument. La fonction suivante :

1
2
def element(L,i):
    return L[i]

est sensée fonctionner avec une liste transmise en premier argument et un entier en second. L’appel suivant :

>>> element(10,1)

déclenche une erreur lors de l’exécution de la ligne 2 car un entier n’est pas indexable. Pour clarifier le code, on peut ajouter des annotations, qui permettent de connaître les types des paramètres et de la valeur renvoyée :

1
2
def element(L:list,i:int):->float
    return L[i]

Les annotations permettent à l’utilisateur de connaître les types attendus par simple examen de la signature de la fonction, ou bien de la manière suivante :

>>> print(element.__annotations__)
{'L': <class 'list'>, 'i': <class 'int'>, 'return': <class 'float'>}

Cependant, ces informations ne sont destinées qu’à l’utilisateur. L’interpréteur n’effectue aucune vérification des types des données (il en est bien incapable avant l’exécution). Par exemple, l’appel suivant ne déclenche aucune erreur :

>>> element(['a','b','c'],1)
'b'

alors que l’objet renvoyé est une chaîne de caractères et non pas un flottant comme indiqué dans l’annotation.