# Bases de Python - Partie 3 - Fonctions

## Fonctions

Vous connaissez déjà certaines fonctions, celles que vous avez utilisées lors du précédent TP : `print`, `input`, ... 
Il en existe un très grand nombre, mais il est aussi très courant que les développeurs doivent écrire leurs propres fonctions.

Qu'est-ce qu'une fonction ?

```admonish success title="Définition"
Une fonction est un sous-programme (c'est à dire une suite d'instructions) permettant d'effectuer des actions que l'on souhaite répéter.
- Elle possède un nom par lequel on peut l'appeler
- Elle peut prendre des paramètres en entrée
- Elle peut renvoyer des données en sortie
```

Autrement dit, une fonction exécute un ensemble d'actions pré-enregistrées, auxquelles on a donné un nom pour pouvoir réaliser ces actions plus facilement, sans avoir à toujours les répéter. On peut lui passer des informations nécessaires à son exécution, et elle peut nous renvoyer un résultat.

### Appeler une fonction

Appeler une fonction, c'est lui demander de faire la tâche pour laquelle elle a été conçue : exécuter les instructions qui la composent.

Les fonctions s'appellent avec la syntaxe suivante :

```python
nom_de_la_fonction(parametre_1, parametre_2, ...., parametre_n)
```

- Vous mettez le nom de la fonction, suivi de parenthèses
- Ce qui se trouve entre les parenthèses s'appelle les **paramètres** parfois appelés **arguments**
- On sépare les paramètres par une virgule
- Si une fonction n'a pas besoin de paramètres, les parenthèses sont quand même indispensables.

#### Paramètres

Les paramètres sont des données que l'ont fournit à la fonction pour qu'elle puisse adapter son résultat à notre besoin. Ils sont généralement d'un ou plusieurs types définis, qui dépendent de la fonction utilisée.

```admonish example title="Exemple"
La fonction `print` affiche une chaîne de caractères. On lui passe donc la chaîne que l'on veut afficher en paramètre, de type `str` :
~~~python
# Pour afficher "Hello, World!", on passe en paramètre "Hello, World!" à print
print("Hello, World!")
~~~
```

#### Paramètres nommés

Certaines fonctions peuvent prendre des paramètres particuliers, nommés. C'est à dire qu'au lieu de se baser sur l'ordre des paramètres, on va écrire leur nom.

Cela ressemblera à ça :
```python
nom_de_la_fonction(nom_parametre=valeur_parametre)
```

```admonish example title="Exemple"
La fonction `print` sépare ses arguments avec des espaces. Si on donne une valeur au paramètre `sep`, on peut changer le caractère que `print` utilisera pour séparer les chaînes de caractères
~~~python
# Par défaut, print sépare les chaînes de caractères avec des espaces
>>> print("foo", "bar")
foo bar

# Mais si on met sep='|', c'est le caractère '|' qui séparera les chaînes de caractères
>>> print("foo", "bar", sep="|")
foo|bar
~~~
```

#### Retourner une valeur

Une fonction peut renvoyer un résultat, que l'on peut alors affecter à une variable avec la syntaxe suivante :
 ```python
ma_variable = ma_fonction()
```

```admonish example title="Exemple"
Si la fonction `add` fait la somme des nombres qu'on lui passe en paramètre :
~~~python
ma_variable = add(1, 3) # add fait 1 + 3 et retourne le résultat dans ma_variable
# ma_variable vaut alors 4
~~~
```

```admonish tip title="Remarque"
Si une fonction ne renvoie rien, alors elle renverra en réalité la valeur spéciale `None`. 

`None` est considérée comme `False` dans une condition
```

### Créer nos propres fonctions

On va pouvoir regrouper des instructions que l'on souhaite répéter et créer des fonctions pour appeler toutes ces instructions avec un simple nom.
Les fonctions servent à rendre votre code plus clair et mieux découpé. Elles permettent de réutiliser du code.

Pour déclarer une fonction en python :
```python
def nom_de_la_fonction(parametre_1, parametre_2, ..., parametre_n):
    # Ici on mettra les instructions indentées (décalées) 
```
- On utilise le mot-clé `def` suivi du nom de la fonction. 
- Entre parenthèses on ajoute les paramètres qu'il faudra **fournir**
- À la fin de la ligne on met `:` (encore et toujours)
- On passe à la ligne suivante et **on indente** pour écrire les instructions.

```admonish warning title="Attention"
Deux fonctions ne peuvent pas avoir le même nom, sinon, la dernière a avoir été déclarée écrase la précédente.
```

#### Les paramètres
A l'intérieur d'une fonction, les paramètres fonctionnent comme des variables : on peut les utiliser avec leur nom.

```admonish example title="Exemple"
On veut écrire une fonction `ma_fonction` qui affiche deux nombres, puis on l'appelle avec les nombres 2 et 5
~~~python
def add(a, b):
    print(a)
    print(b)

add(2, 5)
# Résultat affiché :
# 2
# 5
~~~
```

##### Valeurs par défaut

Les paramètres peuvent être optionnels. Pour cela, il faut qu'ils aient une **valeur par défaut**. 

La valeur par défaut est la valeur que prend le paramètre si on ne lui en fournit aucune autre.

Pour ajouter une valeur par défaut à un paramètre, on utilise la syntaxe suivante :

```python
def ma_fonction(parametre=valeur_par_defaut): # valeur_par_defaut peut être de n'importe quel type : on peut mettre une chaine de caractères, un entier, un flottant, ...
    # Mon code ici
```


```admonish example title="Exemple"
~~~python
def ma_fonction(parametre="valeur par défaut"):
    print(parametre)

ma_fonction()
# Résultat affiché : valeur par défaut
ma_fonction(parametre="Autre valeur")
# Résultat affiché : Autre valeur

## Autres exemples
def ma_fonction(parametre=3):
    # Mon code ici

def ma_fonction(parametre=True):
    # Mon code ici

~~~
```

#### L'instruction `return`

L'instruction `return` permet deux choses :
- Sortir immédiatement de la fonction
- Si on lui fournit une valeur, la fonction **renvoie** cette valeur (sinon, elle renvoie la valeur spéciale `None`)

```admonish example title="Exemple"
La fonction `add` additionne les deux nombres passé en paramètre
~~~python
def add(a, b):
    somme = a + b
    return somme
~~~
```

```admonish warning title="Important"
Le code se trouvant **après** l'instruction `return` dans une fonction ne sera jamais exécuté
```

#### Bonnes pratiques

- Faites des fonctions courtes qui vont à l'essentiel (20 lignes est un bon maximum)
- Choisissez bien les noms de vos fonctions et de vos paramètres pour qu'ils soient clairs et explicites
- Faites en sorte que vos fonctions fassent une seule tâche (qui peut nécessiter plusieurs instructions, bien évidemment !)
- Vous pouvez documenter vos fonctions à l'aide d'une **docstring** : après l'en-tête de la fonction, avant le bloc d'instructions vous pouvez écrire une chaîne de caractères qui commence par 3 `"` ou `'`. 
    Exemple:
    ```python
    def ma_fonction(arg_1, arg2):
        """Ceci est une docstring simple pour documenter ma fonction."""
    ```

    ```python
    def ma_fonction(arg_1, arg2):
        """Ceci est une docstring au format Numpy qui vient documenter ma fonction.

        Une docstring peut prendre plusieurs lignes. Une bonne docstring vient expliquer les arguments
        et ce que renvoie la fonction.

        Parameters
        ----------
        arg_1 : Any
            Argument d'exemple
        arg_2 : Any
            Autre argument d'exemple

        Returns
        -------
        bool
            Retourne un booléen pour l'exemple
        """
        # Bloc d'instructions
    ```

## Modules

Il existe de nombreuses fonctions fournies avec Python, de base. Pourtant, il est tout de même possible de rajouter de très nombreuses 
fonctionnalités à nos programmes en important des modules.

### Qu'est-ce qu'un module ?

Un module, c'est du code que l'on a écrit dans un fichier. En général, on rassemble au même endroit 
des fonctions, variables, constantes etc.. ayant un rapport entre elles dans un seul et même fichier.

Cela permet d'**importer** toutes les fonctionnalités du module en une seule fois et de pouvoir les utiliser. On dit programme que l'on veut utiliser les fonctionnalités du module en l'important.

Les modules sont pratiques pour plusieurs choses :
- Partager du code entre développeurs : certaines fonctionnalités peuvent servir à d'autres, et les modules permettent de facilement réutiliser le code
- Découper un programme en portions plus petites, et qui ont un lien entre elles, de façon à le rendre plus digeste, ou réutiliser certaines portions à plusieurs endroits ou dans plusieurs programmes

### Importer un module

Pour importer (c'est à dire utiliser) un module, on peut utiliser l'instruction `import` suivi du nom du module.

```admonish example title="Exemple"
Pour utiliser tout un tas de fonctions mathématiques, on peut importer le module `math`
~~~python
import math
~~~
```

Après un import, en apparence rien ne se passe, mais en réalité le programme vient de rendre toutes les fonctions du module accessibles. On peut ensuite appeler une fonction en la précédant du nom du module suivi d'un point.

```admonish example title="Exemple"
Pour utiliser la fonction racine carrée (`sqrt` pour square root) du module `math` et calculer la racine carrée de 16:
~~~python
import math

math.sqrt(16)

# Resultat : 4
~~~
```

Il existe une autre syntaxe, si on ne veut pas importer la totalité du module, mais seulement une fonction choisie : `from ... import ...`. La fonction peut alors être utilisée sans le nom du module. 

```admonish example title="Exemple"
Pour utiliser la fonction racine carrée (`sqrt` pour square root) du module `math` et calculer la racine carrée de 16, mais cette fois avec la syntaxe `from ... import ...`:
~~~python
from math import sqrt

sqrt(16)

# Resultat : 4
~~~
```

### Créer un module

Pour créer un module, rien de plus simple : il suffit de créer un nouveau fichier.

Le fichier devra :
- Avoir un nom en minuscules
- se terminer par l'extension `.py`

Et on pourra alors importer le module en utilisant son nom (sans l'extension `.py`).

```admonish example title="Exemple"
Pour utiliser le module `mon_module.py`, dans lequel se trouve `ma_fonction`, on pourra écrire
~~~python
import mon_module

### Ou encore 

from mon_module import ma_fonction
~~~
```
