Résoudre un problème grâce aux paradigmes objet et fonctionnel



L’objectif de ce document est d’illustrer une fois encore l’intérêt des différents paradigmes de programmation.

Cas d’étude : fonction avec paramètres

On considère l’équation qui traduit le mouvement d’une balle lancée verticalement vers le haut avec une vitesse $\vec{v}_0$ depuis l’origine des altitudes telle que l’écrit un physicien : $$y(t) = -\dfrac{1}{2}g\, t^2 + v_0\, t$$ Ce physicien a donc tendance à considérer que $y$ est une fonction de la variable $t$ et que cette fonction dépend des paramètres $v_0$ et $g$.

  1. Comment définir cette fonction en informatique, sachant que $g$ est un paramètre fixe $g=\pu{9,81 m/s2}$ mais que $v_0$ est un paramètre qu’il peut être utile de faire varier ?

Réponse
1
2
3
4
5
6
7
8
def y(t: float, v0: float) -> float:
    """
    Calcule l'altitude d'une balle lancée vers le haut verticalement avec la vitesse v0 au cours du temps depuis l'origine des altitudes.

    REMARQUE : la fonction définit un paramètre constant g = 9.81
    """
    g = 9.81
    return -0.5 * g * t**2 + v0 *t

Problème

Le physicien qui vient de définir la fonction de la question 1. peut vouloir utiliser les fonctions que certains modules, tels que les modules math, numpy ou scipy, proposent. Ces fonctions étant les plus générales possibles, elles implémentent le point de vue du physicien, $y$ est une fonction de $t$, plutôt que celui de l’informaticien, $y$ est une fonction de $t$ et de $v_0$. Comment modifier la définition de la fonction y pour qu’elle puisse être utilisée avec n’importe quelle autre fonction ?

Illustration du problème

On peut avoir envie de déterminer la vitesse $v_y(t)$ de la balle. La relation mathématique entre $v_y(t)$ et $y(t)$ est la suivante $$v_y(t) = \dfrac{\mathrm{d} y}{\mathrm{dt}} = y\rq (t)$$ $v_y(t)$ est la dérivée par rapport au temps de $y(t)$.

On peut montrer, en analyse numérique, que, en un point d’abscisse $x_0$ $$ f\rq (x_0) \approx \dfrac{f(x_0 + h) - f(x_0)}{h}$$

  1. Définir la fonction diff dont la spécification est :
1
2
3
4
5
6
def diff(f: Callable, x0: float, h: float=1E-7) -> float:
    """
    Calcule la dérivée de la fonction f au point d'abscisse x0.

    REMARQUE : le paramètre h est fixé à h = 1e-7 par défaut.
    """

Réponse
1
2
3
4
5
6
7
def diff(f: Callable, x0: float, h: float = 1E-7) -> float:
    """
    Calcule la dérivée de la fonction f au point d'abscisse x0.

    REMARQUE : le paramètre h est fixé à h = 1e-5 par défaut.
    """
    return (f(x0 + h) - f(x0)) / h

  1. Tester cette fonction en calculant la dérivée de $\cos(x)$ au point $x=0$. Rappel : $(\cos (0))\rq = -\sin(0) = 0$.

Réponse
1
2
import math as m
print(diff(m.cos, 0))

  1. La fonction diff peut-elle être utilisée pour déterminer la dérivée de la fonction y ?

Réponse

Non, puisque diff ne prend que le nom de la fonction à dériver en argument et pas ses paramètres. diff suppose que la fonction f est une fonction à une seule variable.


Première (très mauvaise !) solution : utilisation d’une variable globale

  1. Écrire la fonction y1 dont la spécification est
1
2
3
4
5
6
7
8
def y1(t: float) -> float:
    """
    Calcule l'altitude d'une balle lancée vers le haut verticalement avec la vitesse v0 au cours du temps depuis l'origine des altitudes.

    REMARQUE : la fonction définit un paramètre constant g = 9.81

    REMARQUE : la fonction utilise la variable globale v0.
    """
  1. Déterminer la valeur de la vitesse de la balle à la date $t=\pu{0,1 s}$ en utilisant la fonction y1. Remarque : $v0 = \pu{3 m/s}$ et la réponse attendue est $v_y = \pu{2,02 m/s}$.

Réponse
1
2
3
v0 = 3
t = 0.1
print(diff(y1, t))

  1. Pourquoi est-ce une mauvaise idée que d’utiliser des variables globales ?

Réponse

Une variable globale peut être utilisée et modifiée à plusieurs endroits d’un programme. Il n’est pas toujours facile, lors de l’appel d’une fonction utilisant une variable globale, de savoir exactement quelle est sa valeur.


Deuxième méthode : représenter une fonction par une classe

Sans utilisation des méthodes spéciales

Une classe définit un nouveau type et est constituée d’attributs (variables) et de méthodes (fonctions). Les attributs sont accessibles en lecture et en modification par toutes les méthodes ; ce sont des variables globale à la classe !
  1. Définir la classe Y1 dont la spécification est
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
class Y1():
    """
    Classe représentant la fonction mathématique y.
    
    Attributs
    ---------
    g : float
        Paramètre g dont la valeur est fixée à l'initialisation de l'objet.
    v0 : float
        Paramètre v0 dont la valeur est récupérée à l'initialisation de l'objet.
        
    Méthode
    -------
    valeur(self, t: float) -> float
        Retourne la valeur de y à la date t passée en argument.
    """

Réponse
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class Y1():
    """
    Classe représentant la fonction mathématique y.
    
    Attributs
    ---------
    g : float
        Paramètre g dont la valeur est fixée à l'initialisation de l'objet.
    v0 : float
        Paramètre v0 dont la valeur est récupérée à l'initialisation de l'objet.
        
    Méthode
    -------
    valeur(self, t: float) -> float
        Retourne la valeur de y à la date t passée en argument.
    """
    def __init__(self, v0: float, g: float = 9.81) -> None:
        self.v0 = v0
        self.g = g

    def valeur(self, t: float) -> float:
        """
        Retourne la valeur de y à la date t passée en argument.
        """
        return -0.5 * self.g * t**2 + self.v0 * t

  1. Créer un objet de type Y1 et vérifier que la valeur de la fonction $y$ à la date $t=\pu{0,1 s}$, lorsque $v_0=\pu{3 m/s}$ est $\pu{0,25 m}$.

Réponse
1
2
yo = Y1(3)
print(yo.valeur(0.1))  # ou assert yo.valeur(0.1) == y(0.1, 3)

  1. Déterminer la vitesse de la balle à cet instant.

Réponse
1
2
yo = Y1(3)
print(diff(yo.valeur, 0.1))

En utilisant une méthode spéciale

On peut penser que la définition de la classe Y1 fait trop apparaître le concept d’objet au détriment de celui de fonction. En particulier, l’appel yo.valeur(0.1) n’est pas aussi clair que yo(0.1).
Les classes possèdent cependant, en Python, une méthode spéciale nommée __call__ appelée automatiquement chaque fois que l’on utilise un objet comme une fonction.

  1. Définir la classe Y2 dont le code est proche de celui de Y1 mais qui redéfinit la méthode __call__ à la place de définir la méthode valeur.

Réponse
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
class Y2():
    """
    Classe représentant la fonction mathématique y.
    
    Attributs
    ---------
    g : float
        Paramètre g dont la valeur est fixée à l'initialisation de l'objet.
    v0 : float
        Paramètre v0 dont la valeur est récupérée à l'initialisation de l'objet.
    """

    def __init__(self, v0: float, g: float = 9.81) -> None:
        self.v0 = v0
        self.g = g

    def __call__(self, t: float) -> float:
        """
        Retourne la valeur de y à la date t passée en argument.
        """
        return -0.5 * self.g * t**2 + self.v0 * t

  1. Déterminer la vitesse de la balle à dans les mêmes conditions qu’à la question 10.

Réponse
1
2
yo = Y2(3)
print( yo(0.1) )

Troisième méthode : premiers pas dans le monde fonctionnel

En programmation fonctionnelle, les fonctions sont des objets de première classe : une fonction peut être passée comme argument à une autre fonction, une fonction peut retourner une fonction.

Étudier le code suivant

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
def fixe_param(v0: float) -> Callable:
    g = 9.81

    def calcule_y(t: float) -> float:
        return -0.5 * g * t**2 + v0 * t

    return calcule_y

y3 = fixe_param(3)
print( y3(0.1) )
  1. Qu’attend comme argument la fonction fixe_param ?

Réponse

fixe_param possède le paramètre v0.


  1. Que sont les variables v0 et g pour la fonction calcule_y ?

Réponse

Ce sont des variables externes (non locales). En fait calcule_y les considère comme des variables globales (même si elles sont seulement locales à la fonction fixe_param). Le dernier point de ce document précise le vocabulaire.


  1. Quel est le type de y3 ?

Réponse

y3 référence une fonction.


  1. Que signifie l’instruction y3(0.1) ?

Réponse

y3(0.1) est un appel de fonction. On appelle la fonction référencée par la variable y3 avec l’argument 0.1.


Utiliser le site Pythontutor pour bien comprendre l’effet de chacune des instructions.
  1. Quelles lignes de code faudrait-il modifier si maintenant la balle est lancée à la vitesse $v_0 = \pu{5 m/s}$ et que l’on cherche toujours sa position à la date $t = \pu{0,1 s}$ ?

Réponse
1
2
y5 = fixe_param(5)
print( y5(0.1) )

  1. Quel est le rôle de la fonction fixe_param ?

Réponse

fixe_param enveloppe calcule_y, elle permet de réaliser une fermeture (closure).


Dans un langage de programmation, une fermeture (en anglais : closure) est une fonction accompagnée de son environnement lexical. L’environnement lexical d’une fonction est l’ensemble des variables non locales qu’elle a capturé, soit par valeur (c’est-à-dire par copie des valeurs des variables), soit par référence (c’est-à-dire par copie des adresses mémoires des variables). Une fermeture est donc créée, entre autres, lorsqu’une fonction est définie dans le corps d’une autre fonction et utilise des paramètres ou des variables locales de cette dernière.


Suggestions de lecture :