web-dev-qa-db-fra.com

Importations relatives dans Python 3

Je veux importer une fonction d'un autre fichier dans le même répertoire.

Parfois, cela fonctionne pour moi avec from .mymodule import myfunction mais parfois, je reçois un:

SystemError: Parent module '' not loaded, cannot perform relative import

Parfois, cela fonctionne avec from mymodule import myfunction, mais parfois, je reçois aussi un:

SystemError: Parent module '' not loaded, cannot perform relative import

Je ne comprends pas la logique ici et je n’ai trouvé aucune explication. Cela a l'air complètement aléatoire.

Quelqu'un pourrait-il m'expliquer quelle est la logique derrière tout cela?

544

malheureusement, ce module doit être à l'intérieur du paquet, et il doit aussi parfois pouvoir être exécuté en tant que script. Une idée de comment je pourrais y arriver?

Il est assez courant d'avoir une mise en page comme celle-ci ...

main.py
mypackage/
    __init__.py
    mymodule.py
    myothermodule.py

... avec un mymodule.py comme ça ...

#!/usr/bin/env python3

# Exported function
def as_int(a):
    return int(a)

# Test function for module  
def _test():
    assert as_int('1') == 1

if __== '__main__':
    _test()

... un myothermodule.py comme ça ...

#!/usr/bin/env python3

from .mymodule import as_int

# Exported function
def add(a, b):
    return as_int(a) + as_int(b)

# Test function for module  
def _test():
    assert add('1', '1') == 2

if __== '__main__':
    _test()

... et un main.py comme ça ...

#!/usr/bin/env python3

from mypackage.myothermodule import add

def main():
    print(add('1', '1'))

if __== '__main__':
    main()

... qui fonctionne bien lorsque vous exécutez main.py ou mypackage/mymodule.py, mais échoue avec mypackage/myothermodule.py, en raison de l'importation relative ...

from .mymodule import as_int

La façon dont vous êtes censé l'exécuter est ...

python3 -m mypackage.myothermodule

... mais c'est un peu verbeux, et ne se mélange pas bien avec une ligne Shebang comme #!/usr/bin/env python3.

Le correctif le plus simple pour ce cas, en supposant que le nom mymodule soit globalement unique, consisterait à éviter d'utiliser les importations relatives, et simplement d'utiliser ...

from mymodule import as_int

... bien que, si ce n'est pas unique, ou si la structure de votre paquet est plus complexe, vous devrez inclure le répertoire contenant votre répertoire de paquet dans PYTHONPATH, et procédez comme suit ...

from mypackage.mymodule import as_int

... ou si vous voulez que cela fonctionne "out of the box", vous pouvez commencer par effacer le PYTHONPATH dans le code avec ceci ...

import sys
import os

PACKAGE_PARENT = '..'
SCRIPT_DIR = os.path.dirname(os.path.realpath(os.path.join(os.getcwd(), os.path.expanduser(__file__))))
sys.path.append(os.path.normpath(os.path.join(SCRIPT_DIR, PACKAGE_PARENT)))

from mypackage.mymodule import as_int

C'est un peu pénible, mais il y a une idée de pourquoi dans n email écrit par un certain Guido van Rossum ...

Je suis -1 sur ce sujet et sur tout autre système proposé de la __main__ machine. Le seul cas d'utilisation semble être l'exécution de scripts vivant dans le répertoire d'un module, ce que j'ai toujours considéré comme un anti-modèle. Pour me faire changer d'avis, il faudrait me convaincre que ce n'est pas le cas.

Le fait d'exécuter des scripts à l'intérieur d'un paquet est un antipattern ou non, c'est subjectif, mais personnellement, je le trouve vraiment utile dans un paquet contenant des widgets wxPython personnalisés. Je peux donc exécuter le script de n'importe quel fichier source pour afficher un wx.Frame ne contenant que ce widget à des fins de test.

424
Aya

Explication

À partir de PEP 328

Les importations relatives utilisent l'attribut __ d'un module pour déterminer la position de ce module dans la hiérarchie des packages. Si le nom du module ne contient aucune information sur le package (par exemple, il est défini sur '__main__') , les importations relatives sont résolues comme si le module était un module de niveau supérieur , quel que soit l'emplacement du module. est actuellement situé sur le système de fichiers.

À un moment donné, PEP 338 était en conflit avec PEP 328 :

... les importations relatives reposent sur _ NAME _ == pour déterminer la position du module actuel dans la hiérarchie des packages. Dans un module principal, la valeur de _ NAME _ est toujours '__ main __' , de sorte que les importations relatives explicites échouent toujours travailler pour un module dans un paquet)

et pour résoudre le problème, PEP 366 a introduit la variable de niveau supérieur __package__ :

En ajoutant un nouvel attribut de niveau de module, ce PEP permet aux importations relatives de fonctionner automatiquement si le module est exécuté à l'aide du commutateur - m . Une petite quantité de passe-partout dans le module lui-même permettra aux importations relatives de fonctionner lorsque le fichier est exécuté par son nom. [...] Quand [l'attribut] est présent, les importations relatives seront basées sur cet attribut plutôt que sur le module _ NOM _ attribut. [...] Lorsque le module principal est spécifié par son nom de fichier, l'attribut _ PACKAGE _ == (= ++) est défini sur None . [...] Lorsque le système d'importation rencontre une importation relative explicite dans un module sans __package__ défini (ou défini sur Aucune), il calcule et stocke la valeur correcte ( __ nom __. rpartition ('.') [0] pour les modules normaux et _ NOM _ pour les modules d'initialisation de package)

(c'est moi qui souligne)

Si __name__ est '__main__', __name__.rpartition('.')[0] renvoie une chaîne vide. C'est pourquoi il existe un littéral de chaîne vide dans la description de l'erreur:

SystemError: Parent module '' not loaded, cannot perform relative import

La partie pertinente de la fonction PyImport_ImportModuleLevelObject du CPython :

if (PyDict_GetItem(interp->modules, package) == NULL) {
    PyErr_Format(PyExc_SystemError,
            "Parent module %R not loaded, cannot perform relative "
            "import", package);
    goto error;
}

CPython déclenche cette exception s'il n'a pas pu trouver package (nom du package) dans interp->modules (accessible sous sys.modules ). Puisque sys.modules est "un dictionnaire mappant les noms de modules sur des modules déjà chargés" , il est maintenant clair que le module parent doit être explicitement importé avant effectuer une importation relative .

Note: Le correctif du numéro 18018 a ajouté une autre if block , qui sera exécuté avant le code ci-dessus:

if (PyUnicode_CompareWithASCIIString(package, "") == 0) {
    PyErr_SetString(PyExc_ImportError,
            "attempted relative import with no known parent package");
    goto error;
} /* else if (PyDict_GetItem(interp->modules, package) == NULL) {
    ...
*/

Si package (comme ci-dessus) est une chaîne vide, le message d'erreur sera

ImportError: attempted relative import with no known parent package

Cependant, vous ne le verrez que dans Python 3.6 ou plus récent.

Solution n ° 1: Exécutez votre script avec -m

Considérons un répertoire (qui est un Python package ):

.
├── package
│   ├── __init__.py
│   ├── module.py
│   └── standalone.py

Tous les fichiers de package commencent par les mêmes 2 lignes de code:

from pathlib import Path
print('Running' if __== '__main__' else 'Importing', Path(__file__).resolve())

J'inclus ces deux lignes seulement pour rendre l'ordre des opérations évident. Nous pouvons les ignorer complètement, car ils n’affectent pas l’exécution.

__ init __. py et module.py ne contient que ces deux lignes (c’est-à-dire qu’elles sont effectivement vides).

standalone.py tente en outre d'importer module.py via l'importation relative:

from . import module  # explicit relative import

Nous savons bien que /path/to/python/interpreter package/standalone.py va échouer. Cependant, nous pouvons exécuter le module avec l'option de ligne de commande -m qui "recherche sys.path pour le module nommé et exécuter son contenu. en tant que module __main__ ":

vaultah@base:~$ python3 -i -m package.standalone
Importing /home/vaultah/package/__init__.py
Running /home/vaultah/package/standalone.py
Importing /home/vaultah/package/module.py
>>> __file__
'/home/vaultah/package/standalone.py'
>>> __package__
'package'
>>> # The __package__ has been correctly set and module.py has been imported.
... # What's inside sys.modules?
... import sys
>>> sys.modules['__main__']
<module 'package.standalone' from '/home/vaultah/package/standalone.py'>
>>> sys.modules['package.module']
<module 'package.module' from '/home/vaultah/package/module.py'>
>>> sys.modules['package']
<module 'package' from '/home/vaultah/package/__init__.py'>

-m fait tout le travail d'importation pour vous et définit automatiquement __package__, mais vous pouvez le faire vous-même dans le menu déroulant.

Solution n ° 2: Configurez __package__ manuellement

Veuillez le considérer comme une preuve de concept plutôt que comme une solution réelle. Il ne convient pas pour une utilisation dans du code du monde réel.

PEP 366 propose une solution de contournement à ce problème, mais elle est incomplète, car la définition de __package__ seul ne suffit pas. Vous devez importer au moins N les packages précédents dans la hiérarchie des modules, où N est le nombre de répertoires parents (par rapport au répertoire du script) dans lesquels le module importé sera recherché.

Ainsi,

  1. Ajoutez le répertoire parent du prédécesseur Nth du module actuel à sys.path

  2. Supprimer le répertoire du fichier actuel de sys.path

  3. Importer le module parent du module actuel en utilisant son nom complet

  4. Définissez __package__ sur le nom complet de 2

  5. Effectuer l'importation relative

J'emprunterai des fichiers de Solution # 1 et ajouterai quelques sous-packages supplémentaires:

package
├── __init__.py
├── module.py
└── subpackage
    ├── __init__.py
    └── subsubpackage
        ├── __init__.py
        └── standalone.py

Cette fois standalone.py importera module.py du package le package en utilisant l'importation relative suivante

from ... import module  # N = 3

Nous devrons faire précéder cette ligne du code standard pour que cela fonctionne.

import sys
from pathlib import Path

if __== '__main__' and __package__ is None:
    file = Path(__file__).resolve()
    parent, top = file.parent, file.parents[3]

    sys.path.append(str(top))
    try:
        sys.path.remove(str(parent))
    except ValueError: # Already removed
        pass

    import package.subpackage.subsubpackage
    __package__ = 'package.subpackage.subsubpackage'

from ... import module # N = 3

Cela nous permet d’exécuter standalone.py par nom de fichier:

vaultah@base:~$ python3 package/subpackage/subsubpackage/standalone.py
Running /home/vaultah/package/subpackage/subsubpackage/standalone.py
Importing /home/vaultah/package/__init__.py
Importing /home/vaultah/package/subpackage/__init__.py
Importing /home/vaultah/package/subpackage/subsubpackage/__init__.py
Importing /home/vaultah/package/module.py

Vous trouverez une solution plus générale englobée dans une fonction here . Exemple d'utilisation:

if __== '__main__' and __package__ is None:
    import_parents(level=3) # N = 3

from ... import module
from ...module.submodule import thing

Solution n ° 3: Utilisez les importations absolues et setuptools

Les étapes sont -

  1. Remplacer les importations relatives explicites par des importations absolues équivalentes

  2. Installez package pour le rendre importable

Par exemple, la structure de répertoire peut être la suivante

.
├── project
│   ├── package
│   │   ├── __init__.py
│   │   ├── module.py
│   │   └── standalone.py
│   └── setup.py

setup.py est

from setuptools import setup, find_packages
setup(
    name = 'your_package_name',
    packages = find_packages(),
)

Le reste des fichiers ont été empruntés à Solution # 1 .

L’installation vous permettra d’importer le paquet quel que soit votre répertoire de travail (en supposant qu’il n’y aura pas de problème de nommage).

Nous pouvons modifier standalone.py pour utiliser cet avantage (étape 1):

from package import module  # absolute import

Changez votre répertoire de travail en project et exécutez /path/to/python/interpreter setup.py install --user (--user installe le package dans votre répertoire de packages site ) (étape 2):

vaultah@base:~$ cd project
vaultah@base:~/project$ python3 setup.py install --user

Vérifions qu'il est maintenant possible d'exécuter standalone.py en tant que script:

vaultah@base:~/project$ python3 -i package/standalone.py
Running /home/vaultah/project/package/standalone.py
Importing /home/vaultah/.local/lib/python3.6/site-packages/your_package_name-0.0.0-py3.6.Egg/package/__init__.py
Importing /home/vaultah/.local/lib/python3.6/site-packages/your_package_name-0.0.0-py3.6.Egg/package/module.py
>>> module
<module 'package.module' from '/home/vaultah/.local/lib/python3.6/site-packages/your_package_name-0.0.0-py3.6.Egg/package/module.py'>
>>> import sys
>>> sys.modules['package']
<module 'package' from '/home/vaultah/.local/lib/python3.6/site-packages/your_package_name-0.0.0-py3.6.Egg/package/__init__.py'>
>>> sys.modules['package.module']
<module 'package.module' from '/home/vaultah/.local/lib/python3.6/site-packages/your_package_name-0.0.0-py3.6.Egg/package/module.py'>

Note : Si vous décidez de suivre cette voie, vous feriez mieux d'utiliser virtual environment installer des paquets en isolation.

Solution n ° 4: Utilisez des importations absolues et du code standard

Franchement, l'installation n'est pas nécessaire - vous pouvez ajouter du code passe-partout à votre script pour que les importations absolues fonctionnent.

Je vais emprunter des fichiers de Solution # 1 et changer standalone.py :

  1. Ajoutez le répertoire parent de package à sys.path avant en essayant d'importer quoi que ce soit de package en utilisant des importations absolues:

    import sys
    from pathlib import Path # if you haven't already done so
    file = Path(__file__).resolve()
    parent, root = file.parent, file.parents[1]
    sys.path.append(str(root))
    
    # Additionally remove the current file's directory from sys.path
    try:
        sys.path.remove(str(parent))
    except ValueError: # Already removed
        pass
    
  2. Remplacez l'importation relative par l'importation absolue:

    from package import module  # absolute import
    

standalone.py s'exécute sans problèmes:

vaultah@base:~$ python3 -i package/standalone.py
Running /home/vaultah/package/standalone.py
Importing /home/vaultah/package/__init__.py
Importing /home/vaultah/package/module.py
>>> module
<module 'package.module' from '/home/vaultah/package/module.py'>
>>> import sys
>>> sys.modules['package']
<module 'package' from '/home/vaultah/package/__init__.py'>
>>> sys.modules['package.module']
<module 'package.module' from '/home/vaultah/package/module.py'>

Je pense que je devrais vous avertir: essayez de ne pas le faire, en particulier si votre projet a une structure complexe.


Remarque: PEP 8 recommande l’utilisation des importations absolues, mais indique que, dans certains cas, les importations relatives explicites sont acceptables:

Les importations absolues sont recommandées, car elles sont généralement plus lisibles et ont tendance à être plus saines (ou du moins à donner de meilleurs messages d'erreur). [...] Cependant, les importations relatives explicites sont une alternative acceptable aux importations absolues, en particulier lorsqu'il s'agit de présentations complexes, dans lesquelles l'utilisation d'importations absolues serait inutilement verbeuse.

200
vaultah

Mettez ceci dans le fichier __init__.py de votre paquet:

# For relative imports to work in Python 3.6
import os, sys; sys.path.append(os.path.dirname(os.path.realpath(__file__)))

En supposant que votre paquet soit comme ça:

├── project
│   ├── package
│   │   ├── __init__.py
│   │   ├── module1.py
│   │   └── module2.py
│   └── setup.py

Maintenant, utilisez des importations régulières dans votre paquet, comme:

# in module2.py
from module1 import class1

Cela fonctionne à la fois dans python 2 et 3.

52
am5

J'ai rencontré ce problème. Une solution de contournement de hack est importée via un bloc if/else comme suit:

#!/usr/bin/env python3
#myothermodule

if __== '__main__':
    from mymodule import as_int
else:
    from .mymodule import as_int


# Exported function
def add(a, b):
    return as_int(a) + as_int(b)

# Test function for module  
def _test():
    assert add('1', '1') == 2

if __== '__main__':
    _test()
33
goffer

J'espère que cela sera utile à quelqu'un - j'ai parcouru une demi-douzaine de messages superposés en essayant de déterminer des importations relatives similaires à celles publiées ci-dessus. J'ai tout mis en place comme suggéré mais je frappais toujours ModuleNotFoundError: No module named 'my_module_name'

Comme je développais juste localement et que je jouais, je n'avais pas créé/exécuté de fichier setup.py. Je n’avais pas non plus apparemment réglé mon PYTHONPATH.

J'ai réalisé que lorsque j'ai exécuté mon code comme je l'avais été lorsque les tests étaient dans le même répertoire que le module, je n'ai pas pu trouver mon module:

$ python3 test/my_module/module_test.py                                                                                                               2.4.0
Traceback (most recent call last):
  File "test/my_module/module_test.py", line 6, in <module>
    from my_module.module import *
ModuleNotFoundError: No module named 'my_module'

Cependant, quand j’ai explicitement spécifié le chemin, les choses ont commencé à fonctionner:

$ PYTHONPATH=. python3 test/my_module/module_test.py                                                                                                  2.4.0
...........
----------------------------------------------------------------------
Ran 11 tests in 0.001s

OK

Donc, si quelqu'un a essayé quelques suggestions, pense que son code est structuré correctement et se trouve toujours dans une situation similaire, essayez l'une des solutions suivantes si vous n'exportez pas le répertoire actuel vers votre PYTHONPATH:

  1. Exécutez votre code et incluez explicitement le chemin comme ceci: $ PYTHONPATH=. python3 test/my_module/module_test.py
  2. Pour éviter d'appeler PYTHONPATH=., créez un fichier setup.py avec le contenu suivant et exécutez python setup.py development pour ajouter des packages au chemin:
# setup.py
from setuptools import setup, find_packages

setup(
    name='sample',
    packages=find_packages()
)
4
LaCroixed

Pour éviter ce problème, j’ai trouvé une solution avec le paquetage repackage , qui fonctionne depuis un certain temps. Il ajoute le répertoire supérieur au chemin de la bibliothèque:

import repackage
repackage.up()
from mypackage.mymodule import myfunction

Le reconditionnement peut effectuer des importations relatives qui fonctionnent dans un grand nombre de cas, en utilisant une stratégie intelligente (inspection de la pile d'appels).

3
fralau

J'avais besoin d'exécuter python3 à partir du répertoire principal du projet pour que cela fonctionne.

Par exemple, si le projet a la structure suivante:

project_demo/
├── main.py
├── some_package/
│   ├── __init__.py
│   └── project_configs.py
└── test/
    └── test_project_configs.py

Solution

Je voudrais exécuter python3 dans le dossier project_demo / et ensuite effectuer une

from some_package import project_configs
2
Árthur

si les deux packages sont dans votre chemin d'importation (sys.path) et que le module/classe souhaité est dans exemple/exemple.py, pour accéder à la classe sans importation relative, essayez:

from example.example import fkt
1
Salt999