---
title: Découvrir les fonctions
author: Steve Kossouho
---

# Découvrir les fonctions

----

## L'utilité des fonctions

Le principe d'une fonction, c'est d'être :

1. Minimaliste si possible,
2. Réutilisable,
3. Un gain de temps et d'espace

----

## Syntaxe de déclaration de fonctions

```python {.numberLines}
def my_simple_function():
  # Affiche un texte à chaque fois qu'on l'exécute
  print("Bonjour, vous m'avez appelé ?")
  print("Je suis une fonction très simple.")


my_simple_function()
```

La syntaxe permet de déclarer la fonction et dire à Python de lui associer du code.

----

## Typographie des fonctions

Les noms de fonctions se choisissent, par **convention** (suivez les conventions), comme toute autre variable :

- Tout en minuscules (ex. `exponential`)
- Mots séparés par des underscores (ex. `get_warning_count`)
- Peuvent contenir des chiffres sauf au début (ex. `log4`)

----

## Référence _versus_ appel de fonction

Lorsque vous déclarez une fonction appelée `action` :

```python {.numberLines}
def action():
    pass
```

`action` est une **variable** de type fonction dont vous pouvez ensuite exécuter le code associé.

- `action` est donc la référence de la fonction.
- `action()`{.python} exécute le code de `action`, et récupère son expression de retour.

----

## Valeurs de retour d'une fonction

En mathématiques, une fonction renvoie toujours une valeur. 
Par exemple, on peut déclarer `∀ x ∈ ℝ, f(x) = x × 15`{.latex}. 
  
En Python, pour qu'une fonction qu'on déclare renvoie une valeur (transmette une valeur au code qui l'exécute) 
lorsqu'on l'exécute, il faut utiliser le mot-clé `return`{.python}.

Lorsque le mot-clé est rencontré par l'interpréteur, il interrompt immédiatement l'exécution de la 
fonction et la valeur est transmise comme résultat de l'appel de la fonction.

```python {.numberLines}
def squared(value):
    # Renvoyer la valeur élevée au carré
    return value ** 2.0
    
result = squared(10)  # on récupère 100
print(result)
```

----

### Valeurs de retour et cas particuliers

Le mot-clé `return`{.python} a quelques comportements implicites :

- Il peut être utilisé tout seul, sans expression : `return`{.python}. C'est équivalent à écrire `return None`{.python}, mais moins explicite.
- Si une fonction se termine sans avoir utilisé le mot-clé `return`{.python} :
  - C'est toujours une fonction valide (on l'a vu dans le premier exemple)
  - Implicitement, l'interpréteur Python retourne également la valeur `None`{.python}.

. . .

En clair, cela signifie qu'une fonction en Python renvoie **toujours** une valeur qui peut être assignée
à une variable, même si la valeur `None`{.python} a souvent peu d'utilité.

----

## Passer quelques arguments aux fonctions

Déclarer une fonction simple, c'est déjà pas mal, mais en déclarer une qui dépend d'un ou de 
plusieurs arguments reçus en entrée, c'est encore plus utile !

Python propose au moins 4 types d'arguments différents, dont les usages sont évidemment différents,
et parmi ceux-ci, deux sont absolument essentiels et nous allons les aborder :

1. Arguments positionnels 
2. Arguments par défaut (avec valeur par défaut)

----

## Arguments : Positionnels

Derrière cet adjectif un peu pompeux se cache le type d'argument le plus simple à déclarer et à utiliser :

```python {.numberLines}
def f(x, y):
    # Accepte deux arguments, affiche leur valeur
    # Mais ne renvoie rien d'autre que `None`
    print(x, y)
    
f(1, 2)  # exécute la fonction, donc affiche "1 2"
```

. . .

Passer des valeurs à ces arguments est **obligatoire** lorsqu'on souhaite exécuter la fonction,
et les valeurs sont passées aux arguments dans le même ordre que dans la signature; 
ici, à la ligne 5, `1`{.python} va dans `x`{.python} et `2`{.python} va dans `y`{.python}.
C'est de ce comportement que vient la notion de **positionnalité**.

----

## Arguments : Valeurs par défaut

```python {.numberLines}
def f(x, alpha=15, beta=16):
    # Accepte trois arguments, dont deux avec une valeur par défaut
    print(x, alpha, beta)
    return (x, alpha, beta)  # renvoie un tuple

f(1)  # ça marche, alpha vaut 15, beta vaut 16
f(46, 10, 12)  # acceptable
f(99, alpha=23)  # conseillé
f(99, 23)  # équivalent à l'exemple du dessus
f(True, beta=23)  # conseillé et obligatoire
f(True, , 23)  # ceci est une erreur de syntaxe, on ne met pas deux virgules de suite en vrac
f(beta=23, x=50)  # passer un arg. positionnel par son nom : déconseillé
```

. . .

Les arguments avec valeur par défaut sont facultatifs; cela signifie que si le développeur les omet 
lorsqu'il exécute la fonction, Python choisira pour ces arguments leur valeur par défaut.

----

### Arguments : ordre

Malheureusement, mais c'est techniquement nécessaire, il y a un ordre imposé par l'interpréteur Python 
lorsque vous déclarez les arguments acceptés par une fonction : 
les arguments positionnels **doivent** être déclarés **en premier** dans la liste des arguments, s'il y en a.

. . .

Tous les arguments, positionnels (obligatoires) et par défaut, peuvent cependant être passés par leur nom,
mais ne faites jamais ça, vous induirez vos relecteurs en erreur :

```python {.numberLines}
def f(x, alpha=15, beta=16):
    return (x, alpha, beta)  # renvoyer un tuple avec x, alpha et beta

# x peut être passé par son nom, non positionnellement, mais c'est déconseillé
print(f(beta=23, x=50))  
```

----

## Extra : Argument "étoile" (séquence)

Il existe un type d'argument, qui n'apparaît qu'une seule fois **maximum**, et dont le nom est précédé d'une étoile dans la signature de la fonction.

Cet argument apparaît après les arguments positionnels, et _de forte préférence_ avant les arguments avec valeur par défaut.
Lors de l'exécution de la fonction, cet argument contient toujours un tuple valide, même vide.
Également, comme cet argument contient toujours un tuple, il est généralement nommé avec un nom **au pluriel**.

```python {.numberLines}
def stretchable(number, *words):
    print(number, words)  # words est toujours un tuple
    
stretchable(15, "word 1", "word 2", "word 3")  # les 3 arguments vont dans le tuple `words`
stretchable(15, True, 3.14159)  # les 2 arguments vont dans le tuple `words`
stretchable(1)  # le tuple `words` sera vide
```

----

### Argument "étoile" : usage

Un argument de ce type accepte, lors de l'appel de la fonction, 
un nombre de valeurs arbitraire (aucune ou plus), et ces valeurs sont passées comme des arguments positionnels 
(sans nom d'argument).

Cette technique est utilisée dans la fonction `print`{.python} pour pouvoir afficher à la suite plusieurs arguments.

```python {.numberLines}
from sys import stdout

def custom_print(*values):
    for value in values:
        stdout.write(value)
        stdout.write(" ")
    stdout.write("\n")  # Échappement pour passer à la ligne

custom_print("Hello", "et", "bienvenue !")
```

----

On peut directement renseigner une liste pour ce type d'argument, en passant, lors de l'appel de fonction, 
une expression de _liste_ (ou _tuple_) précédée par une étoile :

```python {.numberLines}
def function_with_extensible_args(number, *words):
    print(number, words)  # words est toujours une liste


function_with_extensible_args(15, *["word 1", "word 2", "word 3"], "word 4")  # raccourci
```

----

## Extra : Argument "double-étoile" (dictionnaire)

Un dernier type d'argument, apparaît aussi une seule fois **maximum**, et généralement en tout dernier dans les arguments. 
Celui-ci s'utilise en passant des noms d'arguments qui n'existent pas ailleurs dans la signature de la fonction :

```python {.numberLines}
from typing import Any

def function_with_custom_named_args(other: Any = 15, **kwargs):
    print(other, kwargs)  # toujours un dictionnaire


function_with_custom_named_args(plop="Hello", foo=19)  # vont dans le dictionnaire `kwargs`.
function_with_custom_named_args(other="Hello", tornado=19)  # tornado ira dans le dictionnaire `kwargs`.
```

On appelle souvent cet argument l'argument [mots-clés]{.naming}, et s'appelle très fréquemment `kwargs` ou
`options`.

----

### Fonctionnement de l'argument

Si l'on appelle `function_with_custom_named_args`{.python} avec un argument `plop`, Python regarde si la fonction
accepte explicitement un argument de ce nom :

- Si oui, l'argument prendra cette valeur. 
- Si non, l'argument est ajouté comme association dans `kwargs`, de telle façon que `kwargs == {"plop": "Hello"}`{.python}.

```python {.numberLines}
def function_with_custom_named_args(other=15, **kwargs):
    print(other, kwargs)  # toujours un dictionnaire


function_with_custom_named_args(plop="Hello")
```

L'argument est de plus en plus rarement utilisé, sauf par des bibliothèques qui ont besoin d'accepter des noms
calculés dynamiquement pour fonctionner.

----

## Arguments : Ordre de déclaration

Si vous deviez avoir dans la déclaration de vos fonctions tous les types d'arguments que nous avons vus, l'ordre de leur
apparition **devrait** être le suivant :

1. `positionnel` (toujours premier)
2. `étoile` (utilisé positionnellement, toujours second)
3. `par défaut` (utilisé de préférence par nom, de préférence troisième)
4. `double-étoile` (utilisé par nom)

----

## Superbonus : Arguments spéciaux

----

### Arguments spéciaux : Slash

Depuis Python 3.8 (2019), vous pouvez contrôler l'utilisation de vos fonctions en ajoutant dans votre signature un argument
simplement nommé `/`{.python}. Il ne peut exister qu'une fois au maximum.

Lorsque vous spécifiez cet argument dans votre signature, **tous les arguments qui le précèdent ne 
peuvent pas être spécifiés autrement que positionnellement** :

```python {.numberLines}
def my_function(a, b, /, c, d):
    print(a, b, c, d)

my_function(1, 2, 3, 4)  # autorisé
my_function(1, 2, c=3, d=4)  # autorisé
my_function(1, b=2, c=3, d=4)  # impossible, b est positionnel uniquement
```

Lorsqu'un argument `/`{.python} est présent, les arguments `*args`{.python} sont interdits. La raison est que, si un tel
argument est présent, on est automatiquement tenu de passer les arguments qui suivent par leur nom.

----

### Arguments spéciaux : Étoile

Vous pouvez contrôler l'utilisation de vos fonctions en ajoutant dans votre signature un argument
simplement marqué `*`{.python}. Il ne peut exister qu'une fois au maximum, et se trouver après l'argument
`/`{.python}.

Lorsque vous spécifiez cet argument dans votre signature, **tous les arguments qui le suivent ne 
peuvent être spécifiés autrement qu'en précisant leur nom** :

```python {.numberLines}
def my_function(a, b, c=3, *, d=4):
    print(a, b, c, d)

my_function(1, 2, 3, d=4)  # autorisé
my_function(1, 2, 3, 4)  # impossible, d ne peut pas être utilisé positionnellement
```
