Wyjaśnienie
Od PEP 328
Względne importy używają atrybutu __name__ modułu, aby określić pozycję tego modułu w hierarchii pakietów. Jeśli nazwa modułu nie zawiera żadnych informacji o pakiecie (np. Jest ustawiona na „__main__”),
importy względne są rozstrzygane tak, jakby moduł był modułem najwyższego poziomu , niezależnie od tego, gdzie moduł faktycznie znajduje się w systemie plików.
W pewnym momencie PEP 338 był w konflikcie z PEP 328 :
... import względny opiera się na __name__ w celu ustalenia aktualnej pozycji modułu w hierarchii pakietów. W głównym module wartość __name__ to zawsze „__main__” , więc jawne importowanie względne zawsze się nie powiedzie (ponieważ działają tylko dla modułu wewnątrz pakietu)
Aby rozwiązać ten problem, PEP 366 wprowadził zmienną najwyższego poziomu __package__
:
Dodając nowy atrybut na poziomie modułu, ten PEP pozwala na automatyczne importowanie względne, jeśli moduł jest wykonywany za pomocą
przełącznika -m . Niewielka ilość płyty kotłowej w samym module pozwoli na działanie importu względnego, gdy plik jest wykonywany według nazwy. [...] Gdy [atrybut] jest obecny, import względny będzie oparty na tym atrybucie, a nie na module __name__ . [...] Gdy główny moduł jest określony nazwą pliku, wówczas atrybut __package__ zostanie ustawiony na Brak . [...] Gdy system importu napotka jawny import względny w module bez zestawu __package__ (lub z ustawionym na None), obliczy i zapisze poprawną wartość (__name __. rpartition ('.') [0] dla normalnych modułów i __name__ dla modułów inicjalizujących pakiety)
(moje podkreślenie)
Jeśli __name__
jest '__main__'
, __name__.rpartition('.')[0]
zwraca pusty ciąg. Oto dlaczego w opisie błędu występuje literał pusty łańcuch:
SystemError: Parent module '' not loaded, cannot perform relative import
Istotna część tego CPython jest PyImport_ImportModuleLevelObject
funkcją :
if (PyDict_GetItem(interp->modules, package) == NULL) {
PyErr_Format(PyExc_SystemError,
"Parent module %R not loaded, cannot perform relative "
"import", package);
goto error;
}
CPython zgłasza ten wyjątek, jeśli nie mógł znaleźć package
(nazwa pakietu) w interp->modules
(dostępny jako sys.modules
). Ponieważ sys.modules
jest to „słownik, który odwzorowuje nazwy modułów na moduły, które zostały już załadowane” , jest teraz jasne, że moduł nadrzędny musi zostać jawnie zaimportowany absolutnie przed wykonaniem importu względnego .
Uwaga: Łatka z wydania 18018 dodała kolejny if
blok , który zostanie wykonany przed powyższym kodem:
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) {
...
*/
Jeśli package
(tak jak powyżej) jest pusty ciąg, pojawi się komunikat o błędzie
ImportError: attempted relative import with no known parent package
Jednak zobaczysz to tylko w Pythonie 3.6 lub nowszym.
Rozwiązanie nr 1: Uruchom skrypt za pomocą -m
Rozważmy katalog (który jest pakietem Python ):
.
├── package
│ ├── __init__.py
│ ├── module.py
│ └── standalone.py
Wszystkie pliki w pakiecie zaczynają się od tych samych 2 linii kodu:
from pathlib import Path
print('Running' if __name__ == '__main__' else 'Importing', Path(__file__).resolve())
Uwzględniam te dwie linie tylko po to, aby porządek operacji był oczywisty. Możemy je całkowicie zignorować, ponieważ nie mają one wpływu na wykonanie.
__init__.py i module.py zawierają tylko te dwie linie (tzn. są faktycznie puste).
standalone.py dodatkowo próbuje zaimportować module.py poprzez import względny:
from . import module # explicit relative import
Wiemy, że /path/to/python/interpreter package/standalone.py
to się nie powiedzie. Możemy jednak uruchomić moduł z -m
opcją wiersza poleceń , która „wyszuka sys.path
nazwany moduł i wykona jego zawartość jako __main__
moduł” :
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
wykonuje wszystkie operacje importowania dla Ciebie i automatycznie ustawia __package__
, ale możesz to zrobić samodzielnie w
Rozwiązanie nr 2: Ustaw __package__ ręcznie
Potraktuj to jako dowód koncepcji, a nie faktyczne rozwiązanie. Nie nadaje się do użycia w kodzie rzeczywistym.
PEP 366 ma obejście tego problemu, jednak jest niekompletne, ponieważ __package__
samo ustawienie nie wystarczy. Będziesz musiał zaimportować co najmniej N poprzednich pakietów w hierarchii modułów, gdzie N jest liczbą katalogów nadrzędnych (w stosunku do katalogu skryptu), które będą wyszukiwane w poszukiwaniu importowanego modułu.
A zatem,
Dodaj katalog nadrzędny n-tego poprzednika bieżącego modułu dosys.path
Usuń katalog bieżącego pliku z sys.path
Zaimportuj moduł nadrzędny bieżącego modułu, używając jego pełnej nazwy
Ustaw __package__
na pełną nazwę od 2
Wykonaj import względny
Pożyczę pliki z rozwiązania nr 1 i dodam kilka dodatkowych paczek:
package
├── __init__.py
├── module.py
└── subpackage
├── __init__.py
└── subsubpackage
├── __init__.py
└── standalone.py
Tym razem standalone.py zaimportuje module.py z pakietu pakietu przy użyciu następującego importu względnego
from ... import module # N = 3
Będziemy musieli poprzedzić ten wiersz kodem bojlera, aby działał.
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
Pozwala nam to wykonać standalone.py według nazwy pliku:
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
Bardziej ogólne rozwiązanie zawarte w funkcji można znaleźć tutaj . Przykładowe użycie:
if __name__ == '__main__' and __package__ is None:
import_parents(level=3) # N = 3
from ... import module
from ...module.submodule import thing
Rozwiązanie nr 3: Użyj importu bezwzględnego i setuptools
Kroki to -
Zastąp wyraźny import względny równoważnym importem absolutnym
Zainstaluj, package
aby można go było importować
Na przykład struktura katalogów może wyglądać następująco
.
├── project
│ ├── package
│ │ ├── __init__.py
│ │ ├── module.py
│ │ └── standalone.py
│ └── setup.py
gdzie jest plik setup.py
from setuptools import setup, find_packages
setup(
name = 'your_package_name',
packages = find_packages(),
)
Pozostałe pliki zostały zapożyczone z rozwiązania nr 1 .
Instalacja pozwoli ci zaimportować pakiet niezależnie od katalogu roboczego (zakładając, że nie wystąpią problemy z nazewnictwem).
Możemy zmodyfikować standalone.py, aby wykorzystać tę zaletę (krok 1):
from package import module # absolute import
Zmień katalog roboczy na project
i uruchom /path/to/python/interpreter setup.py install --user
( --user
instaluje pakiet w katalogu pakietów witryny ) (krok 2):
vaultah@base:~$ cd project
vaultah@base:~/project$ python3 setup.py install --user
Sprawdźmy, czy można teraz uruchomić standalone.py jako skrypt:
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'>
Uwaga : jeśli zdecydujesz się zejść tą drogą, lepiej byłoby używać środowisk wirtualnych do instalowania pakietów w izolacji.
Rozwiązanie nr 4: Użyj importu bezwzględnego i trochę kodu typu „kocioł”
Szczerze mówiąc, instalacja nie jest konieczna - możesz dodać do skryptu trochę kodu, aby importowanie absolutne działało.
Pożyczę pliki z Rozwiązania nr 1 i zmienię standalone.py :
Dodaj katalog nadrzędny pakiet aby sys.path
przed przystąpieniem do importu czegokolwiek z pakietu za pomocą importu bezwzględne:
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
Zastąp import względny importem bezwzględnym:
from package import module # absolute import
standalone.py działa bez problemów:
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'>
Uważam, że powinienem cię ostrzec: staraj się tego nie robić, zwłaszcza jeśli twój projekt ma złożoną strukturę.
Na marginesie, PEP 8 zaleca stosowanie importu bezwzględnego, ale stwierdza, że w niektórych scenariuszach dopuszczalny jest wyraźny import względny:
Zalecane jest importowanie bezwzględne, ponieważ są one zazwyczaj bardziej czytelne i zwykle lepiej się zachowują (lub przynajmniej dają lepsze komunikaty o błędach). [...] Jednak wyraźny import względny jest akceptowalną alternatywą dla importu absolutnego, szczególnie w przypadku złożonych układów pakietów, w których użycie importu absolutnego byłoby niepotrzebnie gadatliwe.