2. Un exemple, plein de fonctions#

Objectif.

Pourquoi des fonctions ?

  • Evite de ré-écrire, de dupliquer, du code

  • Simplifie la lecture donc la compréhension

  • Ré-utilise des fonctions existantes

  • Une nouvelle structure pour écrire les algorithmes/codes de façon plus modulaire

    • découper un traitement compliqué en une suite d’étapes plus simples

    • une étape = un appel de fonction

    • s’applique récursivement

  • Facilite la validation du traitement : test unitaire

Notre exemple.

Code qui calcule le nombre de combinaisons de \(p\) éléments choisis dans \(n\) éléments :

\[\cal{C}_n^p = \frac{n!}{p! (n-p)!}.\]

Rappel : \(n! = n \times (n-1) \times \dots \times 2 \times 1\) et \(0! = 1\).

Warning

La version en ligne de ce notebook ne permet pas une prise ne compte correcte des entrées (input()). Ces entrées sont donc ponctuellement modifiées : mise en commentaires et initialisation en ligne.

Exemple.

n = int(input("n:"))
p = int(input("p:"))

est remplacé par :

#n = int(input("n:"))
#p = int(input("p:"))
n = 3
p = 2

2.1. Comme au premier semestre : sans fonction#

# Role : calcule C_n^p
#entrées
#n = int(input("n:"))
#p = int(input("p:"))
n = 3
p = 2

#traitement : on calcule chaque terme de l'expression visée
f_n = 1
for i in range(1, n+1):
    f_n = f_n  * i
    
f_p = 1
for i in range(1, p+1):
    f_p = f_p  * i

f_nmp = 1
for i in range(1, n-p+1):
    f_nmp = f_nmp  * i

# le calcul de l'expression
num = f_n
den = f_p * f_nmp
res = num // den

# sortie
print(res) 
3

2.2. Avec une fonction prédéfinie : exemple d’appel de fonction#

Indicateur : import, (paramètre effectif)

from math import factorial
help(factorial)
Help on built-in function factorial in module math:

factorial(n, /)
    Find n!.
    
    Raise a ValueError if x is negative or non-integral.
#entrées
#n = int(input("n:"))
#p = int(input("p:"))
n = 3
p = 2

#traitement avec appels de la fonction prédéfinie factorial
num = factorial(n)
d1 = factorial(p)
d2 = factorial(n-p)
res = num // (d1 * d2)

# sortie
print(res) 
3

2.3. Avec notre propre fonction : d’abord la signature (l’en-tête)#

Indicateur : def( les paramètres formels et leurs types ) et pass

# notre fonction
def fact(n : int) -> int:
    pass

2.4. Avec notre propre fonction : ensuite au moins un appel#

Indicateur : ( paramètre effectif )

Interdiction pédagogique Ne pas utiliser le paramètre formel comme paramètre effectif

Dans ce qui précède :

  • la ligne avec def est la signature (presque complète) de notre fonction factorielle

  • ajouter l’intruction pass de façon indentée permet d’exécuter la cellule de définition

  • mais pas d’exécuter un appel de cette fonction.

En effet, notre fonction fact() n’effectue encore aucun traitement. Les appels effectués pour calculer num, d1 et d2 retournent des valeurs None. Ce qui ne permet pas de continuer les calculs (de res) et déclenche une erreur lors de l’exécution de la cvellule suivante.

# Motto : entrées/traitement/sorties !!
#
#entrées
#mon_n = int(input("n:"))
#mon_p = int(input("p:"))
mon_n = 3
mon_p = 2

#traitement avec appels de notre fonction fact
num = fact(mon_n)
d1 = fact(mon_p)
d2 = fact(mon_n - mon_p)

# on commente le calcul suivant qui va déclencher une erreur
#res = num // (d1 * d2)

# sortie
print(res) 
3

2.5. Validons notre fonction avec des tests unitaires#

Indicateur : assert

Comme avec l’appel, l’exécution de ces tests unitaires déclenche une erreur tant que le corps de la fonction n’est pas écrit (mais simplement remplacé par pass).

Ces tests, qui peuvent être écrits avant le corps, sont donc mis en commentaires (entre ''') en attendant.

'''
assert fact(0) == 1
assert fact(1) == 1

# utilisation d'une propriété générale pour un choix arbitraire de valeurs (ici 55..75)
for i in range(55,75):
    assert fact(i) == i * fact(i-1)
'''

2.6. Avec notre propre fonction : puis son corps#

Indicateur : la zone indentée sous le def( les paramètres formels et leurs types ), return

# notre fonction enfin complète
def fact(n : int) -> int:
    res = 1
    for i in range(1,n+1):
        res = res * i
    return res
#premiers tests unitaires de notre fonction fact()
assert fact(0) == 1
assert fact(1) == 1

# utilisation d'une propriété générale pour un choix arbitraire de valeurs (ici 55..75)
for i in range(55,75):
    assert fact(i) == i * fact(i-1)
#entrées
#mon_n = int(input("n:"))
#mon_p = int(input("p:"))
mon_n = 3
mon_p = 2

#traitement avec appels de notre fonction fact()
num = fact(mon_n)
d1 = fact(mon_p)
d2 = fact(mon_n - mon_p)

res = num // (d1 * d2)

# sortie
print(res) 
3

2.7. Avec notre propre fonction : on l’utilise à souhait#

Indicateur : ( paramètre effectif )

2.7.1. Rmq. Où placer la définition d’une fonction ?#

#entrées
#mon_n = int(input("n:"))
#mon_p = int(input("p:"))
mon_n = 3
mon_p = 2

#les fonctions locales (ici une seule) 
def fact(n : int) -> int:
    res = 1
    for i in range(1,n+1):
        res = res * i
    return res

#le traitement principal "main"
num = fact(mon_n)
d1 = fact(mon_p)
d2 = fact(mon_n - mon_p)
res = num // (d1 * d2)

# sortie
print(res) 
3

On vient d’appeler 3 fois la fonction fact().

2.8. On termine notre fonction avec sa description#

Indicateur : docstring

def fact(n : int) -> int:
    '''
    Calcule n! de façon itérative
    '''
    # ceci est un commentaire
    
    res = 1
    for i in range(1,n+1):
        res = res * i
    return res
help(fact)
Help on function fact in module __main__:

fact(n: int) -> int
    Calcule n! de façon itérative

2.9. Un autre corps pour un en-tête identique#

Ou : Le même quoi mais un autre comment.

def fact2(n :int) -> int:
    '''
    Calcule n! de façon itérative avec un while
    '''
    res = 1
    i = 1
    while i < n + 1:
        res = res * i
        i = i + 1
    return res
#les tests unitaires ne changent pas 
assert fact2(0) == 1
assert fact2(1) == 1

# utilisation d'une propriété générale pour un choix arbitraire de valeurs (ici 55..75)
for i in range(55,75):
    assert fact2(i) == i * fact2(i-1)

2.10. (\(\star\)) Bonus : une première récursion gratuite#

def fact_rec(n :int) -> int:
    '''
    Calcule n! de façon récursive
    '''
    if n == 0 or n == 1:
        return 1
    return n * fact_rec(n - 1)

2.10.1. Validation immédiate#

#les tests unitaires ne changent pas 
assert fact_rec(0) == 1
assert fact_rec(1) == 1

# utilisation d'une propriété générale pour un choix arbitraire de valeurs (ici 55..75)
for i in range(55,75):
    assert fact_rec(i) == i * fact_rec(i-1)

2.10.2. Utilisation#

#entrées
#mon_n = int(input("n:"))
#mon_p = int(input("p:"))
mon_n = 3
mon_p = 2

#traitement
num = fact_rec(mon_n)
d1 = fact_rec(mon_p)
d2 = fact_rec(mon_n - mon_p)
res = num // (d1 * d2)

# sortie
print(res) 
3

2.11. Une fonction locale#

Une fonction peut être définie à l’intérieur de la définition d’une autre fonction : elle est locale à cette fonction englobante. Elle peut être appelée dans la définition de la fonction englobante.

2.11.1. Exemple#

def c_n_p(n : int, p : int) -> int:
    '''
    Calcule le nombre de combinaisons de p éléments parmi n : n! / p!(n-p)!
    '''
    def fact_locale(k : int) -> int:
        '''
        calcule k!
        '''
        res = 1
        for k in range(2, k+1):
            res = res * k
        return res
    
    num = fact_locale(n)
    denom = fact_locale(p) * fact_locale(n-p)
    return num//denom

2.11.2. Validation#

On commence par écrire une validation de cette nouvelle fonction

assert c_n_p(3, 2) == 3
assert c_n_p(3, 3) == 1

2.11.3. Utilisation#

#entrées
#mon_n = int(input("n:"))
#mon_p = int(input("p:"))
mon_n = 3
mon_p = 2

#calcul de c_n_p par appel de la fonction
c32 = c_n_p(3, 2)

# affichage
print(c32)
3

2.12. Compétences#

2.12.1. Avoir les idées claires#

  • reconnaître et utiliser une fonction prédéfinie ou existante

  • reconnaître et distinguer :

    • définition et paramètres formels vs. appel et paramètres effectifs

    • spécification, en-tête, signature : spécifier pour utiliser, pour vérifier vs. corps : implémentation du traitement

  • comprendre que appel = changement de contexte

    • trace de l’exécution vs. séquentialité des instructions écrites

    • dynamique vs. statique

  • distinguer appelant vs. appelé :

    • le rôle de l’appel,

    • le rôle du return

  • identifier la portée des variables :

    • variables locales vs. variables plus globales

  • se souvenir que l’effet de bord est indésirable

  • définir et écrire une spécification de fonction avec des paramètres de type tableau

2.12.2. Ce qu’il faut savoir faire#

Cadre : en/pour python

  • utiliser une fonction prédéfinie ou existante

  • définir et écrire la spécification d’une fonction qui réalise un traitement décrit en français, ou qui résout un problème (simple) décrit en français

  • définir et écrire des appels simples (ou des tests unitaires)

  • définir, écrire et utiliser des tests unitaires

  • définir et écrire l’implémentation d’une fonction associée à une spécification