Spiegazione
Dal PEP 328
Le importazioni relative utilizzano l'attributo __name__ di un modulo per determinare la posizione di quel modulo nella gerarchia dei pacchetti. Se il nome del modulo non contiene alcuna informazione sul pacchetto (ad es. È impostato su '__main__'), le
importazioni relative vengono risolte come se il modulo fosse un modulo di livello superiore , indipendentemente da dove si trova effettivamente il modulo sul file system.
Ad un certo punto PEP 338 è in conflitto con PEP 328 :
... le importazioni relative si basano su __name__ per determinare la posizione del modulo corrente nella gerarchia dei pacchetti. In un modulo principale, il valore di __name__ è sempre '__main__' , quindi le importazioni relative esplicite falliranno sempre (poiché funzionano solo per un modulo all'interno di un pacchetto)
e per risolvere il problema, PEP 366 ha introdotto la variabile di livello superiore __package__
:
Aggiungendo un nuovo attributo a livello di modulo, questo PEP consente alle importazioni relative di funzionare automaticamente se il modulo viene eseguito utilizzando l'opzione -m
. Una piccola quantità di boilerplate nel modulo stesso consentirà alle relative importazioni di funzionare quando il file viene eseguito per nome. [...] Quando è presente [l'attributo], le importazioni relative saranno basate su questo attributo piuttosto che sull'attributo __name__ del modulo . [...] Quando il modulo principale è specificato dal suo nome file, l' attributo __package__ sarà impostato su Nessuno . [...] Quando il sistema di importazione rileva un'importazione relativa esplicita in un modulo senza __package__ impostato (o con esso impostato su Nessuno), calcolerà e memorizzerà il valore corretto (__name __. rpartition ('.') [0] per i moduli normali e __name__ per i moduli di inizializzazione del pacchetto)
(enfatizzare il mio)
Se __name__
è '__main__'
, __name__.rpartition('.')[0]
restituisce una stringa vuota. Ecco perché c'è una stringa vuota letterale nella descrizione dell'errore:
SystemError: Parent module '' not loaded, cannot perform relative import
La parte rilevante della PyImport_ImportModuleLevelObject
funzione di 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 solleva questa eccezione se non è stato in grado di trovare package
(il nome del pacchetto) in interp->modules
(accessibile come sys.modules
). Poiché sys.modules
è "un dizionario che associa i nomi dei moduli ai moduli che sono già stati caricati" , è ora chiaro che il modulo padre deve essere importato in modo esplicito assoluto prima di eseguire l'importazione relativa .
Nota: la patch dal numero 18018 ha aggiunto un altro if
blocco , che verrà eseguito prima del codice sopra:
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) {
...
*/
Se package
(come sopra) è una stringa vuota, il messaggio di errore sarà
ImportError: attempted relative import with no known parent package
Tuttavia, lo vedrai solo in Python 3.6 o versioni successive.
Soluzione n. 1: esegui lo script usando -m
Considera una directory (che è un pacchetto Python ):
.
├── package
│ ├── __init__.py
│ ├── module.py
│ └── standalone.py
Tutti i file nel pacchetto iniziano con le stesse 2 righe di codice:
from pathlib import Path
print('Running' if __name__ == '__main__' else 'Importing', Path(__file__).resolve())
Includo queste due righe solo per rendere evidente l'ordine delle operazioni. Possiamo ignorarli completamente, poiché non influiscono sull'esecuzione.
__init__.py e module.py contengono solo quelle due righe (cioè sono effettivamente vuote).
standalone.py tenta inoltre di importare module.py tramite l'importazione relativa:
from . import module # explicit relative import
Sappiamo bene che /path/to/python/interpreter package/standalone.py
fallirà. Tuttavia, possiamo eseguire il modulo con l' -m
opzione della riga di comando che "sys.path
__main__
cercherà il modulo nominato ed eseguirà il suo contenuto come modulo" :
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
fa tutte le cose di importazione per te e le imposta automaticamente __package__
, ma puoi farlo tu stesso in
Soluzione n. 2: impostare __package__ manualmente
Per favore trattalo come una prova di concetto piuttosto che una soluzione reale. Non è adatto per l'uso nel codice del mondo reale.
PEP 366 ha una soluzione alternativa a questo problema, tuttavia, è incompleto, poiché l'impostazione __package__
da sola non è sufficiente. Dovrai importare almeno N pacchetti precedenti nella gerarchia dei moduli, dove N è il numero di directory principali (relative alla directory dello script) che verranno ricercate per il modulo da importare.
Così,
Aggiungere la directory principale dell'ennesimo predecessore del modulo corrente asys.path
Rimuovere la directory del file corrente da sys.path
Importa il modulo padre del modulo corrente usando il suo nome completo
Impostare __package__
il nome completo da 2
Eseguire l'importazione relativa
Prenderò in prestito i file dalla soluzione n. 1 e aggiungerò altri subpackages:
package
├── __init__.py
├── module.py
└── subpackage
├── __init__.py
└── subsubpackage
├── __init__.py
└── standalone.py
Questa volta standalone.py importerà module.py dal pacchetto del pacchetto usando la seguente importazione relativa
from ... import module # N = 3
Dovremo precedere quella riga con il codice del boilerplate, per farlo funzionare.
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
Ci permette di eseguire standalone.py per nome file:
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
Una soluzione più generale racchiusa in una funzione è disponibile qui . Esempio di utilizzo:
if __name__ == '__main__' and __package__ is None:
import_parents(level=3) # N = 3
from ... import module
from ...module.submodule import thing
Soluzione n. 3: utilizzare importazioni e strumenti di installazione assoluti
I passaggi sono -
Sostituire le importazioni relative esplicite con importazioni assolute equivalenti
Installa package
per renderlo improprio
Ad esempio, la struttura della directory può essere la seguente
.
├── project
│ ├── package
│ │ ├── __init__.py
│ │ ├── module.py
│ │ └── standalone.py
│ └── setup.py
dove setup.py è
from setuptools import setup, find_packages
setup(
name = 'your_package_name',
packages = find_packages(),
)
Il resto dei file è stato preso in prestito dalla soluzione n . 1 .
L'installazione ti permetterà di importare il pacchetto indipendentemente dalla tua directory di lavoro (supponendo che non ci saranno problemi di denominazione).
Possiamo modificare standalone.py per utilizzare questo vantaggio (passaggio 1):
from package import module # absolute import
Modificare la directory di lavoro in project
ed eseguirla /path/to/python/interpreter setup.py install --user
( --user
installa il pacchetto nella directory dei pacchetti del sito ) (passaggio 2):
vaultah@base:~$ cd project
vaultah@base:~/project$ python3 setup.py install --user
Verifichiamo che ora è possibile eseguire standalone.py come 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'>
Nota : se decidi di seguire questa strada, ti conviene usare ambienti virtuali per installare i pacchetti in modo isolato.
Soluzione n. 4: utilizzare le importazioni assolute e un po 'di codice del boilerplate
Francamente, l'installazione non è necessaria: potresti aggiungere un po 'di codice boilerplate allo script per far funzionare le importazioni assolute.
Prenderò in prestito file dalla soluzione n. 1 e cambierò standalone.py :
Aggiungi la directory principale del pacchetto a sys.path
prima di tentare di importare qualsiasi cosa dal pacchetto usando le importazioni assolute:
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
Sostituisci l'importazione relativa con l'importazione assoluta:
from package import module # absolute import
standalone.py funziona senza problemi:
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'>
Sento che dovrei avvertirti: cerca di non farlo, specialmente se il tuo progetto ha una struttura complessa.
Come nota a margine , PEP 8 raccomanda l'uso di importazioni assolute, ma afferma che in alcuni scenari sono accettabili esplicite importazioni relative:
Si consigliano importazioni assolute, in quanto di solito sono più leggibili e tendono a comportarsi meglio (o almeno a fornire messaggi di errore migliori). [...] Tuttavia, le importazioni relative esplicite sono un'alternativa accettabile alle importazioni assolute, soprattutto quando si tratta di layout di pacchetti complessi in cui l'uso di importazioni assolute sarebbe inutilmente prolisso.