Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

Licence CC BY-NC-ND Thierry Parmentelat & Arnaud Legout Inria - UCA

Complément - niveau avancé

# notre utilitaire pour afficher le code des modules
from modtools import show_module, find_on_disk

Attributs spéciaux

Les objets de type module possèdent des attributs spéciaux ; on les reconnaît facilement car leur nom est en __truc__, c’est une convention générale dans tout le langage : on en a déjà vu plusieurs exemples, comme la méthode __iter__().

Voici pour commencer les attributs spéciaux les plus utilisés ; pour cela nous reprenons le package d’un notebook précédent :

import package_jouet
The history saving thread hit an unexpected error (OperationalError('attempt to write a readonly database')).History will not be written to the database.
chargement du package package_jouet
Chargement du module package_jouet.module_jouet dans le package 'package_jouet'
__name__

Le nom canonique du module :

package_jouet.__name__
'package_jouet'
package_jouet.module_jouet.__name__
'package_jouet.module_jouet'
__file__

L’emplacement du fichier duquel a été chargé le module ; pour un package ceci dénote un fichier __init__.py :

package_jouet.__file__
'/__w/course/course/modules/package_jouet/__init__.py'
package_jouet.module_jouet.__file__
'/__w/course/course/modules/package_jouet/module_jouet.py'
__all__

Il est possible de redéfinir dans un module la variable __all__, de façon à définir les symboles qui sont réellement concernés par un import *, comme c’est décrit ici.

Je rappelle toutefois que l’usage de import * est fortement déconseillé dans du code de production.

Import absolu

La mécanique des imports telle qu’on l’a vue jusqu’ici est ce qui s’appelle un import absolu qui est depuis python-2.5 le mécanisme par défaut : le module importé est systématiquement cherché à partir de sys.path.

Dans ce mode de fonctionnement, si on trouve dans le même répertoire deux fichiers foo.py et bar.py, et que dans le premier on fait :

import bar

eh bien, alors qu’il existe ici même un fichier bar.py, l’import ne réussit pas (sauf si le répertoire courant est dans sys.path ; en général ce n’est pas le cas).

Import relatif

Ce mécanisme d’import absolu a l’avantage d’éviter qu’un module local, par exemple random.py, ne vienne cacher le module random de la bibliothèque standard. Mais comment peut-on faire alors pour charger le module random.py local ? C’est à cela que sert l’import relatif.

Voyons cela sur un exemple qui repose sur la hiérarchie suivante :

package_relatif/
                __init__.py  (vide)
                main.py
                random.py

Le fichier __init__.py ici est vide, et voici le code des deux autres modules :

import package_relatif
# le code de main.py
code = find_on_disk(package_relatif, "main.py")
!cat $code

# pour importer un module entier en mode relatif
from . import random as local_random_module


# la syntaxe pour importer seulement un symbole
from .random import alea


print(
    f"""On charge main.py
    __name__={__name__}
    alea={alea()}""")

Nous avons illustré dans le point d’entrée main.py deux exemples d’import relatif :

Les deux clauses as sont bien sûr optionnelles, on les utilise ici uniquement pour bien identifier les différents objets en jeu.

Le module local random.py expose une fonction alea qui génére un string aléatoire en se basant sur le module standard random :

# le code de random.py
code = find_on_disk(package_relatif, "random.py")
!cat $code
import random

print(f"On charge le module random local {__name__}")

def alea():
    return(f"[[{random.randint(0, 10)}]]")

Cet exemple montre comment on peut importer un module local de nom random et le module random qui provient de la librairie standard :

import package_relatif.main
On charge le module random local package_relatif.random
On charge main.py
    __name__=package_relatif.main
    alea=[[6]]
print(package_relatif.main.alea())
[[10]]
Pour remonter dans l’arborescence

Il faut savoir également qu’on peut “remonter” dans l’arborescence de fichiers en utilisant plusieurs points . consécutifs. Voici un exemple fonctionnel, on part du même contenu que ci-dessus avec un sous-package, comme ceci :

package_relatif/
                __init__.py      (vide)
                main.py
                random.py
                subpackage/
                           __init__.py  (vide)
                           submodule.py
# voyons le code de submodule:
import package_relatif.subpackage
# le code de submodule/submodule.py
code = find_on_disk(package_relatif.subpackage, "submodule.py")
!cat $code

# notez ici la présence des deux points pour remonter
from ..random import alea as imported

print(f"On charge {__name__}")

def alea():
    return f"<<{imported()}>>"
import package_relatif.subpackage.submodule
On charge package_relatif.subpackage.submodule
print(package_relatif.subpackage.submodule.alea())
<<[[10]]>>

Ce qu’il faut retenir

Sur cet exemple, on montre comment un import relatif permet à un module d’importer un module local qui a le même nom qu’un module standard.

Avantages de l’import relatif

Bien sûr ici on aurait pu faire

import package_relatif.random

au lieu de

from . import random

Mais l’import relatif présente notamment l’avantage d’être insensible aux renommages divers à l’intérieur d’une bibliothèque.

Dit autrement, lorsque deux modules sont situés dans le même répertoire, il semble naturel que l’import entre eux se fasse par un import relatif, plutôt que de devoir répéter ad nauseam le nom de la bibliothèque - ici package_relatif - dans tous les imports.

Frustrations liées à l’import relatif

Se base sur __name__ et non sur __file__

Toutefois, l’import relatif ne fonctionne pas toujours comme on pourrait s’y attendre. Le point important à garder en tête est que lors d’un import relatif, c’est l’attribut __name__ qui sert à déterminer le point de départ.

Concrètement, lorsque dans main.py on fait :

from . import random

l’interpréteur :

Aussi cet import est-il retranscrit en

from package_relatif import random

De la même manière

from .random import run

devient

from package_relatif.random import run

Par contre l’attribut __file__ n’est pas utilisé : ce n’est pas parce que deux fichiers python sont dans le même répertoire que l’import relatif va toujours fonctionner. Avant de voir cela sur un exemple, il nous faut revenir sur l’attribut __name__.

Digression sur l’attribut __name__

Il faut savoir en effet que le point d’entrée du programme - c’est-à-dire le fichier qui est passé directement à l’interpréteur python - est considéré comme un module dont l’attribut __name__ vaut la chaîne "__main__".

Concrètement, si vous faites

python3 tests/montest.py

alors la valeur observée dans l’attribut __name__ n’est pas "tests.montest", mais la constante "__main__".

C’est pourquoi d’ailleurs (et c’est également expliqué ici) vous trouverez parfois à la fin d’un fichier source une phrase comme celle-ci :

if __name__ == "__main__":
    <faire vraiment quelque chose>
    <comme par exemple tester le module>

Cet idiome très répandu permet d’insérer à la fin d’un module du code - souvent un code de test - qui :

L’attribut __package__

Pour résumer :

Du coup, par construction, il n’est quasiment pas possible d’utiliser les imports relatifs à partir du script de lancement.

Pour pallier à ce type d’inconvénients, il a été introduit ultérieurement (voir PEP 366 ci-dessous) la possibilité pour un module de définir (écrire) l’attribut __package__, pour contourner cette difficulté.

Ce qu’il faut retenir

On voit que tout ceci est rapidement assez scabreux. Cela explique sans doute l’usage relativement peu répandu des imports relatifs.

De manière générale, une bonne pratique consiste à :

S’agissant des tests :

python3 -m unittest tests.jeu_de_tests

et dans ce contexte-là, il est possible par exemple pour les tests de recourir à l’import relatif.

Pour en savoir plus

Vous pourrez consulter :