Względny import w Pythonie 3

Chcę zaimportować funkcję z innego pliku w tym samym katalogu.

Czasami mi to działa z from .mymodule import myfunction ale czasami dostaję:

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

Czasami działa z from mymodule import myfunction, ale czasami dostaję też:

SystemError: Parent module '' not loaded, cannot perform relative import
Nie rozumiem tu logiki i nie mogłem znaleźć żadnego wyjaśnienia. To wygląda zupełnie przypadkowo. Czy ktoś mógłby mi wyjaśnić, jaka jest logika tego wszystkiego?
Author: martineau, 2013-06-07

8 answers

Niestety ten moduł musi znajdować się wewnątrz pakietu, a także czasami musi być uruchamiany jako skrypt. Każdy pomysł, jak mógłbym osiągnąć?

To dość powszechne mieć taki układ...

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

...z mymodule.py w ten sposób...

#!/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()

...a myothermodule.py Jak to...

#!/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()

...i main.py w ten sposób...

#!/usr/bin/env python3

from mypackage.myothermodule import add

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

if __name__ == '__main__':
    main()

...który działa dobrze, gdy uruchamiasz main.py lub mypackage/mymodule.py, ale nie działa z mypackage/myothermodule.py, ze względu na względną import...

from .mymodule import as_int

Sposób, w jaki powinieneś to prowadzić jest...

python3 -m mypackage.myothermodule

...ale jest nieco gadatliwy i nie miesza się dobrze z linią shebang jak #!/usr/bin/env python3.

Najprostszą poprawką w tym przypadku, zakładając, że nazwa {[16] } jest globalnie unikalna, byłoby unikanie importu względnego i po prostu używanie...

from mymodule import as_int

...chociaż, jeśli nie jest unikalny lub struktura pakietów jest bardziej złożona, musisz dołączyć katalog zawierający Twój katalog PYTHONPATH, i zrób to tak...

from mypackage.mymodule import as_int

...lub jeśli chcesz, aby działało "po wyjęciu z pudełka", możesz najpierw przerobić PYTHONPATH w kodzie za pomocą tego...

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
To trochę bolesne, ale jest wskazówka, dlaczego w e-mailu napisanym przez pewnego Guido van Rossuma...

Jestem -1 na tym i na innych proponowanych twiddlingach __main__ maszyny. Jedynym przypadkiem użycia wydaje się uruchamianie skryptów, które zdarzają się żyć w katalogu modułu, który mam zawsze postrzegane jako antypattern. Żeby zmienić zdanie musisz mnie przekonać, że nie jest.

Czy uruchamianie skryptów wewnątrz pakietu jest antypaternem, czy nie jest subiektywne, ale osobiście uważam, że jest to naprawdę przydatne w pakiecie, który mam, który zawiera niestandardowe widżety wxPython, więc mogę uruchomić skrypt dla dowolnego pliku źródłowego, aby wyświetlić wx.Frame zawierający tylko ten widżet do celów testowych.

 352
Author: Aya,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2013-06-07 13:46:06

Wyjaśnienie

From PEP 328

Import względny używa atrybutu __Nazwa__ modułu, aby określić, że pozycji modułu w hierarchii pakietów. Jeśli nazwa modułu nie zawiera żadnych informacji o pakiecie (np. jest ustawiony na ' _ _ main__') wtedy względne importowanie jest rozwiązywane tak, jakby moduł był najwyższym poziomem moduł , niezależnie od tego, gdzie moduł znajduje się w pliku system.

W pewnym momencie PEP 338 328:

... względny import opiera się na _ _ nazwa _ _ , aby określić aktualny pozycji modułu w hierarchii pakietów. W module głównym wartość __name _ _ jest zawsze ' _ _ main _ _ ' , więc jawny względny import zawsze nie będzie działać (ponieważ działają tylko dla modułu wewnątrz pakietu)

I aby rozwiązać problem, PEP 366 wprowadził zmienną najwyższego poziomu __package__:

Dodając nowy atrybut poziomu modułu, ten PEP pozwala na względne importuje do pracy automatycznie, jeśli moduł jest wykonywany przy użyciu - M Zamiana. Niewielka ilość kotła w samym module pozwoli względny import działa, gdy plik jest wykonywany według nazwy. [...] Gdy [atrybut] jest obecny, względny import będzie oparty na tym atrybutie zamiast atrybutu module _ _ name _ _ . [...] Gdy głównym moduł jest określony przez jego nazwę pliku, wtedy atrybut __pakiet__ zostanie ustawiony na None. [...] gdy system importu napotka jawny względny import w moduł bez _ _ package _ _ set (lub z nim ustawionym na None), będzie Oblicz i zapisz prawidłową wartość (__ / align = "left" / rpartition (".')[0] dla zwykłych modułów i _ _ nazwa _ _ dla modułów inicjalizacji pakietów)

(moje)

Jeśli __name__ jest '__main__', __name__.rpartition('.')[0] zwraca pusty łańcuch. Dlatego w opisie błędu znajduje się pusty ciąg znaków:

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

Odpowiednia część CPython ' S PyImport_ImportModuleLevelObject funkcja:

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

CPython podnosi ten wyjątek, jeśli nie mógł znaleźć package (nazwy pakietu) w interp->modules (dostępnej jako sys.modules). Ponieważ sys.modules jest "słownikiem, który mapuje nazwy modułów do modułów, które zostały już załadowane" , jest teraz jasne, że moduł nadrzędny musi być jawnie importowany bezwzględnie przed wykonaniem importu względnego .

Uwaga: patch z numer 18018 dodał kolejny if Blok , który zostanie wykonany przed powyższy kod:

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 samo jak powyżej) jest pustym łańcuchem, komunikat o błędzie będzie wynosił

ImportError: attempted relative import with no known parent package

Jednak zobaczysz to tylko w Pythonie 3.6 lub nowszym.

Rozwiązanie # 1: Uruchom skrypt za pomocą -m

Rozważmy katalog (który jest pakietem Pythona ):

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

Wszystkie pliki w pakiecie zaczynają się tymi samymi 2 linijkami kodu:

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

Włączam te dwie linie tylko aby kolejność operacji była oczywista. Możemy je całkowicie zignorować, ponieważ nie wpływają na egzekucję.

__init__.py i module.py zawierają tylko te dwie linie (tzn. są one skutecznie pusta).

standalone.py dodatkowo próbuje zaimportować module.py Poprzez względny import:

from . import module  # explicit relative import
Jesteśmy świadomi, że zawiedzie. Możemy jednak uruchomić moduł z -m command line option that will " search sys.path dla nazwanego modułu i wykonaj jego zawartość jako moduł __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 robi wszystko za Ciebie i automatycznie ustawia __package__, ale możesz to zrobić siebie w

Rozwiązanie # 2: Ustaw__ pakiet _ _ ręcznie

proszę traktować to jako dowód koncepcji, a nie rzeczywiste rozwiązanie. Nie nadaje się do użycia w kodzie rzeczywistym.

PEP 366 ma obejście tego problemu, jednak jest niekompletne, ponieważ samo ustawienie __package__ nie wystarczy. Będziesz musiał zaimportować co najmniej N poprzedzające pakiety w hierarchii modułów, gdzie N jest liczbą katalogi nadrzędne (względem katalogu skryptu), które będą wyszukiwane dla importowanego modułu.

Tak więc,

  1. Dodaj Katalog nadrzędny NTH poprzednika bieżącego modułu do sys.path

  2. Usuń katalog bieżącego pliku z sys.path

  3. Import modułu nadrzędnego bieżącego modułu przy użyciu jego w pełni kwalifikowanej nazwy

  4. Ustaw __package__ na pełną nazwę od 2

  5. Wykonaj względny import

Pożyczę pliki z rozwiązania # 1 i dodam jeszcze kilka podpakietów:

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

Tym razem standalone.py zaimportuje module.py Z pakietu za pomocą następującego względnego importu

from ... import module  # N = 3

Musimy poprzedzić tę linię kodem kotła, aby to zadziałało.

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 na wykonanie standalone.py wg 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 zawinięte w funkcję można znaleźć tutaj . Przykład użycia:

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

from ... import module
from ...module.submodule import thing
Rozwiązanie # 3: Użyj importu bezwzględnego i setuptools

Kroki są-

  1. Zastąp jawny import względny równoważnym importem bezwzględnym

  2. Zainstaluj package, aby można było go zaimportować

Na przykład struktura katalogów może być follows

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

Gdzie setup.py jest

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

Reszta plików została zapożyczona z rozwiązania # 1 .

Instalacja pozwoli Ci zaimportować pakiet niezależnie od katalogu roboczego (zakładając, że nie będzie problemów z nazwami).

Możemy zmodyfikować standalone.py aby skorzystać z tej przewagi (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 Twoim site-packages directory ) (Krok 2):

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

Sprawdźmy, czy teraz Można uruchomić standalone.py jako scenariusz:

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ę pójść tą drogą, lepiej będzie używać wirtualnych środowisk do instalowania pakietów w izolacji.

Rozwiązanie # 4: użyj importu bezwzględnego i kodu kotła

Szczerze mówiąc instalacja nie jest konieczna - możesz dodać trochę kodu do Twój skrypt, aby import absolutny działał.

Pożyczę pliki z rozwiązanie #1 i zmienię standalone.py :

  1. Dodaj Katalog nadrzędny pakietu do sys.path przed próbą zaimportowania czegokolwiek z pakietu przy użyciu importu bezwzględnego:

    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. Zastąp względny import przez bezwzględny import:

    from package import module  # absolute import
    

standalone.py działa bez problemy:

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'>
Czuję, że powinienem cię ostrzec: staraj się tego nie robić, szczególnie 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 możliwy jest bezpośredni import względny:

Import bezwzględny jest zalecany, ponieważ zwykle jest bardziej czytelny i wydają się być lepiej zachowywane (lub przynajmniej dają lepszy błąd wiadomości). [...] Jednak, jawny względny Import jest akceptowalny alternatywa dla importu bezwzględnego, szczególnie w przypadku złożonych układy pakietów, w których użycie importu bezwzględnego byłoby niepotrzebnie gadatliwy.

 156
Author: vaultah,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-12-31 11:40:35

Natknąłem się na ten problem. Obejście włamania jest importowane za pomocą bloku if / else w następujący sposób:

#!/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
Author: goffer,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-01-13 19:22:45

Włóż to do paczki. init__.py plik :

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

Zakładając, że Twój Pakiet jest taki:

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

Teraz używaj zwykłego importu w pakiecie, jak:

# in module2.py
from module1 import class1

To działa zarówno w Pythonie 2 jak i 3.

 11
Author: am5,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-05-16 12:38:26

Jeśli oba pakiety znajdują się w ścieżce importu (sys.ścieżka), a żądany moduł/Klasa znajduje się w example/example.py, następnie, aby uzyskać dostęp do klasy bez względnego importu, spróbuj:

from example.example import fkt
 1
Author: Salt999,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-03-09 22:18:30

Aby rozwiązać ten problem, opracowałem rozwiązanie z repackage pakiet, który działa dla mnie od jakiegoś czasu. Dodaje górny katalog do ścieżki lib:

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

Przepakowanie może dokonać względnego importu, który działa w szerokim zakresie przypadków, przy użyciu inteligentnej strategii (sprawdzanie stosu wywołań).

 1
Author: fralau,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-12-06 09:42:19

Mam nadzieję, że będzie to miało wartość dla kogoś tam - przejrzałem pół tuzina postów stackoverflow, próbując dowiedzieć się o relatywnych importach podobnych do tych zamieszczonych powyżej. Ustawiłem wszystko zgodnie z sugestią, ale wciąż uderzałem ModuleNotFoundError: No module named 'my_module_name'

Ponieważ tylko rozwijałem lokalnie i bawiłem się, nie stworzyłem/nie uruchomiłem pliku setup.py. Ja też najwyraźniej nie ustawiłem swojego PYTHONPATH.

Zdałem sobie sprawę, że kiedy uruchomiłem mój kod, tak jak byłem, gdy testy były w tym samym katalogu jako moduł nie mogłem znaleźć mojego modułu:

$ 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'

Jednak, kiedy wyraźnie określiłem ścieżkę rzeczy zaczęły działać:

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

OK

Tak więc, jeśli ktoś wypróbował kilka sugestii, uważa, że ich kod jest poprawnie skonstruowany i nadal znajduje się w podobnej sytuacji, jak ja, spróbuj jednej z poniższych opcji, jeśli nie wyeksportujesz bieżącego katalogu do swojej ścieżki PYTHONPATH:

  1. Uruchom kod i jawnie Dołącz ścieżkę w ten sposób: $ PYTHONPATH=. python3 test/my_module/module_test.py
  2. aby uniknąć wywołania PYTHONPATH=., Utwórz plik setup.py o następującej treści i uruchom python setup.py development, aby dodać pakiety do ścieżki:
# setup.py
from setuptools import setup, find_packages

setup(
    name='sample',
    packages=find_packages()
)
 1
Author: LaCroixed,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-04-17 19:40:32

Dla mnie to musiałem uruchomić python3 z głównego katalogu, aby to działało. Na przykład, jeśli projekt ma następującą strukturę:


Project_demo

/ | main.py

| - - some_package

| - - - - __ init __. py

| - - - - project_configs.py

| - - test

| - - - - test_project_configs.py


Rozwiązanie: uruchomiłbym python3 wewnątrz project_demo i następnie z some_package import project_configs .

 0
Author: Árthur,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-09-11 18:49:29