Pokazać, co Python kompiluje podczas importu, czym jest bytecode, jaki jest sens plików .pyc i jak kontrolować proces kompilacji.
- Python kompiluje kod źródłowy
.pydo bytecode — niskopoziomowych instrukcji dla maszyny wirtualnej CPython. - Bytecode trafia do cache: katalog
__pycache__/. - Nazwa pliku
.pyczawiera tag wersji interpretera, np.module.cpython-313.pyc. - Cache przyspiesza kolejne importy, ale nie zmienia logiki programu.
Python nie kompiluje domyślnie do kodu maszynowego jak C/C++. Proces wygląda tak:
Kod źródłowy (.py)
│
▼
Kompilator Pythona (wbudowany)
│
▼
Bytecode (.pyc) ← instrukcje dla maszyny wirtualnej CPython
│
▼
Maszyna wirtualna CPython (interpreter bytecode)
│
▼
Wynik działania programu
To model podobny do Javy (kod źródłowy → bytecode → JVM), ale z ważną różnicą: kompilacja odbywa się automatycznie i transparentnie — programista nie musi wywoływać kompilera.
Bytecode to ciąg instrukcji, które maszyna wirtualna CPython wykonuje krok po kroku. Każda instrukcja ma kod operacji (opcode) i opcjonalny argument.
Możemy obejrzeć bytecode za pomocą modułu dis (disassembler):
import dis
def dodaj(a, b):
return a + b
dis.dis(dodaj)Wynik:
2 0 LOAD_FAST 0 (a)
2 LOAD_FAST 1 (b)
4 BINARY_ADD
6 RETURN_VALUE
Interpretacja:
LOAD_FAST 0 (a)— załaduj argumentana stos,LOAD_FAST 1 (b)— załaduj argumentbna stos,BINARY_ADD— zdejmij dwa elementy ze stosu, dodaj, wynik na stos,RETURN_VALUE— zwróć element ze szczytu stosu.
Dlaczego to ważne? Bytecode wyjaśnia, jak Python wykonuje kod wewnętrznie. Rozumienie tego mechanizmu pomaga debugować problemy z wydajnością i rozumieć semantykę języka.
import dis
def silnia(n):
if n <= 1:
return 1
return n * silnia(n - 1)
dis.dis(silnia)Wynik pokazuje instrukcje warunkowe (POP_JUMP_IF_FALSE), wywołania rekurencyjne (CALL_FUNCTION) i operacje arytmetyczne.
Gdy Python importuje moduł, automatycznie kompiluje go do bytecode i zapisuje w podkatalogu __pycache__:
moj_projekt/
utils.py
__pycache__/
utils.cpython-312.pyc ← bytecode dla Python 3.12
utils.cpython-313.pyc ← bytecode dla Python 3.13
Nazwa pliku utils.cpython-312.pyc zawiera tag implementacji i wersji. Dzięki temu:
- różne wersje Pythona mogą współistnieć w tym samym katalogu,
- aktualizacja Pythona automatycznie powoduje nową kompilację,
- nie trzeba ręcznie czyścić cache.
Python porównuje datę modyfikacji (mtime) pliku źródłowego .py z datą zapisaną w nagłówku pliku .pyc:
Czy .pyc istnieje?
│
├─ NIE → kompiluj .py → zapisz .pyc
│
└─ TAK → porównaj mtime źródła z mtime zapisanym w .pyc
│
├─ źródło nowsze → rekompiluj
└─ źródło nie nowsze → użyj .pyc z cache
Od Pythona 3.7 dostępny jest też tryb walidacji oparty na hashu źródła (zamiast mtime), kontrolowany przez opcję --check-hash-based-pycs.
python -B main.pyLub przez zmienną środowiskową:
export PYTHONDONTWRITEBYTECODE=1
python main.pyPrzydatne, gdy:
- nie chcesz zaśmiecać dysku (np. jednorazowe skrypty),
- testujesz na systemie plików tylko do odczytu.
python -m compileall src/Kompiluje rekursywnie wszystkie pliki .py w katalogu src/ do .pyc. Przydatne:
- przy wdrażaniu aplikacji (przyspieszenie pierwszego importu),
- do weryfikacji poprawności składniowej wielu plików naraz.
import py_compile
py_compile.compile("moj_modul.py", doraise=True)
# Tworzy __pycache__/moj_modul.cpython-3XX.pycPlik: examples/compile_demo.py
def compile_file(path: str) -> pathlib.Path:
source = pathlib.Path(path)
py_compile.compile(str(source), cfile=None, doraise=True)
cache_dir = source.parent / "__pycache__"
candidates = sorted(cache_dir.glob(f"{source.stem}*.pyc"))
if not candidates:
raise FileNotFoundError("Nie znaleziono pliku .pyc")
return candidates[-1]Interpretacja:
- funkcja wymusza kompilację,
- potem znajduje wygenerowany plik
.pyc, - można od razu zobaczyć, jak Python nazywa cache.
Plik: examples/cache_info.py — pokazuje, jak listować wszystkie .pyc w katalogu.
- sprawdzić, kiedy Python odświeża
.pyc, - odróżnić kod źródłowy od cache,
- zrozumieć wpływ wersji interpretera na nazwę pliku
.pyc.
- Uruchom
examples/compile_demo.py. - Odszukaj wygenerowany plik w
__pycache__/. - Zmień jedną linię kodu źródłowego i uruchom ponownie.
- Porównaj datę modyfikacji pliku
.pyc. - Uruchom
examples/cache_info.pyi wypisz wszystkie pliki cache. - Użyj
dis.dis()na prostej funkcji, aby zobaczyć bytecode.
- Student rozumie, że usunięcie
__pycache__nie psuje programu, tylko wymusza ponowne wygenerowanie cache.
- Porównaj nazwę
.pycna dwóch różnych wersjach Pythona. - Użyj
python -m compileall .i sprawdź, co się stanie.
exercises/tasks.py— budowanie nazwy.pyci decyzja o rekompilacji,exercises/solutions_compilation.py— rozwiązania,exercises/test_solutions.py— testy.
- przekonanie, że usunięcie
__pycache__psuje kod (nie psuje — Python odtworzy cache), - kopiowanie
.pycmiędzy różnymi wersjami interpretera (nie zadziała — inny tag), - mylenie bytecode z kodem maszynowym (bytecode nie jest natywny, wymaga VM),
- ignorowanie
.pycw.gitignore(powinny być ignorowane w repozytorium).
Wskazówka: dodaj
__pycache__/i*.pycdo pliku.gitignorew każdym projekcie.
- Dlaczego plik
.pyczawiera tag wersji Pythona? - Co się stanie po usunięciu
__pycache__/? - Czy
.pyczmienia wynik działania programu? - Co pokazuje
dis.dis()i do czego się przydaje? - Kiedy warto użyć
python -B? - Jak skompilować cały katalog do bytecode?