Question Importations relatives dans Python 3


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

Parfois ça marche pour moi avec from .mymodule import myfunction mais parfois je reçois un:

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

Parfois ça marche avec from mymodule import myfunction, mais parfois je reçois aussi:

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

Je ne comprends pas la logique ici, et je n'ai trouvé aucune explication. Cela semble complètement aléatoire.

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


416
2018-06-07 10:26


origine


Réponses:


malheureusement, ce module doit être à l'intérieur du paquet, et il   doit être exécutable en tant que script, parfois. Toute idée comment je pourrais   atteindre cela?

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 __name__ == '__main__':
    _test()

...une 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 __name__ == '__main__':
    _test()

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

#!/usr/bin/env python3

from mypackage.myothermodule import add

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

if __name__ == '__main__':
    main()

... qui fonctionne bien quand vous courez 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 de shebang comme #!/usr/bin/env python3.

La solution la plus simple pour ce cas, en supposant que le nom mymodule est globalement unique, serait d'éviter d'utiliser des importations relatives, et il suffit d'utiliser ...

from mymodule import as_int

... bien que, si ce n'est pas unique, ou que la structure de votre paquet soit plus complexe, vous devrez inclure le répertoire contenant votre répertoire de paquetage dans PYTHONPATHet faites comme ça ...

from mypackage.mymodule import as_int

... ou si vous voulez que cela fonctionne "hors de la boîte", vous pouvez frober le PYTHONPATH dans le code d'abord avec ça ...

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 une douleur, mais il y a une idée de pourquoi dans un courriel écrit par un certain Guido van Rossum ...

Je suis -1 sur ce sujet et sur tous les autres twiddlings proposés de la __main__   machinerie. Le seul cas d'utilisation semble être l'exécution de scripts qui se produisent   vivre dans le répertoire d'un module, que j'ai toujours vu comme un   antipattern. Pour me faire changer d’avis, vous devez me convaincre que   ce n'est pas

Que l'exécution de scripts dans un package soit un antipattern ou non est subjective, mais personnellement, je trouve cela très utile dans un paquet que j'ai qui contient des widgets wxPython personnalisés, donc je peux exécuter le script pour l'un des fichiers source pour afficher un wx.Frame contenant uniquement ce widget à des fins de test.


334
2018-06-07 13:14



Explication

De PEP 328

Les importations relatives utilisent l'attribut __name__ d'un module pour déterminer   la position du module dans la hiérarchie du paquet. Si le nom du module   ne contient aucune information de package (par exemple, elle est définie sur "__main__")    puis les importations relatives sont résolues comme si le module était un niveau supérieur   module, quel que soit l'emplacement du module sur le fichier   système.

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

... les importations relatives dépendent de __prénom__ déterminer le courant   la position du module dans la hiérarchie du paquet. Dans un module principal, le   valeur de __prénom__ est toujours '__principale__', les importations relatives explicites   échouera toujours (car ils ne fonctionnent que pour un module à l'intérieur d'un package)

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

En ajoutant un nouvel attribut au niveau du module, ce PEP permet   les importations pour travailler automatiquement si le module est exécuté en utilisant le -m   commutateur. Une petite quantité de passe-partout dans le module lui-même permettra   les importations relatives à travailler lorsque le fichier est exécuté par nom. [...] Quand [l'attribut] est présent, les importations relatives seront basées sur cet attribut   plutôt que le module __prénom__ attribut. [...] Lorsque le module principal est spécifié par son nom de fichier, le __paquet__ l'attribut sera défini sur Aucun. [...] Lorsque le système d'importation rencontre une importation relative explicite dans un   module sans __package__ set (ou avec set sur None), il sera   calculer et stocker la valeur correcte (__name __. rpartition ('.') [0]   pour les modules normaux et __prénom__ pour les modules d'initialisation des paquets)

(emphase mienne)

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

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

La partie pertinente du CPython PyImport_ImportModuleLevelObject fonction:

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

CPython lève cette exception s'il n'a pas pu trouver package (le nom du paquet) dans interp->modules (accessible en tant que sys.modules). Depuis sys.modules est "un dictionnaire qui associe les noms de modules aux modules déjà chargés", il est maintenant clair que le module parent doit être explicitement importé en absolu avant d'effectuer l'importation relative.

Remarque:  Le patch de la numéro 18018 a ajouté un autre if bloc, 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 verrez cela 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 paquet):

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

Tous les fichiers dans paquet Commençons par les mêmes 2 lignes de code:

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

J'inclus ces deux lignes seulement 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 contiennent 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 le -m option de ligne de commande ça va "chercher sys.path pour le module nommé et exécuter son contenu en tant que __main__ module":

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 toutes les choses d'importation pour vous et définit automatiquement __package__, mais vous pouvez le faire vous-même dans le

Solution # 2: Définir __package__ manuellement

Veuillez le traiter comme une preuve de concept plutôt que comme une solution réelle. Il n'est pas bien adapté pour une utilisation dans le code réel.

PEP 366a une solution de contournement à ce problème, cependant, il est incomplet, car le réglage __package__ seul ne suffit pas. Vous allez devoir importer au moins N les paquets précédents dans la hiérarchie du module, où N est le nombre de répertoires parents (relatifs au répertoire du script) qui seront recherchés pour le module en cours d'importation.

Ainsi,

  1. Ajouter le répertoire parent du Nième prédécesseur du module actuel à sys.path

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

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

  4. Ensemble __package__ au nom qualifié complet de 2

  5. Effectuer l'importation relative

Je vais emprunter des fichiers de la Solution n ° 1 et ajoutez plus de sous-paquetages:

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

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

from ... import module  # N = 3

Nous devrons précéder cette ligne avec le code standard, pour que cela fonctionne.

import sys
from pathlib import Path

if __name__ == '__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

Une solution plus générale enveloppée dans une fonction peut être trouvée ici. Exemple d'utilisation:

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

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

Solution n ° 3: utiliser des importations absolues et setuptools

Les étapes sont -

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

  2. Installer package pour le rendre importable

Par exemple, la structure du répertoire peut être comme suit

.
├── 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 au Solution n ° 1.

L'installation vous permettra d'importer le paquet indépendamment de votre répertoire de travail (en supposant qu'il n'y aura pas de problèmes de nommage).

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

from package import module  # absolute import

Changez votre répertoire de travail pour project et courir /path/to/python/interpreter setup.py install --user (--user installe le paquet dans votre répertoire site-packages) (é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'>

Remarque: Si vous décidez de suivre cette route, vous feriez mieux d'utiliser environnements virtuels installer des paquets isolément.

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

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

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

  1. Ajouter le répertoire parent de paquet à sys.path  avant essayer d'importer quelque chose de paquet 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. Remplacer l'importation relative par l'importation absolue:

    from package import module  # absolute import
    

standalone.py fonctionne 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 sens que je devrais vous avertir: essayez de ne pas le faire, notamment si votre projet a une structure complexe.


En guise de note, PEP 8 recommande l'utilisation d'importations absolues, mais déclare que dans certains scénarios, les importations relatives explicites sont acceptables:

Les importations absolues sont recommandées, car elles sont généralement plus lisibles   et ont tendance à mieux se comporter (ou au moins donner une meilleure erreur   messages). [...] Cependant, les importations relatives explicites sont acceptables   alternative aux importations absolues, en particulier lorsqu'il s'agit de   dispositions d'emballage où l'utilisation des importations absolues serait inutilement   verbeux.


154
2018-01-26 16:54



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

#!/usr/bin/env python3
#myothermodule

if __name__ == '__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 __name__ == '__main__':
    _test()

28
2018-01-26 14:21



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 est comme ceci:

├── 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.


9
2018-03-20 03:07



Si les deux paquets sont dans votre chemin d’importation (sys.path) et que le module / classe que vous voulez est dans example / example.py, alors pour accéder à la classe sans importation relative, essayez:

from example.example import fkt

1
2018-03-09 22:18



Pour éviter ce problème, j'ai conçu une solution avec le reconditionnement paquet, qui a travaillé pour moi pendant un certain temps. Il ajoute le répertoire supérieur au chemin lib:

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

Le reconditionnement peut faire des importations relatives qui fonctionnent dans un large éventail de cas, en utilisant une stratégie intelligente (inspecter la pile d'appels).


1
2017-12-06 09:26



J'espère que cela sera utile pour quelqu'un d'autre - j'ai parcouru une demi-douzaine de messages stackoverflow essayant de déterminer les importations relatives similaires à celles affiché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 ne faisais que développer localement et jouer, je n'avais pas créé setup.py fichier. Je n'avais pas non plus apparemment mis mon PYTHONPATH.

Je me suis rendu compte que lorsque je courais mon code comme je l'avais été lorsque les tests étaient dans le même répertoire que le module, je ne pouvais pas 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 que 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 à celle de moi-même 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éer un setup.py fichier avec le contenu comme le suivant et exécuter python setup.py development pour ajouter des paquets au chemin:
# setup.py
from setuptools import setup, find_packages

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

1
2018-04-17 19:40