modifié le

Application à Python

1. Programme Python

Réaliser un programme Python, c’est créer un ensemble de fichiers sources contenant du code Python.

Python étant un langage interprété, lorsque le développeur livre un programme à un utilisateur, il lui fournit :

  • l’ensemble des fichiers sources du programme,

  • des fichiers de configuration destinés à paramétrer l’application,

  • des fichiers de données destinées à être traitées par l’application,

  • un mode d’emploi.

Lorsque l’utilisateur exploite le programme, il exécute le code fourni tel quel. Il n’a pas à intervenir sur le code, le modifier, pas même modifier des constantes dans le code.

Le développeur peut utiliser un environnement de développement (EDI) pour mettre au point son programme (Spyder,PyDev, Idle, etc.). Il exécute son programme dans cet EDI. Mais l’utilisateur ne dispose pas forcément de cet EDI : il doit pouvoir exécuter l’application nativement sur son système.

Si l’application doit être personnalisable, alors elle doit gérer cet aspect. C’est une tâche à part entière, que l’application doit prendre en charge. Cela se fait généralement par des fichiers de configuration documentés et destinés à être adapté par l’utilisateur.

2. Les fichiers composant l’application

Une application python se compose des fichiers source, de fichiers de configuration et de données, de fichiers de documentation.

2.1. Fichiers source

Un code modulaire se maintient plus facilement qu’un code monolithique. Modulaire signifie qu’il est structuré en sous-programmes (fonctions, classes, etc.), et qu’il se présente sous forme de plusieurs fichiers sources, typiquement un programme principal et des modules.

Une application doit être maintenable (par son créateur, mais aussi par d’autres développeurs), et pour cela, le code doit être judicieusement commenté. Les doc-strings python sont faites pour cela. Ce sont des commentaires multi-lignes placées juste en dessous des en-têtes de fonction ou de classe, ou bien en tête de fichier :

fichier monprog.py
#!/bin/env python3

""" Ceci est mon programme
    qui réalise blablabla """

def mafonction(a,b):
  """ Ceci est la ma fonction qui attend
      2 arguments représentant blablabla
      et renvoyant blablabla """
  ...

if __name__=="__main__":
  """ Programme principal
        Les traitements assurés sont blablabla.
        Le code de retour correspond à blablabla """
    ...
    help(mafonction)

ce qui donne à l’exécution :

Help on function mafonction in module __main__:

mafonction(a, b)
    Ceci est la ma fonction qui attend
    2 arguments représentant blablabla
    et renvoyant blablabla

2.2. Fichier de configuration

Dans le cas le plus simple, c’est un fichier définissant des constantes, par exemple :

hauteur 1
fichier_de_données "data.csv"
seuil 12.3

Le format est à choisir, il n’est pas lié au langage de programmation utilisé. On peut décider d’autoriser des commentaires, des lignes vides, d’utiliser le signe = :

// longueur et largeur inférieures à 100
longueur=80
largeur=50

// la hauteur doit être inférieure à largeur
hauteur=10

ce qui peut augmenter la lisibilité du fichier. Mais cela peut-être insuffisant pour exprimer une configuration complexe. Le format .ini est autre format répandu, plus riche :

 [global]
 constante1=1
 constante2="abcd"

 [local]
 constante1=2
 constante2="efg"

Le format apacheconf en est encore un autre :

<section global>
constante1=1
constante2="abcd"
</section>

<section local>
 constante1=2
 constante2="efg"
  <section interne>
  constante1=3
  </section>
</section>

2.3. Fichier de données

Si l’application souhaite travailler sur des données fournies par l’utilisateur, elle doit le prévoir et le gérer. Elle peut :

  • les lui demander interactivement (les données seront donc saisies au clavier),

 $ ./tri
 entrer des entiers :
 1 5 2 4 3
 les entiers triés sont 1 2 3 4 5
  • les récupérer au lancement de l’application, au travers d’arguments spécifiés sur la ligne de commande,

 $ ./tri 1 5 2 4 3
 les entiers triés sont 1 2 3 4 5
  • les lire dans un fichier de données externe au code, ou depuis un stockage réseau.

2.4. Documentation

Une partie de la documentation s’adresse à l’utilisateur (le guide d’utilisation), l’autre est le manuel technique à destination de l’équipe de développement et de la maintenance.

3. Espace de nommage (namespace)

Un namespace est un conteneur d’identifiants (nom de variable, de fonction, de type, etc.). Dans un namespace, chaque identifiant est unique : impossible d’avoir à la fois une fonction f et une variable f. Par contre, on peut avoir une fonction f dans un namespace A et une variable f dans un namespace B.

Les identifiants globaux d’un programme sont dans le namespace global. Les identifiants locaux à une fonction sont dans le namespace de la fonction. Il est donc possible d’avoir une variable globale x et une variable locale x.

Un namespace est une notion statique, que l’on identifie par une simple lecture du code. Par exemple, étant donnée le programme :

...
def f():
  ...
  def g():
    ...
  ...
...

on identifie les namespaces suivants :

# début du namespace global du programme
...
def f():
  # début du namespace local à f
  ...
  def g():
    # début du namespace local à g
    ...
    # fin du namespace local à g
  # suite du namespace local à f
  ...
  # fin du namespace local à f
# suite du namespace global du programme
...

4. Portée

La portée d’un identifiant est la région dans laquelle il est accessible (directement, sans préfixage). La portée est une notion dynamique, qui évolue en cours d’exécution. Exemple :

x=1
y=1
z=1
def f():
  def g():
    z=3
    print(x,y,z)
  y=2
  print(x,y,z)
  g()
print(x,y,z)
f()
print(x,y,z)
Exercice 1

Exécuter l’exemple précédent et interpréter les résultats.

Il est recommandé de restreindre la portée des identifiants. Cela signifie par exemple utiliser plutôt des variables locales que des variables globales. Il est en effet plus facile de débugger un programme sans variable globale : rechercher l’origine d’une valeur erronée est plus facile si la portée des variables est limitée.

5. Modules

Un module est un moyen de limiter la portée d’identifiants. Un module est un namespace pouvant contenir des variables, des fonctions, etc. Les identifiants qu’il contient sont encapsulés : ils ne risquent donc pas de collisionner avec d’autres identifiants hors du module. C’est l’objectif : éviter les collisions de noms en limitant leur portée.

Concrétement, un module est un fichier source .py : un fichier mon_module.py définit un module mon_module.

fichier mon_module.py
# Ceci est un module nommé mon_module

ma_variable = 3.14 (1)

def ma_fonction():
  return ma_variable
1 ma_variable est une variable globale au module, mais interne au module sans collision possible avec une autre variable ma_variable ailleurs.

Un module est destiné à être importé dans un programme ou dans un autre module. L’instruction import exécute les instructions d’un module. Les identifiants du module deviennent accessibles à celui qui l’importe, à condition de les préfixer :

fichier mon_prog.py
import mon_module        # charge mon_module.py

print(ma_variable) # erreur !
print(mon_module.ma_variable)

ma_variable = 1
print(ma_variable)
print(mon_module.ma_variable)

print(mon_module.ma_fonction())

mon_module.ma_variable = 5
print(mon_module.ma_fonction())

Pour introduire dans le namespace courant un ou plusieurs identifiants de module, on utilise from…​import :

from mon_module import ma_fonction

print(ma_fonction())

On peut aussi importer tous les identifiants du module avec :

from le_module import *

mais cette pratique n’est pas recommandée (car ça ne sert à rien de faire des modules pour en arriver là).

Exercice 2
Question 1

Ajouter des instructions globales dans le module, et déterminer à quel moment elles sont exécutées. Que se passe-t-il si l’on importe plusieurs fois le même module ?

Question 2

Ajouter l’écriture de la variable __name__ dans le module, et même chose dans le fichier principal. Exécuter le programme. Conclusion ?

Question 3

Pourquoi les modules python ont généralement leurs instructions globales dans un test if __name__=="__main__": ?

5.1. Recherche de modules

La variable sys.path contient une liste de répertoires. Lors d’un import, le module importé est recherché successivement dans les répertoires mentionnés dans cette liste, jusqu’à le trouver. Généralement, cette liste commence par une chaine vide, c’est pourquoi les modules sont d’abord recherchés dans le répertoire courant. Tout programme a la possibilité de modifier la valeur de sys.path.

Mais si l’on veut un programme portable, il y a mieux que modifier sys.path. En effet, les chemins par exemple sous Unix et Windows ne s’écrivent pas pareil (/ pour l’un, \ pour l’autre). De même, est-ce une bonne idée d’imposer à l’utilisateur l’endroit où il doit mettre ses modules ?

Pour laisser la liberté à l’utilisateur de s’organiser comme il le souhaite, et pour ne pas dépendre du système qu’il utilise, le mieux est de ne rien fixer dans le programme. Et pour cela, la variable système PYTHONPATH va être utile.

S’il existe une variable système PYTHONPATH (même syntaxe que la variable système PATH), alors python l’utilise pour initialiser sys.path. Plus précisément, sys.path est initialisée de la façon suivante :

  1. le répertoire contenant le script courant

  2. les chemins de $PYTHONPATH

  3. les chemins spécifiques à la machine (dépendant des installations réalisées)

Pour rendre persistante PYTHONPATH d’une session à l’autre, il suffit de mettre dans son .bashrc quelque chose comme :

EXPORT PYTHONPATH=$HOME/mes_modules_finis:$HOME/mes_modules_tests

5.2. Gérer les modules

Il arrive fréquemment d’avoir besoin d’utiliser des modules python tiers, c’est-à-dire ne faisant pas partie de la distribution officielle. Il peut arriver également de vouloir utiliser un module dans une version différente de celle de la distribution officielle. Conséquence : il n’est donc pas toujours possible, sur une machine donnée, avec une installation unique de python, de couvrir tous les besoins de tous les projets.

Une solution à ce problème consiste, pour un projet, à utiliser un environnement virtuel. Un environnement virtuel est une arborescence situé un répertoire personnel du développeur, contenant des modules, et potentiellement un interpéteur python d’une version différente de celle installée sur le système. Un développeur peut se créer autant d’environnements virtuels qu’il le souhaite, et surtout, c’est lui qui le crée et le gère, sans avoir besoin de privilèges particuliers.

5.2.1. Créer un environnement virtuel

Choisir un nouveau répertoire pour cet environnement, par exemple ~/.venv, et faire :

$ python3 -m venv ~/.venv

Cela crée une arborescence sous ~/.venv (faire du -a ~/.venv pour le parcourir)

5.2.2. Activer l’environnement virtuel

$ source ~/.venv/bin/activate

Cela modifie en particulier la variable système PATH.

5.2.3. Installer un module

Lorsque l’environnement est actif, l’installation d’un module python se fait dans cet environnement :

$ pip install un_module

5.2.4. Lister les modules

Pour lister tous les modules installés dans l’environnement :

$ pip list

5.2.5. Utiliser les modules de l’environnement

Pour cela, il suffit d’exécuter le programme lorsque l’environnement est actif.

5.3. Package Python

Lorsqu’un module devient volumineux, on peut le décomposer en plusieurs fichiers sources. Pour cela :

  1. on crée un répertoire portant le nom du module

  2. on crée dans ce répertoire des fichiers sources qui s’importent les uns les autres.

L’un de ces fichiers doit s’appeller __init__.py : c’est le point d’entrée. Importer le package se fait exactement de la même façon qu’importer un module. Et cela importe le fichier __init__.py.