Notes sur les modules et packages en Python

À destination de ceux qui le veulent

Parce qu’il est parfois nécessaire d’avoir des réponses types sous la main pour répondre sur les forums à des incompréhensions quant au système de modules en Python.

Modules

Un module est un fichier Python. Il peut donc contenir des classes, des fonctions et des variables, qui peuvent être importées depuis un autre fichier.

Le nom d’un module correspond au nom du fichier auquel l’extension serait tronquée. Un fichier my_module.py donnera un module se nommant my_module. Cela impose quelques restrictions sur les noms de fichiers, qui sont les mêmes que les noms de variables : ne peuvent être composés que de lettres (accentuées ou non), de chiffres et d’underscores (_), et ne pouvant pas débuter par un chiffre.

Le module peut être utilisé (importé) depuis un autre fichier à l’aide du mot-clef import.

Étant donné un fichier toto.py suivant :

1
2
def tata():
    return 'tutu'

Il est possible depuis la console Python (et de tout autre fichier) d’importer le module toto et sa fonction tata. Plusieurs syntaxes différentes existent pour importer des objets depuis des modules.

On importe le module et utilise ce dont on a besoin

1
2
3
>>> import toto
>>> toto.tata()
'tutu'

On importe que le strict nécessaire depuis le module

1
2
3
>>> from toto import tata
>>> tata()
'tutu'

On importe tout le contenu du module

1
2
3
>>> from toto import *
>>> tata()
'tutu'

Cette dernière méthode étant à éviter car encombre inutilement l’espace de noms.

Répertoires de recherche de modules

Les modules ne peuvent être importés que s’ils appartiennent à un répertoire recherche référencé.

Ces répertoires sont par exemple les dossiers d’installation de Python (comprenant la bibliothèque standard), les dossiers d’installation des bibliothèques tierces, et le répertoire courant.

Ainsi, un module dans un autre répertoire que ceux cités précédemment ne sera pas accessible.

Par exemple, je suis dans un dossier foo, qui comporte un sous-dossier bar, lequel contient notre fichier toto.py de la section précédente.

Alors, si j’ouvre une console Python, il m’est impossible d’importer toto.

1
2
3
4
>>> import toto
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ImportError: No module named 'toto'

La liste des répertoires de recherche est référencée par la variable path du module sys.

1
2
3
>>> import sys
>>> sys.path
['', '/usr/lib/python3.6', '/usr/lib/python3.6/plat-linux', '/usr/local/lib/python3.6/dist-packages']

Cette liste est modifiable, et de nouvelles entrées peuvent donc y être ajoutées.

1
2
3
4
>>> sys.path.append('bar')
>>> import toto
>>> toto.tata()
'tutu'

En gardant à l’esprit que les premiers éléments sont prioritaires : si un module est trouvé dans le premier répertoire, les suivants ne seront pas examinés. Cela peut amener à des erreurs peu compréhensibles en cas de conflits entre les noms. Par exemple, si nous avons dans notre répertoire courant (premier élément de sys.path) un fichier socket.py, le module socket de la bibliothèque standard ne sera plus accessible.

1
2
3
4
5
6
7
>>> import socket
>>> socket.socket
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'module' object has no attribute 'socket'
>>> socket
<module 'socket' from '/home/entwanne/foo/socket.py'>

Mais ajouter des répertoires à sys.path est généralement une mauvaise idée, on préférera placer le module dans un répertoire déjà référencé.

Packages

Les packages (paquets) sont des modules, mais qui peuvent contenir d’autres modules.

Un package correspond à un répertoire sur le système de fichier : il a un nom (nom du package), et contient des fichiers (les modules). Les règles de nom des packages sont donc les mêmes que pour les modules.

Avant Python 3.5, pour être un package, un répertoire devait contenir un fichier __init__.py. Ce n’est plus obligatoire aujourd’hui, mais c’est toujours utile. Quand un package est importé, c’est en fait son module __init__ qui l’est.

Si nous créons un sous-répertoire mypackage dans le répertoire courant, et que nous y écrivons le fichier __init__.py suivant :

1
2
def myfunction():
    return None

Alors mypackage est utilisable comme un module contenant une fonction myfunction.

1
2
>>> import mypackage
>>> mypackage.myfunction()

L’intérêt principal des packages étant tout de même de contenir plusieurs modules. On peut ainsi ajouter un fichier operations.py au répertoire mypackage.

1
2
3
4
5
6
7
8
def addition(a, b):
    return a + b

def soustraction(a, b):
    return a - b

def multiplication(a, b):
    return a * b

Cela revient à disposer d’un module mypackage.operations. Mais ce module n’est par défaut pas importé dans le package : import mypackage ne donne par défaut pas accès à operations, il faudra importer explicitement ce dernier.

1
2
3
4
5
6
7
8
9
>>> import mypackage.operations
>>> mypackage.operations.addition(1, 2)
3
>>> from mypackage import operations
>>> operations.soustraction(1, 2)
-1
>>> from mypackage.operations import multiplication
>>> multiplication(1, 2)
2

On note aussi qu’il n’est pas nécessaire d’importer mypackage pour pouvoir importer mypackage.operations.

Pour donner accès au module operations directement en important mypackage, il est nécessaire de toucher au fichier __init__.py. Ce fichier correspondant à ce qui est chargé à l’importation du package, nous pouvons y importer mypackage.operations, ce qui le rendra directement accessible.

__init__.py
1
import mypackage.operations

Puis en console :

1
2
3
>>> import mypackage
>>> mypackage.operations.addition(1, 2)
3

Un package est un niveau d’indirection supérieur au module, mais il est aussi possible d’avoir des packages de packages, et packages de packages de packages, et plus encore : vers l’infini et au-delà !

Imports relatifs

Au sein d’un package, il est parfois usant d’avoir à en recopier le nom complet.

Dans l’exemple précédent de mypackage/__init__.py important le sous-module operations, on doit réaliser un import mypackage.operations. Si le package change de nom, il faudra mettre à jour cette ligne. Et plus généralement, ça allonge parfois inutilement les lignes, et ne rend pas bien compte du fait qu’il s’agisse d’un module du même package.

Les imports relatifs permettent de résoudre ce problème. À l’intérieur d’un package, le module . référence le package courant.

__init__.py
1
from . import operations

Mais en plus de cela, on a aussi par exemple .operations qui référence directement le module operations. Écrire from .operations import addition dans __init__.py permettrait de donner accès à la fonction addition directement depuis mypackage.addition.

Quand plusieurs packages sont imbriqués, il est aussi possible de référencer les packages parents avec des syntaxes similaires.

  • .. est le package parent ;
  • ... le grand-parent ;
  • etc.

Ainsi, avec subpackage un sous-répertoire de mypackage, et calcul.py un fichier du répertoire subpackage : calcul peut avoir accès à la fonction addition avec le code suivant.

1
from ..operations import addition

import ... et from ... import ...

Les syntaxes import ... et from ... import ... se ressemblent mais ne sont pas équivalentes dans l’évaluation des modules. Cela devient flagrant lors d’imports circulaires.

import

Prenons deux fichiers a.py et b.py qui seraient présents dans le répertoire courant.

a.py
1
2
3
4
import b

def test():
    print(b.x)
b.py
1
2
3
4
5
6
import a

x = 42

def test():
    a.test()

Puis depuis une console Python :

1
2
3
>>> import b
>>> b.test()
42

Tout fonctionne correctement. Mais essayons de comprendre comment cela se passe derrière.

Premièrement, nous importons b et commençons donc l’évaluation du module. Dès le début de l’évaluation, le module est ajouté aux modules importés. Ce cache permet de ne pas importer plusieurs fois un même module : s’il y est présent, il suffit de le retourner plutôt que de l’évaluer une nouvelle fois.

b commence donc à être évalué. Sa première ligne est celle qui importe le module a. L’interpréteur lance donc l’évaluation du module a.

Ce dernier cherche en premier lieu à importer b. b est déjà présent dans les modules importés, même s’il est actuellement vide, l’import se termine donc correctement. La suite du module a est évaluée, puis la suite du module b l’est aussi.

from ... import

Remplaçons maintenant le contenu du fichier a.py par le suivant.

1
2
3
4
from b import x

def test():                                                                                                                                                                                                                            
    print(x)

Ici, si nous ré-exécutons le code précédent depuis la console, ça pose problème !

1
2
3
4
5
6
7
8
>>> import b
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/home/entwanne/b.py", line 1, in <module>
    import a
  File "/home/entwanne/a.py", line 2, in <module>
    from b import x
ImportError: cannot import name 'x'

Pourquoi cette différence ?

Dès le début du module a, nous demandons maintenant à extraire la variable x du module b. Hors, ce dernier module n’est pas encore entièrement chargé (il est pour l’instant bloqué sur la ligne import a, tant que a n’est pas entièrement évalué), donc il contient pas de valeur x pour le moment.

Cela est aussi dû au fait que les noms de variables utilisés au sein des fonctions ne sont pas résolus avant leur exécution. La ligne print(b.x) ne pose donc pas problème, puisque quand la fonction sera appelée, b aura été entièrement chargé.

On peut reproduire l’erreur, sans from ... import, si on tente directement d’utiliser x depuis le module a.

a.py
1
2
3
4
5
6
import b

def test():
    print(x)

x = b.x

En cas de problèmes similaires liés à des imports circulaires, pensez donc aux import simples !


5 commentaires

Il serait bon de préciser le rôle d’importlib.reload. je m’explique. Peut-être avez-vous essayé de modifier, après le 1er import mypackage, l’init en: from .operations import addition, sans succès. Refaire import mypackage ne permet pas de faire mypackage.addition(). C’est parce qu’il faut recharger le module mypackage, considéré comme déjà importé. from importlib import reload, reload(mypackage) et là c’est ok

C’est quelque chose dont je n’ai pas l’habitude de parler parce que ce n’est pas une bonne pratique pour moi.
On devrait considérer le code comme immuable et donc un module n’a normalement pas de raisons de changer une fois le programme démarré (sauf cas particuliers de plugins chargés dynamiquement par exemple, mais dans ces cas on utilise de toute façon déjà l'importlib).

Connectez-vous pour pouvoir poster un message.
Connexion

Pas encore membre ?

Créez un compte en une minute pour profiter pleinement de toutes les fonctionnalités de Zeste de Savoir. Ici, tout est gratuit et sans publicité.
Créer un compte