- Langages et programmation -
Modularité

I. Les modules

L’un des points forts de Python est sa licence libre et la possibilité offerte de créer des modules (bibliothèques) facilement.

A ce titre, Anaconda est une distribution Python contenant : * la bibliothèque standart * des logiciels d’édition (Spyder, Visual Studio Code…) * des terminaux (anaconda prompt qui implémente une invite de commande Windows, anaconda powershell) * de nombreux modules additionnels (dont les modules Jupyter)

Il est possible en une instruction de lister tous modules installés sur une machine :

conda list
# packages in environment at C:\ProgramData\Anaconda3:
#
# Name                    Version                   Build  Channel
_ipyw_jlab_nb_ext_conf    0.1.0                    py37_0  
absl-py                   0.8.1                    pypi_0    pypi
alabaster                 0.7.12                   py37_0  
anaconda                  2019.03                  py37_0  
...
zstd                      1.3.7                h508b16e_0  

Note: you may need to restart the kernel to use updated packages.

II. Installer un module

La façon la plus simple et “propre” d’installer un module Python est de passer par une invite de commande.

Remarque : selon la machine il peut être nécessaire d’avoir ouvert l’invite de commande en administrateur.

Imaginons que l’on souhaite installer le module Pandas (utile pour les traitements de données type csv). Plusieurs solutions s’offrent à nous :

III. Importer un module

Il existe deux façon d’utiliser un module dans un programme python :

1. Import de base :

import <nom_du_module> : permet d’indiquer que l’on utilisera les fonctions du module indiqué. Chaque référence à une fonction se fera sous la forme <nom_du_module>.<fonction>

Par exemple :

import math

math.sqrt(9)
3.0

2. Import d’une fonction particulière :

from <nom_du_module> import <fonction> : permet d’indiquer que l’on utilisera la fonction indiquée du module indiqué. Chaque référence à cette fonction se fera sous la forme <fonction>

Par exemple :

from math import sqrt

sqrt(9)
3.0

Cette deuxième façon de faire permet aussi d’importer l’ensembles des fonctions d’un module en une seule ligne :

from math import *

sqrt(sin(pi/2))
1.0

Il est possible d’indiquer plusieurs modules ou fonctions à la fois :

import math, random

nombre = random.randint(1,20)

math.sqrt(nombre)
3.3166247903554
from math import sqrt, log, exp

sqrt(log(exp(9)))
3.0

2. Pourquoi utiliser une méthode plutôt que l’autre ?

Dans les cas de bases, la seconde méthode est plus facile : si l’on a besoin de choisir un entier aléatoire entre 1 et 6, on n’importe que la fonction randint du module random :

from random import randint

randint(1,6)
4

On peut même tout importer mais on ne sait pas exactement ce que l’on fait :

from random import *

randint(1,6)
4

Quand le code commence à prendre de l’envergure, en projet par exemple, il est intéressant de garder la trace de l’origine de chaque fonction.

En effet dans certains cas, deux modules peuvent contenir une fonction portant le même nom : quelle fonction est alors utilisée ?

from random import *

from numpy.random import *

randint(1,5)
1
# La documentation de la fonction randint importée
help(randint)
Help on built-in function randint:

randint(...) method of mtrand.RandomState instance
    randint(low, high=None, size=None, dtype='l')
    
    Return random integers from `low` (inclusive) to `high` (exclusive).
    
    Return random integers from the "discrete uniform" distribution of
    the specified dtype in the "half-open" interval [`low`, `high`). If
    `high` is None (the default), then results are from [0, `low`).
    
    Parameters
    ----------
    low : int
        Lowest (signed) integer to be drawn from the distribution (unless
        ``high=None``, in which case this parameter is one above the
        *highest* such integer).
    high : int, optional
        If provided, one above the largest (signed) integer to be drawn
        from the distribution (see above for behavior if ``high=None``).
    size : int or tuple of ints, optional
        Output shape.  If the given shape is, e.g., ``(m, n, k)``, then
        ``m * n * k`` samples are drawn.  Default is None, in which case a
        single value is returned.
    dtype : dtype, optional
        Desired dtype of the result. All dtypes are determined by their
        name, i.e., 'int64', 'int', etc, so byteorder is not available
        and a specific precision may have different C types depending
        on the platform. The default value is 'np.int'.
    
        .. versionadded:: 1.11.0
    
    Returns
    -------
    out : int or ndarray of ints
        `size`-shaped array of random integers from the appropriate
        distribution, or a single such random int if `size` not provided.
    
    See Also
    --------
    random.random_integers : similar to `randint`, only for the closed
        interval [`low`, `high`], and 1 is the lowest value if `high` is
        omitted. In particular, this other one is the one to use to generate
        uniformly distributed discrete non-integers.
    
    Examples
    --------
    >>> np.random.randint(2, size=10)
    array([1, 0, 0, 0, 1, 1, 0, 0, 1, 0])
    >>> np.random.randint(1, size=10)
    array([0, 0, 0, 0, 0, 0, 0, 0, 0, 0])
    
    Generate a 2 x 4 array of ints between 0 and 4, inclusive:
    
    >>> np.random.randint(5, size=(2, 4))
    array([[4, 0, 2, 1],
           [3, 2, 2, 0]])
from numpy.random import *

from random import *

help(randint)
Help on method randint in module random:

randint(a, b) method of random.Random instance
    Return random integer in range [a, b], including both end points.

Pour éviter les ambiguités, on importe alors seulement le module (quitte à utiliser un alias pour gagner en simplicité) :

import random
import numpy.random as np_rd

help(random.randint)
help(np_rd.randint)
Help on method randint in module random:

randint(a, b) method of random.Random instance
    Return random integer in range [a, b], including both end points.

Help on built-in function randint:

randint(...) method of mtrand.RandomState instance
    randint(low, high=None, size=None, dtype='l')
    
    Return random integers from `low` (inclusive) to `high` (exclusive).
    
    Return random integers from the "discrete uniform" distribution of
    the specified dtype in the "half-open" interval [`low`, `high`). If
    `high` is None (the default), then results are from [0, `low`).
    
    Parameters
    ----------
    low : int
        Lowest (signed) integer to be drawn from the distribution (unless
        ``high=None``, in which case this parameter is one above the
        *highest* such integer).
    high : int, optional
        If provided, one above the largest (signed) integer to be drawn
        from the distribution (see above for behavior if ``high=None``).
    size : int or tuple of ints, optional
        Output shape.  If the given shape is, e.g., ``(m, n, k)``, then
        ``m * n * k`` samples are drawn.  Default is None, in which case a
        single value is returned.
    dtype : dtype, optional
        Desired dtype of the result. All dtypes are determined by their
        name, i.e., 'int64', 'int', etc, so byteorder is not available
        and a specific precision may have different C types depending
        on the platform. The default value is 'np.int'.
    
        .. versionadded:: 1.11.0
    
    Returns
    -------
    out : int or ndarray of ints
        `size`-shaped array of random integers from the appropriate
        distribution, or a single such random int if `size` not provided.
    
    See Also
    --------
    random.random_integers : similar to `randint`, only for the closed
        interval [`low`, `high`], and 1 is the lowest value if `high` is
        omitted. In particular, this other one is the one to use to generate
        uniformly distributed discrete non-integers.
    
    Examples
    --------
    >>> np.random.randint(2, size=10)
    array([1, 0, 0, 0, 1, 1, 0, 0, 1, 0])
    >>> np.random.randint(1, size=10)
    array([0, 0, 0, 0, 0, 0, 0, 0, 0, 0])
    
    Generate a 2 x 4 array of ints between 0 and 4, inclusive:
    
    >>> np.random.randint(5, size=(2, 4))
    array([[4, 0, 2, 1],
           [3, 2, 2, 0]])

IV. Créer un module

Un module python est en réalité un “simple” fichier contenant des fonctions à utiliser. Le plus souvent lors de l’importation, Python se contente de lire le fichier sans l’exécuter.

La création d’un module consiste donc essentiellement en la création de fonctions.

1. La documentation

On peut avoir besoin de créer un module pour regrouper des objets (classes, fonctions, variables…) qui reserviront dans plusieurs codes ou afin d’alléger un code.

Dans ce cas, on doit coder correctement notre fonction (elle doit être correcte et ne pas bugger) et remplir une documentation.

Observons la définition de la fonction choice de python : code source de python (ligne 296)

 def choice(self, seq):
        """Choose a random element from a non-empty sequence."""
        try:
            i = self._randbelow(len(seq))
        except ValueError:
            raise IndexError('Cannot choose from an empty sequence') from None
        return seq[i]

On observe : * une documentation entre les triples guillemets * une vérification que la liste est non vide. Un erreur est levée si c’est le cas * un retour

Il existe de nombreuses façons de rédiger des documentations.

Retenons qu’une bonne documentation doit avoir : * Une description générale de la fonction * Une description des arguments de la fonction (types, rôle) * Une description de la valeur retournée par la fonction

Par exemple :

def moyenne(tableau, colonne) :
    """
    Fonction calculant la moyenne de la colonne indiquée du tableau fourni en argument
    
    tableau est un tableau de tableaux. Chaque ligne contient différentes colonnes
    colonne est le numéro de la colonne dont on veut calculer la moyenne
    
    Renvoie la moyenne de la colonne concernée au format float
    """
    # Préconditions (partielles)\n
    assert isinstance(tableau, list), "tableau doit être une liste"
    assert isinstance(colonne, int), "colonne doit être un entier"

    somme = 0
    for ligne in tableau :
        somme += ligne[colonne]
    return somme / len(tableau)

2. Les tests / préconditions

Lorsque l’on tape une fonction, on doit s’assurer de sa correction : * le fait que la fonction se termine (terminaison) … * … et qu’elle effectue les bons calculs sans créer d’erreur

Une façon de s’assurer que la fonction ne va pas créer d’erreur est de tester les arguments.

On peut utiliser différentes façon de faire dont le assert :

assert <condition>, <Texte si la condition n'est pas respectée>

Voici un exemple pour la fonction racine_carrée qui doit toujours manipuler des nombres, positifs de plus.

def racine_carrée(nombre) :
    """
    Fonction calculant la racine carré du nombre fourni en argument
    
    nombre est un nombre positif

    Retourne la racine carré de nombre
    """
    
    assert  isinstance(nombre, (float, int)), "Le nombre doit être du type int ou float"

    assert nombre >= 0, "Le nombre doit être positif"
    
    from math import sqrt
    
    return sqrt(nombre)

On peut alors tester notre fonction :

racine_carrée(9) == 3.0
True

Et la gestion des erreurs :

racine_carrée(-9)
---------------------------------------------------------------------------

AssertionError                            Traceback (most recent call last)

<ipython-input-3-5ae24efab6a4> in <module>
----> 1 racine_carrée(-9)


<ipython-input-2-b1b557790c3f> in racine_carrée(nombre)
      5 
      6     # Préconditions à taper
----> 7     assert nombre >= 0, "Le nombre doit être positif"
      8 
      9     from math import sqrt


AssertionError: Le nombre doit être positif