Перейти к содержанию

412 уроков, 25 библиотек и челлендж «Что выведет код?» — бесплатно, код прямо в браузере

Начать обучение
Урок 6 из 20 Средний 15 мин 100 XP

__pycache__: кэш компиляции Python

Каталог __pycache__ появляется сам и живёт по простым правилам: pyc свеж, пока у исходника прежние время и размер. Разбираем инвалидацию, очистку и отключение кэша.

Редакция Питоники

Каталог __pycache__ в проекте появляется без приглашения: первый же импорт модуля — и рядом с исходником лежит свежий pyc с magic-числом версии из пятого урока. Новички спрашивают, можно ли его удалить; опытные — когда он может наврать. Оба ответа следуют из одного устройства: это кэш, который интерпретатор сам создаёт, сам проверяет и сам пересобирает. Урок учит управлять этим механизмом, а не терпеть его.

Когда появляется каталог

Кэш пишется при импорте модуля — не при запуске скрипта. python app.py компилирует app.py и держит байткод в памяти: главный скрипт кэша не получает. А вот каждый импортированный модуль проекта честно компилируется один раз, и результат сохраняется:

импорт модуля кладёт pyc в кэш (на реальной машине)
with open('helper.py', 'w', encoding='utf-8') as f:
    f.write('def twice(n):\n    return n * 2\n')

import helper
import os

print('модуль работает:', helper.twice(21))
print('каталог кэша появился:', os.path.isdir('__pycache__'))

os.remove('helper.py')
Вывод
модуль работает: 42
каталог кэша появился: True
Запуск в песочнице даст False во второй строке: в браузерном интерпретаторе запись кэша отключена (sys.dont_write_bytecode включён), чтобы не засорять общую файловую систему. На вашей машине с локальным Python после первого запуска напечатается True — и в __pycache__ появится файл с меткой вашей версии.

Блок намеренно не запускается в песочнице: там импорт сработает и функция вернёт 42, но каталог не появится — запись кэша в браузерном интерпретаторе выключена. На локальной машине этот же код после первого запуска создаст __pycache__ с файлом helper.cpython-312.pyc (метка вашей версии). Именно поэтому главный файл проекта в кэше не ищут: его байткод живёт один запуск, а модулей — столько, сколько запусков.

В песочнице урока запись кэша при импорте отключена — но собрать pyc явно можно любым инструментом из третьего урока. Такой кэш в этой статье мы и разбираем на поля:

каталог кэша после ручной компиляции
import glob
import os
import py_compile

with open('cached_demo.py', 'w', encoding='utf-8') as f:
    f.write('VALUE = 10\n')

py_compile.compile('cached_demo.py')
print('каталог кэша существует:', os.path.isdir('__pycache__'))
print('pyc в кэше:', len(glob.glob('__pycache__/*.pyc')))

for p in glob.glob('__pycache__/*.pyc'):
    os.remove(p)
if os.path.isdir('__pycache__'):
    os.rmdir('__pycache__')
os.remove('cached_demo.py')
Вывод
каталог кэша существует: True
pyc в кэше: 1

Инвалидация: время и размер

Кэш полезен, только пока он соответствует исходнику. Проверка свежести опирается на два поля заголовка pyc из второго урока: время изменения и размер исходника. При импорте интерпретатор сравнивает поля с текущим файлом — не совпало, значит исходник менялся, и компиляция выполняется заново. Посмотрим на поля своими глазами — сначала время:

время исходника сохраняется в заголовке
import glob
import os
import py_compile

with open('stamp_demo.py', 'w', encoding='utf-8') as f:
    f.write('VALUE = 10\n')

pyc = py_compile.compile('stamp_demo.py')
with open(pyc, 'rb') as f:
    data = f.read()
stored = int.from_bytes(data[8:12], 'little')
print('время исходника сохранено в pyc:', stored == int(os.path.getmtime('stamp_demo.py')))

for p in glob.glob('__pycache__/*.pyc'):
    os.remove(p)
if os.path.isdir('__pycache__'):
    os.rmdir('__pycache__')
os.remove('stamp_demo.py')
Вывод
время исходника сохранено в pyc: True

Поле с 8-го по 12-й байт заголовка — unix-время исходника на момент компиляции, и оно совпало с os.path.getmtime файла. Второе поле — размер — даёт проверке второе измерение: время может соврать на файловых системах, где копирование сбрасывает метки, а размер врёт реже:

размер из заголовка против текущего исходника
import glob
import os
import py_compile

with open('stale_demo.py', 'w', encoding='utf-8') as f:
    f.write('VALUE = 10\n')

pyc = py_compile.compile('stale_demo.py')
with open(pyc, 'rb') as f:
    old_size = int.from_bytes(f.read()[12:16], 'little')

with open('stale_demo.py', 'w', encoding='utf-8') as f:
    f.write('VALUE = 10\nVALUE += 5\n')

new_size = os.path.getsize('stale_demo.py')
print('размер исходника изменился:', new_size != old_size)
print('старый pyc устарел:', old_size != new_size)

for p in glob.glob('__pycache__/*.pyc'):
    os.remove(p)
if os.path.isdir('__pycache__'):
    os.rmdir('__pycache__')
os.remove('stale_demo.py')
Вывод
размер исходника изменился: True
старый pyc устарел: True

Мы дописали строку в исходник после компиляции — размер вырос, и кэш, собранный по старому содержимому, теперь числится устаревшим. Дальше события развиваются без нашего участия: при следующем импорте интерпретатор заметит несовпадение и молча перекомпилирует модуль. Никаких команд для обновления кэша вводить не надо.

обновление времени исходника меняет вердикт
import glob
import os
import py_compile

with open('fresh_demo.py', 'w', encoding='utf-8') as f:
    f.write('VALUE = 1\n')

pyc = py_compile.compile('fresh_demo.py')
with open(pyc, 'rb') as f:
    old_stamp = int.from_bytes(f.read()[8:12], 'little')

os.utime('fresh_demo.py', (1000000000, 1000000000))
new_stamp = int(os.path.getmtime('fresh_demo.py'))
print('время исходника изменилось:', new_stamp != old_stamp)
print('Python заметит такое обновление:', new_stamp != old_stamp)

for p in glob.glob('__pycache__/*.pyc'):
    os.remove(p)
if os.path.isdir('__pycache__'):
    os.rmdir('__pycache__')
os.remove('fresh_demo.py')
Вывод
время исходника изменилось: True
Python заметит такое обновление: True

Функция os.utime передвинула время изменения исходника на 2001 год — содержимое то же, а вердикт изменился: pyc собран по свежему времени, исходник теперь «старше», проверка не сойдётся, и модуль будет перекомпилирован. Этим приёмом пользуются сборочные скрипты: коснуться времени файлов, чтобы кэш гарантированно пересобрался.

Управление кэшем

Кэш полностью управляем: создаётся любым из инструментов третьего урока, проверяется по полям заголовка и удаляется как обычный каталог. Соберём pyc и уберём за собой:

сборка и очистка кэша
import glob
import os
import py_compile

with open('cleanup_demo.py', 'w', encoding='utf-8') as f:
    f.write('VALUE = 1\n')

py_compile.compile('cleanup_demo.py')
before = len(glob.glob('__pycache__/*.pyc'))
for p in glob.glob('__pycache__/*.pyc'):
    os.remove(p)
if os.path.isdir('__pycache__'):
    os.rmdir('__pycache__')
after = len(glob.glob('__pycache__/*.pyc'))
print('pyc до очистки:', before)
print('pyc после очистки:', after)
os.remove('cleanup_demo.py')
Вывод
pyc до очистки: 1
pyc после очистки: 0

Удаление кэша — не поломка: второй урок показывал, что pyc восстанавливается при первом же импорте. Кэш компиляции самодостаточен: удалили каталог — интерпретатор соберёт его заново при первом же импорте. Из-за этого каталог и исключают из git: репозиторий хранит исходники, кэш у каждой машины свой.

ДействиеКакРезультат
Отключить записьpython -B или переменная PYTHONDONTWRITEBYTECODE=1Импорты работают, pyc не создаются
Очистить кэшУдалить каталоги __pycache__ в проектеПересоберётся при первом импорте
Пересобрать принудительноpython -m compileall с forceВсе pyc пересоздаются
Проверить свежесть вручнуюСверить поля 8:16 заголовка с исходникомТот же критерий, что у интерпретатора
очистка кэша проекта из консоли
find . -type d -name __pycache__ -exec rm -rf {} +
Командная строка недоступна в песочнице: команда выполняется на своей машине (macOS/Linux; на Windows в PowerShell: Get-ChildItem -Recurse -Directory -Filter __pycache__ | Remove-Item -Recurse). Находят все __pycache__ в дереве проекта и удаляют целиком.
принудительная пересборка кэша из консоли
python -m compileall -f мой_проект/
Командная строка недоступна в песочнице. Флаг -f — консольная форма параметра force: пересобирает все pyc, даже числящиеся свежими. Пригодится после копирования проекта со сбитыми временами файлов.
  • кэш пишется при импорте модулей; главный скрипт запуска не кэшируется;
  • свежесть проверяется по двум полям заголовка pyc: времени и размеру исходника;
  • изменился исходник — pyc пересобирается сам при следующем импорте, команд не нужно;
  • очистка безопасна: __pycache__ восстанавливается автоматически;
  • запись отключается флагом -B или переменной PYTHONDONTWRITEBYTECODE, а не правкой кода.

Кэш — забота интерпретатора, и после этого урока — ваша тоже: вы знаете, когда он появляется, как проверяется и как им управлять. Дальше по разделу — marshal в седьмом уроке: формат, которым байткод упаковывается внутрь pyc.

Что выведет код?

Сначала предскажи ответ в голове — это главный навык программиста.

import glob
import os

before = len(glob.glob('__pycache__/*.pyc'))
if os.path.isdir('__pycache__'):
    for p in glob.glob('__pycache__/*.pyc'):
        os.remove(p)
    os.rmdir('__pycache__')
after = len(glob.glob('__pycache__/*.pyc'))
print(before >= 0, after)
import os

with open('quiz_mod.py', 'w', encoding='utf-8') as f:
    f.write('V = 1\n')
size_before = os.path.getsize('quiz_mod.py')
with open('quiz_mod.py', 'a', encoding='utf-8') as f:
    f.write('V += 2\n')
size_after = os.path.getsize('quiz_mod.py')
print(size_after != size_before)
os.remove('quiz_mod.py')
import os
import time

with open('quiz_time.py', 'w', encoding='utf-8') as f:
    f.write('V = 1\n')
stamp = int(os.path.getmtime('quiz_time.py'))
print(isinstance(stamp, int))
os.remove('quiz_time.py')
Проверь себя
0 / 7

1. Когда Python пишет pyc в __pycache__?

2. По каким полям интерпретатор решает, устарел ли pyc?

3. Исходник изменился. Что нужно сделать, чтобы обновить кэш?

4. Что произойдёт после удаления каталога __pycache__?

5. Как отключить запись pyc-файлов?

6. Почему __pycache__ не коммитят в git?

7. Замедляет ли python -B программу?

Карточки терминов
Запомнено: 0 / 7
Практика

Соберите pyc файла book.py с содержимым PAGES = 120, прочитайте поле размера из заголовка и напечатайте, что оно совпадает с текущим размером файла. За собой уберите и pyc, и исходник.

practice.py
Вопросы и ответы по уроку

Что такое __pycache__ и зачем он нужен?

Это каталог кэша компиляции: при импорте модуля Python сохраняет его байткод (pyc), чтобы при следующих запусках пропустить компиляцию. Свежесть проверяется по времени и размеру исходника; изменился исходник — pyc пересобирается сам.

Урок 2: устройство pyc-файла

Можно ли удалять __pycache__?

Да, безопасно: это только кэш. При первом же импорте Python пересоберёт его из исходников. Единственный эффект удаления — чуть более медленный первый запуск: компиляция модулей выполнится заново.

Почему __pycache__ появился, хотя я ничего не компилировал?

Компиляция в Python происходит всегда при импорте — это часть работы интерпретатора, а не отдельная команда. Каталог создаёт сам интерпретатор при первом импорте модулей проекта.

Урок 1: байткод и VM

Как отключить создание __pycache__?

Запускайте интерпретатор с флагом -B или задайте переменную окружения PYTHONDONTWRITEBYTECODE=1. Импорты продолжат работать, компиляция останется — просто pyc не записывается на диск.

Может ли __pycache__ подсовывать устаревший код?

Только в экзотике: копирование проекта без времён файлов или совпавший размер при изменённом содержимом. Лечение простое — удалить каталог кэша; после пересборки вопрос закрыт. Классические IDE-сценарии «проверка сломалась» лечатся так же.

Понравился урок? Сошлитесь на него

«Кэш компиляции самодостаточен: удалили каталог — интерпретатор соберёт его заново при первом же импорте.»

Скопируйте готовую ссылку в формате HTML, Markdown или чистый адрес и вставьте в статью на Habr, VC, Telegram-канал или свой блог — так о проекте узнают новые читатели.

TelegramVK

Что дальше

Осталось в разделе: 20 из 20. Прогресс хранится в вашем браузере.

Похожие уроки по темам

Подобраны автоматически по пересечению тем и ключевых слов.