Explication
À partir de PEP 328
Les importations relatives utilisent l'attribut __name__ 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 sur le système de fichiers.
À un moment donné, le PEP 338 est entré en conflit avec le PEP 328 :
... les importations relatives s'appuient sur __name__ pour déterminer la position actuelle du module dans la hiérarchie des packages. Dans un module principal, la valeur de __name__ est toujours '__main__' , donc les importations relatives explicites échoueront toujours (car elles ne fonctionnent que pour un module à l'intérieur d'un package)
et pour résoudre le problème, le PEP 366 a introduit la variable de niveau supérieur __package__
:
En ajoutant un nouvel attribut au niveau du 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 sera exécuté par son nom. [...] Lorsqu'il [l'attribut] est présent, les importations relatives seront basées sur cet attribut plutôt que sur l' attribut module __name__ . [...] Lorsque le module principal est spécifié par son nom de fichier, l' attribut __package__ sera défini sur Aucun . [...] Lorsque le système d'importation rencontre une importation relative explicite dans un module sans __package__ défini (ou avec défini sur Aucun), il calcule et stocke la valeur correcte (__name __. rpartition ('.') [0] pour les modules normaux et __name__ pour les modules d'initialisation de paquet)
(c'est moi qui souligne)
Si __name__
est '__main__'
, __name__.rpartition('.')[0]
renvoie une chaîne vide. C'est pourquoi il y a 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 PyImport_ImportModuleLevelObject
fonction de 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 lève cette exception s'il n'a pas pu trouver package
(le nom du package) dans interp->modules
(accessible en tant que sys.modules
). Puisqu'il sys.modules
s'agit d' un "dictionnaire qui mappe les noms des modules aux modules qui ont déjà été chargés" , il est désormais clair que le module parent doit être explicitement importé en absolu avant d'effectuer une importation relative .
Remarque: Le correctif du problème 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 qu'en Python 3.6 ou plus récent.
Solution n ° 1: exécutez votre script à l'aide de -m
Considérez un répertoire (qui est un package Python ):
.
├── package
│ ├── __init__.py
│ ├── module.py
│ └── standalone.py
Tous les fichiers du package commencent par les 2 mêmes lignes de code:
from pathlib import Path
print('Running' if __name__ == '__main__' else 'Importing', Path(__file__).resolve())
J'inclus ces deux lignes uniquement 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 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
cela échouera. Cependant, nous pouvons exécuter le module avec l' -m
option de ligne de commande qui "recherchera sys.path
le module nommé et exécutera 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
effectue toutes les opérations d'importation pour vous et définit automatiquement __package__
, mais vous pouvez le faire vous-même dans le
Solution n ° 2: définissez __package__ manuellement
Veuillez le considérer comme une preuve de concept plutôt qu'une solution réelle. Il n'est pas bien adapté à une utilisation dans le code du monde réel.
PEP 366 a une solution de contournement à ce problème, cependant, il est incomplet, car le réglage __package__
seul n'est pas suffisant. Vous allez devoir importer au moins N 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) qui sera recherché pour le module importé.
Donc,
Ajoutez le répertoire parent du Nème prédécesseur du module actuel àsys.path
Supprimer le répertoire du fichier actuel de sys.path
Importez le module parent du module actuel en utilisant son nom complet
Définissez __package__
le nom complet de 2
Effectuer l'importation relative
J'emprunterai des fichiers à la Solution # 1 et ajouterai d'autres sous-packages:
package
├── __init__.py
├── module.py
└── subpackage
├── __init__.py
└── subsubpackage
├── __init__.py
└── standalone.py
Cette fois, standalone.py importera module.py à partir du package de package à l'aide de l'importation relative suivante
from ... import module # N = 3
Nous devrons faire précéder cette ligne du code passe-partout 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
Il 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: utilisez les importations absolues et les setuptools
Les étapes sont -
Remplacer les importations relatives explicites par des importations absolues équivalentes
Installer package
pour le rendre importable
Par exemple, la structure du répertoire peut être la suivante
.
├── project
│ ├── package
│ │ ├── __init__.py
│ │ ├── module.py
│ │ └── standalone.py
│ └── setup.py
où setup.py est
from setuptools import setup, find_packages
setup(
name = 'your_package_name',
packages = find_packages(),
)
Les autres fichiers ont été empruntés à la solution n ° 1 .
L'installation vous permettra d'importer le package quel que soit votre répertoire de travail (en supposant qu'il n'y aura aucun problème de dénomination).
Nous pouvons modifier standalone.py pour utiliser cet avantage (étape 1):
from package import module # absolute import
Modifiez votre répertoire de travail project
et exécutez /path/to/python/interpreter setup.py install --user
( --user
installe le package 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 désormais 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 voie, il serait préférable d'utiliser des environnements virtuels pour installer les packages de manière isolée.
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 faire fonctionner les importations absolues.
Je vais emprunter des fichiers à la Solution # 1 et changer standalone.py :
Ajoutez le répertoire parent du package à sys.path
avant d' essayer d'importer quoi que ce soit du package à l' aide d'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
Remplacez l'importation relative par l'importation absolue:
from package import module # absolute import
standalone.py s'exécute sans problème:
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 dois vous avertir: essayez de ne pas le faire, surtout si votre projet a une structure complexe.
En guise de note complémentaire , le 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 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 de colis complexes où l'utilisation d'importations absolues serait inutilement verbeuse.