3.6. Fonctions

3.6.1. Généralités

3.6.1.1. Définition

Une fonction se définit à l’aide du mot-clé def suivi d’un identifiant puis de parenthèses. Ces dernières contiennent la définition des différents arguments que la fonction peut recevoir : rappelons qu’un argument permet de modifier le comportement d’une fonction en lui fournissant des données supplémentaires, qui permettent ainsi un comportement différent en fonction du contexte. Enfin, la ligne contenant l’instruction def se termine par deux points et est suivie d’un bloc d’instructions(indentées). On aura donc la forme suivante :

def name(args):
    instructions

Une fonction peut retourner une valeur grâce au mot-clé return (dans certains langages, on appelle procédure une fonction ne retournant rien, mais la distinction n’est pas importante ici). Il faut noter que le mot-clé return interrompt l’exécution de la fonction, c’est à dire que toute instruction située après sera ignorée (sauf dans un cas que nous verrons dans le chapitre Erreurs et exceptions).

Note

En réalité, une fonction retournera toujours une valeur, qui sera None si aucun return n’est présent.

Voici quelques exemples de définitions de fonctions :

>>> def func():
...     return 1
>>> func()
1
>>> def square(n):
...     m = n**2
...     return m
>>> square(3)
9
>>> def nothing():
...     pass
>>> print(nothing())
None

La première fonction n’est pas très utile puisqu’elle est équivalente à entrer directement 1. Il aurait été possible de simplifier la deuxième fonction en écrivant directement return n**2.

3.6.1.2. Valeurs de retour

Il est possible de retourner n’importe quel type d’objet grâce à return.

>>> def return_obj(obj):
...     return obj
>>> type(return_obj(1))
<class 'int'>
>>> type(return_obj('string'))
<class 'str'>
>>> type(return_obj([1, 2, 3]))
<class 'list'>
>>> return_obj([1, 2, 3])
[1, 2, 3]

S’il n’est pas possible d’utiliser plusieurs return pour retourner plusieurs valeurs, il est possible de retourner plusieurs objets par un même return, soit explicitement avec un objet de notre choix (tuple, liste, dictionnaire), soit implicitement (ce sera alors un tuple).

>>> def tds(n):
...     return n*3, n/2, n**2
>>> tds(4)
(12, 2.0, 16)

3.6.1.3. Docstrings

Il convient d’associer à chaque fonction une docstring, c’est à dire une chaine de documentation. Celle-ci permet de donner diverses informations sur la fonction. Les docstrings peuvent être utilisées par certains générateurs de documentation pour créer automatiquement la documentation d’un programme.

Une docstring doit respecter quelques conventions :

  • la première ligne consiste en une courte description de la fonction (moins d’une ligne), et elle doit débuter par une majuscule et finir par un point ;
  • la deuxième ligne doit être vide ;
  • les lignes suivantes décrivent d’une manière plus complète la fonction (comment l’appeler, les objets retournés, etc), éventuellement en utilisant des marquages pour les générateurs de documentations.

Seule la première ligne n’est pas optionnelle. Il est possible d’accéder au docstring d’une fonction grâce à l’attribut __doc__.

def func():
    ''' Do nothing.

    This function does not make anything.
    '''

    pass

L’on peut alors afficher la documentation avec print(func.__doc__).

3.6.1.4. Remarques

Rappelons que les variables dans une fonction sont locales (et donc supprimées par le ramasse-miettes de Python dès que l’interpréteur sort de la fonction).

>>> def func():
...     a = 1
>>> func()
>>> a
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
NameError: name 'a' is not defined

Une fonction est un objet appellable. Il est donc nécessaire d’utiliser des parenthèses même si elle ne prend aucun argument. Dans le cas contraire, l’on aurait simplement accès à l’objet fonction lui-même, et le code à l’intérieur ne serait pas exécuté.

>>> def re1():
...     return 1
>>> re1
<function re1 at ...>
>>> re1()
1

Au-delà d’une simple remarque de forme, réaliser que les fonctions sont des objets permet certaines combinaisons intéressantes. Par exemple, imaginons que je dispose trois fonctions dans un tuple, alors je peux accéder à celle que je souhaite simplement à partir de l’index :

>>> l = [9, 2, 5]
>>> t = (min, max, sum)
>>> t[0](l)
2
>>> t[2](l)
16

Ici l’intérêt est minime car le code perd en clarté et il aurait été plus simple d’écrire directement min(l) puis sum(l). Un exercice à la fin du chapitre propose une application plus concrète de ce mécanisme.

Enfin, il est fortement déconseillé d’afficher du texte (via print() par exemple) directement dans le corps d’une fonction (du moins, pour une autre raison que du débuggage), car cela peut conduire à des effets indésirables.

3.6.2. Arguments

3.6.2.1. Général

Il est possible de créer une fonction avec autant d’argument que l’on souhaite. Dans l’exemple qui suit, nous utilisons trois arguments, mais l’on aurait très bien pu en utiliser deux comme cinq ou vingt, ou même aucun.

>>> def add(a, b, c):
...     return a + b + c
>>> add(1, 2, 3)
6
>>> add(2, -4, 7)
5

Il est toutefois important de signaler que, lorsque l’on appelle la fonction, les arguments doivent être dans l’ordre où ils apparaissent dans la définition.

>>> def test(string, integer):
...     print('The string is '+ string +' and the integer is '+ str(integer))
>>> test('abc', 1)
The string is abc and the integer is 1
>>> test(1, 'abc')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 1, in test
TypeError: Can't convert 'int' object to str implicitly

Ici, une exception est levée car, en théorie, il faut fournir en premier une chaine puis un nombre. Or l’on fait l’inverse, ce pour quoi le programme n’a pas été prévu à la base.

3.6.2.2. Arguments par défaut

Dans la définition de la fonction, il est possible de donner une valeur par défaut à certains arguments, de sorte que l’on ait pas à fournir tous les arguments lorsque l’on appelle la fonction. La valeur par défaut ne sera utilisée que si aucune valeur n’est fournie pour l’argument en question. Une valeur par défaut se définit par argument=valeur.

Warning

Il est nécessaire de placer les arguments ayant une valeur par défaut en dernier, sans quoi une exception SyntaxError sera levée.

>>> def add(n, m=1):
...     return n + m
>>> add(2, 5)
7
>>> add(2)
3

L’on peut utiliser une variable pour indiquer la valeur par défaut d’un argument. Toutefois la valeur de la variable utilisée est celle qu’elle avait au moment de la définition de la fonction.

>>> i = 5
>>> def f(arg=i):
...     return arg
>>> f()
5
>>> i = 6
>>> f()
5

Warning

Il est fortement déconseillé d’utiliser un objet mutable comme valeur par défaut, car, comme nous l’avons vu au-dessus, la valeur par défaut n’est évaluée qu’une seule fois.

>>> def f(a, L=[]):
...     L.append(a)
...     return L
>>> for i in range(3):
...     print(f(i))
[0]
[0, 1]
[0, 1, 2]

Note

Il est fortement conseillé de toujours mettre une valeur par défaut à tous les arguments d’une fonction, a fortiori lorsqu’ils ne sont pas forcément nécessaires.

3.6.2.3. Arguments nommés

Lors de l’appel d’une fonction il est possible d’attribuer explicitement la valeur d’un argument (cela fonctionne même si nous n’avons pas attribué de valeur par défaut) en écrivant argument=valeur.

Il faut aussi faire attention à ne pas fournir deux (ou plus) valeurs pour un même argument.

>>> def test(string, integer=2):
...     print('string is '+ string +' and integer is '+ str(integer))
>>> test('abc', 1)
string is abc and integer is 1
>>> test('abc', integer=1)
string is abc and integer is 1
>>> test(string='abc', integer=1)
string is abc and integer is 1
>>> test(integer=1, string='abc')
string is abc and integer is 1
>>> test(string='abc')
string is abc and integer is 2

Toutefois, au même titre que l’attribution d’une valeur par défaut, il est nécessaire de placer les arguments nommés à la fin.

3.6.2.4. Capture d’arguments non définis

Il existe deux sortes d’arguments spéciaux qui permettent de “capturer” les arguments fournis par l’utilisateur mais qui ne sont pas définis dans la fonction :

  • **kwargs, qui sera un dictionnaire des arguments nommés (où l’argument sera la clé) ;
  • *args, qui sera un tuple des autres arguments.

Cela permet donc d’obtenir une liste d’arguments de taille variable, et non définie à l’avance, ce qui peut être un gage de souplesse. *args doit absolument être placé avant **kwargs.

>>> def func(n, *args, **kwargs):
...     return(n, args, kwargs)
>>> func(1, 2, 3, 4, a=5, b=6)
(1, (2, 3, 4), {'a': 5, 'b': 6})

Il est bien sûr possible de manipuler les variables dans le corps de la fonction comme un tuple et un dictionnaire normaux (puisque c’est ce qu’ils sont). Toutefois il faut prendre garde à ne pas utiliser les astériques en dehors de la définition.

Note

Les noms kwargs et args sont utilisés par conventions, mais ils peuvent être nommés n’importe comment. Ce qui compte est le nombre d’astérisques devant le nom.

Finalement, au lieu de transmettre les valeurs une par une à une fonction, nous pouvons transmettre en argument une séquence (ordonnée) ou un dictionnaire dont la taille et le type des objets contenus correspondent exactement à ceux attendus par la fonction : dans ce cas, la variable doit être précédée d’une (séquence) ou deux (dictionnaire) étoiles *. Le terme en anglais pour cette opération est unpack (dépaquetter) [1].

>>> def add(a, b, c):
...     return a + b + c
>>> t = (1, 2, 3)
>>> add(*t)
6
>>> d = {'a': 1, 'b': 2, 'c': 3}
>>> add(**d)
6

Il faut tenir compte de l’ordre dans les séquences, mais pas dans les dictionnaires puisque ces derniers utilisent le nom des arguments pour les attribuer correctement.

Sans l’astérisque, nous aurions eu une exception TypeError car nous n’aurions pas fourni tous les arguments nécessaires :

>>> add(t)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: add() takes exactly 3 arguments (1 given)

3.6.3. Fonctions récursives

Une fonction récursive est une fonction qui s’appelle elle-même. Définissons par exemple une fonction calculant la factorielle d’un nombre.

>>> def fact(n):
...     if n == 0:
...         return 1
...     else:
...         return n * fact(n-1)
>>> fact(4)
24

La récursivité sera abordée en détails plus loin.

3.6.4. Fonctions anonymes

Il s’agit d’une fonctionnalité héritée de langages de programmation fonctionnelle. Le mot-clé lambda permet de créer une fonction anonyme, c’est à dire une fonction employée directement là où l’on en a besoin, au lieu de la définir auparavant. Une expression lambda peut donc être utilisée n’importe où un objet fonction devrait être requis en temps normal.

>>> (lambda a: a**2)(2)
4
>>> f = lambda a: a**2
>>> f(2)
4
>>> def power(n):
...     return lambda m: m**n
>>> f = power(3)
>>> f(2)
8
>>> power(3)(2)
8

Il est bien entendu possible de définir une expression lambda utilisant plusieurs arguments.

>>> f = lambda a, b: a + b
>>> f(1, 2)
3

Remarquons qu’une seule expression peut être définie dans une fonction lambda, et que le return est implicite (le rajouter lève une exception SyntaxError).

Note

Les fonctions lambda peuvent compliquer la lecture du code, et il est conseillé pour cette raison de ne pas en utiliser trop. De même, dès qu’une même fonction lambda est utilisée à plusieurs endroits, il faut songer sérieusement à définir une fonction normale.

3.6.5. Generators

Un générateur peut être vu comme une fonction dynamique qui va générer une séquence de valeurs. Une telle fonction est obtenue en utilisant le mot-clé yield au lieu de return pour retourner des valeurs. Dans ce cas, l’appel à la fonction retournera un objet generator, dont nous pourrons extraire les valeurs à l’aide de différentes fonctions : par exemple, chaque appel de next() (re)lance l’exécution de la fonction, puis l’arrête dès que le mot-clé yield est rencontré, et retourne finalement la valeur donnée par yield. En résumé :

  • on appelle la fonction, qui retourne un objet générateur ;
  • le premier appelle de next() lance l’exécution de la fonction ;
  • au premier yield rencontré, l’exécution s’arrête et un objet est retourné ;
  • on appelle à nouveau next(), et la fonction reprend son exécution là où elle avait été stoppée par yield ;
  • l’exécution s’arrête à nouveau dès qu’un yield est rencontré ;
  • et ainsi de suite.

Pour commencer, prenons un exemple de générateur simple :

>>> def gen():
...     yield 1
...     yield 2
...     yield 3
>>> g = gen()
>>> g
<generator object gen at ...>
>>> next(g)
1
>>> next(g)
2
>>> next(g)
3
>>> next(g)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration

L’on observe qu’une exception StopIteration est levée dès que le générateur est “épuisé”. Ceci permet de contrôler ce qui se passe dès qu’il ne peut plus retourner d’objet.

Les générateurs peuvent se comporter comme des itérateurs, c’est à dire qu’une boucle for permet d’itérer sur les objets qu’il contient. À ce moment-là, la boucle for appelle implicitement la fonction next() à chaque itération, et elle se termine dès que l’exception StopIteration est levée.

Comme exemple nous allons définir la suite de Fibonacci, dont la fonction retourne les n premiers termes :

>>> def fibo(n):
...     a, b = 0, 1
...     while b < n:
...         yield b
...         a, b = b, a + b
>>> for i in fibo(6): print(i)
1
1
2
3
5
>>> g = fibo(3)
>>> next(g)
1
>>> sum(fibo(6))
12

3.6.6. Fonctions built-in

Quelques fonctions n’appartenant à aucun module sont définies sans qu’il ne soit nécessaire des les importer (se référer au chapitre Modules pour plus d’informations). On les appelle fonctions built-in (que l’on pourrait traduire par “intégrée”, mais le terme anglais restera utilisé). Elles son généralement souvent utilisées, d’où leur disponibilité immédiate.

Ici sera dressée une liste des fonctions les plus importantes, la découverte des autres étant laissée en exercice.

dir([object])

Retourne une liste des toutes les méthodes et attributs de object s’il est fourni, sinon retourne une liste de tous les objects du contexte courant. Cette fonction est essentiellement utilisée dans l’interpréteur interactif pour tester les objets.

eval(expression)

Cette fonction analyse et exécute expression (qui doit être une chaine).

>>> x = 1
>>> eval('x + 2')
3
globals()

Retourne un dictionnaire représentant les variables présente dans le contexte global.

hasttr(object, attribut)

Retourne True si l’objet possède l’attribut en question.

help([object])

Affiche l’aide au sujet d’un objet si object est fourni, sinon affiche l’aide générale. Cette fonction est destinée à être utilisée dans l’interpréteur interactif.

input([prompt])

Si le paramètre prompt est fourni, alors il sera affiché sur la sortie standard. Dans tous les cas, la fonction lit une ligne écrite (dont saisie est interrompue par la touche entrée) et la retourne sous forme de chaine.

>>> name = input('Enter your name: ')
Enter your name: Harold
>>> print(name)
Harold
isinstance(object, class)

Retourne True si object est une instance de class. class peut aussi être un tuple contenant plusieurs classes, et la fonction retournera True si object est une instance de l’un d’entre eux. Il s’agit de LA manière la plus correcte de vérifier le type d’un objet.

>>> if isinstance(1, int):
...     print('1 is an instance of int.')
1 is an instance of int.
>>> if isinstance(1, (int, str)):
...     print('1 is an instance of int or str.')
1 is an instance of int or str.
>>> if isinstance("1", (int, str)):
...     print('"1" is an instance of int or str.')
"1" is an instance of int or str.
len(seq)

Retourne la longueur de la séquence seq.

locals()

Fait de même que globals(), mais avec le contexte local.

print([obj1, ..., objn][, sep=' '][, end='n'])

Affiche obj1 jusque objn, séparés par sep, en ajoutant end à la fin. sep et end doivent être des chaines.

range([start=0], stop[, step=1])

Retourne un objet range, qui est un objet itérable équivalent à une liste d’entiers, commençant à start et finissant à end - 1, et séparés par le pas sep.

>>> list(range(3))
[0, 1, 2]
>>> list(range(2, 5))
[2, 3, 4]
>>> list(range(1, 9, 2))
[1, 3, 5, 7]
>>> list(range(0, -3, -1))
[0, -1, -2]
repr(obj)

Affiche la représentation (définie par la méthode __repr__() de obj.

reversed(seq)

Retourne l’inverse de la séquence passée en argument. Ceci est équivalent à seq.reverse(), exceptée que cette fonction retourne un nouvel objet, et peut donc être utilisée dans une boucle for.

>>> for i in reversed(range(3)):
...     print(i)
2
1
0
round(x[, n=0])

Arrondit x à n décimales.

sorted(seq)

Retourne une séquence triée à partir de seq. Cette fonction est équivalent à appeler la méthode seq.sort(), excepté le fait qu’elle retourne un nouvel objet (comme reversed()).

sum(iterable)

Permet de sommer un itérable contenant uniquement des nombres (de n’importe quels types, qui peuvent être mélangés).

Quelques exemples d’utilisations :

>>> sum([1+4j, 3.3, -2])
(2.3+4j)

3.6.7. Exercices

  1. Écrire une fonction qui permet de convertir des francs en euros.

    Pour rappel : 1€ = 6.55957F.

  2. Écrire une fonction qui calcule le périmètre d’un cercle de rayon R (qui sera donc l’argument).

    Pour rappel : le périmètre est P = 2×π×R. On pourra prendre π = 3.14 ou, mieux, chercher un module qui contient ce nombre et l’utiliser.

  3. Écrire une fonction qui prend en argument un nombre n et qui calcule n! (n factorielle), sans utiliser la récursivité.

    Rappel : n! correspond à la multiplication des n premiers entiers avec la convention que 0! = 1. Ainsi, par exemple, 4! = 1×2×3×4 = 24.

  4. Écrire une fonction qui prend deux arguments, l’un d’eux étant une fonction. La première fonction doit appliquer la fonction passée en argument au second argument, et renvoyer le résultat.

  5. Écrire une fonction qui prend une date de naissance en argument et qui retourne le nombre de jours écoulés depuis.

    On pensera aux années bissextiles ; une année est bissextile si :

    • elle est divible par 4 mais par par 100 ;
    • elle est divisible par 400.

    On pourra utiliser le module datetime pour obtenir la date du jour courant automatiquement.

  6. Écrire une fonction similaire à range(), mais qui accepte des flottants en argument.

Footnotes

[1]Nous reviendrons sur le dépaquettage dans le chapitre sur l’utilisation avancée des types.