Wyjaśnić, jak interpreter odnajduje moduł i pakiet, jaką rolę mają sys.path, katalog roboczy i zmienna PYTHONPATH. Pokazać narzędzia diagnostyczne i typowe anty-wzorce.
Gdy interpreter napotka instrukcję import modul, wykonuje następujące kroki:
Python utrzymuje słownik sys.modules, który przechowuje wszystkie już załadowane moduły:
import sys
print("json" in sys.modules) # False — jeszcze nie ładowaliśmy
import json
print("json" in sys.modules) # True — teraz jest w cache
print(sys.modules["json"]) # <module 'json' from '...'>Jeśli moduł jest w cache, import jest natychmiastowy — Python nie szuka pliku ponownie. To wyjaśnia, dlaczego wielokrotne import json w różnych plikach projektu nie powoduje wielokrotnego ładowania.
Jeśli modułu nie ma w cache, Python odpytuje findery z listy sys.meta_path:
import sys
print(sys.meta_path)
# [<class '_frozen_importlib.BuiltinImporter'>, ← moduły wbudowane (np. sys, builtins)
# <class '_frozen_importlib.FrozenImporter'>, ← moduły „zamrożone"
# <class '_frozen_importlib_external.PathFinder'>] ← szukanie na ścieżkach (sys.path)Każdy finder implementuje metodę find_spec(name, path). Pierwszy finder, który zwróci specyfikację (ModuleSpec), wygrywa.
PathFinder (trzeci z finderów) przeszukuje katalogi z sys.path:
import sys
for i, entry in enumerate(sys.path):
print(f"[{i}] {entry}")Typowy wynik:
[0] /home/user/projekt ← katalog uruchamianego skryptu (lub pusty string dla REPL)
[1] /usr/lib/python3.12 ← biblioteka standardowa
[2] /usr/lib/python3.12/lib-dynload ← rozszerzenia C
[3] /home/user/projekt/.venv/lib/python3.12/site-packages ← pakiety zainstalowane pip
Kolejność ma znaczenie! Python szuka od indeksu 0 w górę i zatrzymuje się przy pierwszym znalezieniu.
Jeśli żaden finder nie znalazł modułu, Python zgłasza wyjątek:
import nieistniejacy_modul
# ModuleNotFoundError: No module named 'nieistniejacy_modul'import modul
│
├─ modul w sys.modules? ──── TAK → zwróć z cache (natychmiastowo)
│
└─ NIE → odpytaj sys.meta_path
│
├─ BuiltinImporter → moduł wbudowany? → TAK → załaduj
├─ FrozenImporter → moduł zamrożony? → TAK → załaduj
└─ PathFinder → szukaj w sys.path
│
├─ znaleziono .py/.pyc → załaduj, dodaj do sys.modules
└─ nie znaleziono → ModuleNotFoundError
Diagram: diagrams/import_resolution.png
| Źródło | Opis |
|---|---|
| Katalog skryptu | Jeśli uruchamiasz python /home/user/app/main.py, to /home/user/app jest pierwszym wpisem. |
Pusty string "" |
Oznacza bieżący katalog roboczy (przy uruchomieniu z REPL lub python -c). |
PYTHONPATH |
Zmienna środowiskowa — katalogi oddzielone dwukropkiem (Linux) lub średnikiem (Windows). |
| Standardowe ścieżki | Dodane automatycznie: stdlib, lib-dynload, site-packages. |
Plik .pth |
Pliki w site-packages mogą dodawać ścieżki do sys.path. |
PYTHONPATH rozszerza listę miejsc, gdzie Python szuka modułów:
# Linux / macOS:
export PYTHONPATH="/home/user/moje_moduly:/home/user/inny_projekt"
python main.py
# Windows (PowerShell):
$env:PYTHONPATH = "C:\moje_moduly;C:\inny_projekt"
python main.pyWpisy z PYTHONPATH są dodawane do sys.path po katalogu skryptu, ale przed ścieżkami standardowymi.
Ponieważ Python cache'uje moduły w sys.modules, zmiana pliku .py nie jest widoczna bez restartu interpretera. W trybie interaktywnym (REPL) można wymusić przeładowanie:
import importlib
import moj_modul
# ... edytujemy moj_modul.py ...
importlib.reload(moj_modul) # wymusza ponowne załadowanie z dyskuOgraniczenia reload():
- nie przeładowuje zależności modułu,
- obiekty utworzone przed przeładowaniem nadal korzystają ze starej wersji klas,
- nie jest przeznaczony do użycia w kodzie produkcyjnym.
Częsty błąd w kodzie studenckim:
import sys
sys.path.insert(0, "/home/user/moje_moduly") # ANTY-WZORZEC!
import moj_modulDlaczego to źle:
- ścieżka jest zakodowana na sztywno — nie zadziała na innym komputerze,
- zmienia kolejność wyszukiwania — może przesłonić moduły standardowe,
- nie jest powtarzalne — wymaga modyfikacji kodu zamiast konfiguracji środowiska.
Lepsze alternatywy:
- użyj
PYTHONPATH, - zainstaluj pakiet w trybie editable:
pip install -e ., - skonfiguruj
pyproject.tomli zainstaluj projekt.
Plik: examples/path_inspector.py
def read_environment() -> dict[str, str]:
return {
"PYTHONPATH": os.environ.get("PYTHONPATH", "<not-set>"),
"first_sys_path": sys.path[0] if sys.path else "<empty>",
}Interpretacja:
PYTHONPATHrozszerza miejsca, gdzie Python szuka modułów,sys.path[0]zwykle wskazuje katalog uruchamianego skryptu.
Plik: examples/find_module.py
def find_module_origin(module_name: str) -> str | None:
spec = importlib.util.find_spec(module_name)
if spec is None:
return None
return spec.originTo wygodny sposób sprawdzenia, skąd faktycznie ładuje się moduł (plik, built-in, brak).
print(find_module_origin("json")) # /usr/lib/python3.12/json/__init__.py
print(find_module_origin("sys")) # built-in (zwróci None lub 'built-in')
print(find_module_origin("xyz_brak")) # None — moduł nie istnieje- opanować procedurę diagnostyczną,
- rozróżniać problem ścieżki od problemu kodu,
- utrwalić rolę
sys.path.
- Uruchom
examples/path_inspector.pyi zapisz wynik. - Uruchom ten sam plik z innego katalogu roboczego.
- Porównaj
first_sys_path. - Użyj
examples/find_module.pydla modułu istniejącego i nieistniejącego. - Opisz, kiedy pojawi się
Nonei co to oznacza.
- Student potrafi metodycznie wyjaśnić źródło błędu importu.
- Tymczasowo ustaw
PYTHONPATHna własny katalog i sprawdź, jak zmienia się wynik wyszukiwania.
Gdy pojawi się błąd ModuleNotFoundError: No module named 'xyz':
- Sprawdź katalog roboczy:
print(os.getcwd())— czy jesteś tam, gdzie myślisz? - Wypisz
sys.path: — czy katalog z modułem jest na liście? - Sprawdź
PYTHONPATH:print(os.environ.get("PYTHONPATH"))— czy jest ustawiony poprawnie? - Użyj
find_spec:importlib.util.find_spec("xyz")— czy Python w ogóle widzi moduł? - Sprawdź aktywne środowisko:
which python(Linux) /Get-Command python(PowerShell) — czy używasz właściwego interpretera? - Dopiero potem modyfikuj strukturę pakietu lub importy.
exercises/tasks.py— klasyfikacja pochodzenia modułu i widoczności ścieżek,exercises/solutions_import_mechanism.py— rozwiązania,exercises/test_solutions.py— testy.
- ręczne dopisywanie niepoprawnych ścieżek do
sys.path(zakodowane na sztywno), - zależność od konkretnego IDE bez rozumienia katalogu startowego,
- mylenie
PYTHONPATHz instalacją pakietu przezpip, - zapominanie o aktywacji
venv—pip installtrafia do globalnego interpretera, - tworzenie pliku o nazwie kolidującej z modułem stdlib (np.
json.py,random.py).
- Co daje cache
sys.modules? - Kiedy
find_speczwróciNone? - Czym różni się widoczność modułu od poprawności samego kodu modułu?
- Jakie są elementy
sys.pathi skąd się biorą? - Dlaczego ręczne
sys.path.insert()to anty-wzorzec? - Kiedy przydaje się
importlib.reload()?
