Bienvenue sur IndexError.

Ici vous pouvez poser des questions sur Python et le Framework Django.

Mais aussi sur les technos front comme React, Angular, Typescript et Javascript en général.

Consultez la FAQ pour améliorer vos chances d'avoir des réponses à vos questions.

import "relatif" avec et sans package, python2 & python3

+8 votes

Est-ce qu'il existe un moyen "propre" de faire un import "relatif" (meme package), qui fonctionne:

  • Avec python2 et python3
  • Que cela soit un main, ou importé comme part d'un package

Exemple, avec les fichier ci-dessous, pouvoir faire:

python2 ./foo.py
python3 ./foo.py
python2 ./pkg/bar.py
python3 ./pkg/bar.py

foo.py

import pkg.bar

pkg/bar.py

# Que mettre ici ? Aucun ne marche dans tous les cas pré-cités
import baz
import pkg.baz as baz
from . import baz

if __name__ == '__main__':
    print("Running bar")

pkg/baz.py

print("baz imported")
demandé 19-Jan-2015 par ze (308 points)
edité 20-Jan-2015 par ze

Où se trouve le module baz ?

baz se trouve dans pkg, à coté de bar.

./foo.py (import pkg.bar)
./pkg/__init__.py (vide)   
./pkg/bar.py (tente d'importer baz)
./pkg/baz.py

3 Réponses

+1 vote

Je suppose qu'il existe fichier __init__.py dans le répertoire pkg et que tu cherches
à importer baz dans pkg/bar.py même si tu as écrit bar dans le code de ta question.

En python 3, import baz fera échouer l'exécution de foo.py car il part du principe que l'import est absolu et non relatif. Il n'y a pas de baz importable depuis foo, il faut donc rajouter le répertoire pkg au PYTHONPATH pour résoudre l'import.

Concernant import pkg.baz as baz, il me semble qu'on écrit plutôt from pkg import baz ... Dans tous les cas le problème vient de l'exécution du fichier bar.py qui n'a pas accès à son module parent. Pour arranger ça, il suffit d'ajouter le dossier contenant foo.py au PYTHONPATH.

Dans le dernier cas, l'exécution directe des fichiers à l'intérieur du module pkg ne fonctionne pas. Il faut les exécuter en tant que partie du module avec la commande python -m pkg.bar

Tu peux réaliser l'ajout au PYTHONPATH via les variables d'environnement ou via
Python, en rajoutant par exemple les lignes suivantes avant import baz :

import os.path
import sys
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
répondu 19-Jan-2015 par yomytho (296 points)

(import name fixed)

Oki, c'est un workaround, il faudra cependant également ajouter des vérifications pour ne pas ajouter plusieurs fois inutilement le chemin s'il y a de nombreux module qui utiliserait ce méchanisme.

Il y a une réponse assez détaillée (in english) sur Stackoverflow : http://stackoverflow.com/a/16985066/213649

Plutôt que de faire des vérifications qui rendraient la manipulation du PYTHONPATH plus dégueulasse qu'elle ne l'est déjà, autant modifier la variable d'environnement correspondante, ça sera plus propre.

Il y a peut-être une autre question à se poser : a-t-on vraiment besoin de pouvoir exécuter tous les fichiers du module directement ? Si je prend comme exemple django , tu peux créer des commandes au sein de tes applications mais quoi qu'il se passe elles seront exécutée via le script manage.py, situé à la racine de ton projet.

Exemple simpliste :

Dans pkg/bar.py placer l'exécution du print dans une fonction. Le code suivant :

if __name__ == '__main__':
    print("Running bar")

devient :

def main():
    print("Running bar")

Décider de l'exécution de cette fonction dans foo.py :

import sys
import pkg

if sys.argv.pop() == 'bar':
    pkg.bar.main()

sys.argv nous donne accès aux arguments de la commande, on peut donc exécuter les commandes suivantes :

python2 ./foo.py
python3 ./foo.py
python2 ./foo.py bar
python3 ./foo.py bar

Si tu choisis cette solution, utilise docopt plutôt que sys.argv (qui est plus "bas niveau").

0 votes

En cherchant un peu a faire les choses "à la main", je suis arrivé au code suivant.

import importlib
__pkg = ''.join(__name__.rpartition('.')[:2])
baz = importlib.import_module(__pkg + 'baz')

# __pkg gets the package prefix to use:
#
# 'topmodule.module.pkg'
#   => ('topmodule.module', '.', 'pkg')
#   => 'topmodule.module.'
#
# '__main__'
#   => ('', '', '__main__')
#   => ''

L'autre solution proposé est de modifier sys.path, entrainant potentiellement des effets de bords si utilisé depuis d'autres modules.

Que pensez vous des différentes solutions?

répondu 20-Jan-2015 par ze (308 points)
+2 votes

Une solution à mettre dans baz.py :

# activate  python 3 behabvior in Python 2
from __future__ import absolute_import

try:
    # we are doing python pkg/x.py
    import baz
# we are doing python foo.py
except ImportError:
    from . import baz

Mais utiliser python -m "pkg.bar" me parait plus approprié.

Cependant je ne ferais ni l'un ni l'autre personnellement. Je mets toujours tout en import absolu, ça évite bien des problèmes.

Dans ton cas précis, je ferais des sous commandes de foo.py de sorte que :

python script.py foo # import foo et fait son job
python script.py baz # import baz et fait son job
python script.py bar # import baz et fait son job

Avec une lib du genre : https://pypi.python.org/pypi/begins/0.9

Cela a le bénéfice de documenter les paramètres de la commande, permettre de taper --help et voir ce qui est permis, avoir juste un seul point d'entrée à retenir, etc.

En prime, on peut hooker le resultat à dans setup.py pour que ça installe une commande au niveau du système à l'installation du package si besoin.

répondu 20-Jan-2015 par Sam (4,980 points)
edité 20-Jan-2015 par Sam

D'accord avec l'approche, le système d'import de Python est assez permissif et donc réfléchir à une bonne API (avec un point d'entrée recommandé) est essentiel.
Pour s'en convaincre, le code de fabric.api est un bon exemple.

Mais reposer sur un import relatif est plus safe car avec l'absolu, d'où vient baz ? De pkg.baz ou d'un baz fourni par une lib tierce ?
Rien que de me poser la question m'encourage à utiliser la notation relative !

...