Устройство Python
Чем отличается модуль от пакета?
Модуль — это файл, содержащий код Python, который может быть повторно использован в других программах.
Пакет — это директория, содержащая один или несколько модулей (или пакетов внутри пакетов), а также специальный файл __init__.py, который выполняется при импорте пакета. Он может содержать код, который инициализирует переменные, функции и классы, и становится доступным для использования внутри модулей, находящихся внутри этого пакета. Это обеспечивает общую функциональность для всех модулей, находящихся внутри пакета.
Например, если у нас есть пакет mypackage, в нем может находится несколько модулей, таких как module1.py, module2.py. В файле __init__.py определяются функции и переменные, которые могут использоваться внутри module1 и module2.
Некоторые примеры импорта:
import mymodule # импортируем модуль
from mypackage import mymodule # импортируем модуль из пакета
from mypackage.mymodule import myfunction # импортируем функцию из модуля в пакете
Пример со структурой файлов
project/
main.py
shapes/ # пакет
__init__.py # обязательный файл для пакета
circle.py # модуль в пакете
rectangle.py # модуль в пакете
# Импорт конкретного модуля из пакета
from shapes import circle
result = circle.area(5)
print(result) # 78.53975
# Импорт функции напрямую из модуля пакета
from shapes.rectangle import area
result = area(4, 6)
print(result) # 24
# Импорт всего пакета (требует настройки __init__.py)
import shapes
# После настройки __init__.py можно использовать:
# result = shapes.circle.circumference(3)
Настройка __init__.py необходима для корректного импорта всего пакета. Без неё команда import shapes загрузит пакет, но не предоставит прямого доступа к его модулям и функциям. Основная настройка включает определение списка __all__для контроля импорта через звёздочку и явный реэкспорт ключевых компонентов на верхний уровень пакета. Также в __init__.py обычно указывают метаданные пакета, такие как версия и автор, и могут размещать код инициализации, который выполняется при первом импорте.
Статья на хабре Python модули и пакеты
Относительные и абсолютные импорты в Python
Относительные и абсолютные импорты в Python отличаются тем, как указывается путь к модулю, который импортируется.
Абсолютные импорты
При абсолютном импорте указывается полный путь к модулю с корня проекта или установленного пакета. Например:
from myproject.mypackage import mymodule
Абсолютные импорты более читаемы и однозначны, их легче поддерживать. Это рекомендуется согласно PEP 8. Абсолютные импорты работают всегда, независимо от того, откуда запускается скрипт.
Относительные импорты
Относительный импорт задает путь от текущего модуля с помощью точек:
-
Одна точка . — текущий каталог
-
Две точки .. — уровень выше
-
Три точки ... — на два уровня выше и так далее
Пример:
from . import mymodule # импорт модуля из текущей папки
from ..subpackage import othermodule # импорт из родительской папки
Относительные импорты удобны внутри пакетов для коротких путей и удобства поддержки, но работают только в пакетах (внутри модуля, который запускается как часть пакета), а не в скриптах, запущенных напрямую.
Главные отличия
-
Абсолютные пути всегда начинаются с корня проекта (пакета).
-
Относительные пути задают перемещение относительно текущего модуля.
В практике предпочтительнее использовать абсолютные импорты для внешнего кода, а относительные — для внутренних модулей пакета. Однако в точке входа (главный исполняемый файл) всегда используйте абсолютные импорты.
Статья на хабре - Приручаем импорты в Python, или как не сойти с ума
Что происходит при запуске кода в Python?
Python — интерпретируемый язык программирования, поэтому коду не нужна компиляция. Он выполняется на ходу, но используется промежуточная форма, которая называется компиляцией байт-кода.
Вот как это происходит под капотом кратко:
- при первом запуске кода на Python компилирует его в байт-код;
- после этого виртуальная машина Python (PVM) выполняет его;
- байт-код хранится в папке pycache, у таких файлов расширение .pyc;
- периодически Python проверяет, есть ли у файла .py скомпилированный байт-код в формате .pyc. Если его нет или он старше основного файла, то процесс компиляции запускается снова.
Интерпретатор Python - это программа, которая выполняет другие программы. Когда вы пишете программу на языке Python, интерпретатор читает ваш код и выполняет содержащиеся в нем инструкции. Интерпретатор представляет собой слой программной логики между вашим программным кодом и аппаратурой вашего компьютера.
ИНТЕРПРЕТАТОР PYTHON (python.exe / python3)
├── КОМПИЛЯТОР (compiler) - компилирует .py в байт-код
└── ВИРТУАЛЬНАЯ МАШИНА (PVM) - выполняет байт-код
В зависимости от используемой версии Python сам интерпретатор может быть реализован как программа на языке C (CPython), как набор классов Java (Jython) и в других вариантах реализации.
Процесс выполнения Python-программы
Компиляция в байт-код
После запуска сценария Python сначала компилирует исходный текст в байт-код. Компиляция - это этап перевода, а байт-код представляет собой низкоуровневое, платформонезависимое представление исходной программы. Python транслирует инструкции исходного кода в группы инструкций байт-кода для повышения производительности, поскольку выполнение байт-кода эффективнее прямой интерпретации исходного кода.
Создание .pyc файлов
После компиляции создаются файлы с расширением .pyc, которые сохраняются в папке __pycache__. Имена этих файлов включают информацию о версии Python (например: module.cpython-310.pyc).
Повторный запуск программы
При последующих запусках программы:
- Интерпретатор проверяет существование соответствующего
.pycфайла - Сравнивает дату модификации исходного файла и скомпилированного байт-кода
- Если исходный код не изменялся, выполняется этап компиляции, и программа запускается из существующего
.pycфайла - Если исходный код был изменен, происходит перекомпиляция в байт-код
Резервный механизм
Если Python не может записать файл с байт-кодом (например, из-за отсутствия прав на запись на диск), программа продолжает работать корректно - байт-код компилируется в оперативную память и удаляется после завершения программы.
Виртуальная машина Python (PVM)
После компиляции байт-код передается Виртуальной машине Python (Python Virtual Machine - PVM), которая выполняет инструкции байт-кода. PVM - это механизм времени выполнения, который всегда присутствует в составе системы Python и является финальным звеном в цепочке выполнения программы.
Важные особенности
- Автоматизация: Все описанные процессы (компиляция, создание .pyc файлов, выполнение в PVM) происходят автоматически и прозрачно для программиста
- Производительность: Байт-код выполняется эффективнее исходного кода, но медленнее машинного кода
- Переносимость: Байт-код является платформонезависимым, что обеспечивает кроссплатформенность Python-программ
- Структура: Интерпретатор Python состоит из компилятора (преобразует код в байт-код) и виртуальной машины (выполняет байт-код)
- JIT-компиляция: В Python 3.13 появился экспериментальный JIT-компилятор, но он требовал специальной сборки интерпретатора с флагом --enable-experimental-jit. В текущей стабильной версии Python 3.14 (выпущенной в октябре 2025) JIT-компилятор стал стандартной опцией, включаемой при компиляции Python с флагом --enable-jit. Эта реализация использует технику "копирования и исправления" (Copy-and-Patch), которая динамически компилирует байткод в машинный код во время выполнения программы. Важно отметить, что JIT в Python 3.14 по умолчанию отключён при стандартной установке и требует явного включения либо при компиляции интерпретатора, либо через переменные окружения во время выполнения. Производительные улучшения наиболее заметны в циклах и часто вызываемых функциях, но не дают радикального ускорения для всех типов кода. Весь этот сложный процесс скрыт от пользователя, что делает работу с Python простой и интуитивно понятной.
Сборщик мусора
В Python реализована автоматическая сборка мусора, что освобождает разработчика от необходимости явно управлять выделением и освобождением памяти.
Система использует два взаимодополняющих механизма:
1. Reference Counting (подсчёт ссылок) — основной механизм
Это фундаментальный механизм управления памятью в Python:
- Каждый объект имеет счётчик ссылок (
ob_refcnt) - При создании новой ссылки счётчик увеличивается
- При удалении ссылки счётчик уменьшается
- Когда счётчик достигает нуля, объект немедленно удаляется из памяти
Преимущества:
- Мгновенное освобождение памяти
- Предсказуемое поведение
- Низкие накладные расходы
Ограничение: не справляется с циклическими ссылками
2. Циклический сборщик мусора с поколенческой оптимизацией
Дополнительный механизм для обработки случаев, которые не может разрешить reference counting.
Поколенческая организация (Generations)
Объекты отслеживаются в трёх группах (поколениях) на основе их "возраста":
- Поколение 0: недавно созданные объекты
- Поколение 1: объекты, пережившие одну сборку мусора
- Поколение 2: долгоживущие объекты, пережившие несколько сборок
Эмпирическое наблюдение, лежащее в основе этой оптимизации: большинство объектов становятся недостижимыми вскоре после создания.
Алгоритм обнаружения циклических ссылок
Сборщик использует алгоритм Mark-and-Sweep (метка и очистка):
-
Mark (пометка): Начиная от "корневых" объектов (глобальные переменные, локальные переменные в стеке вызовов, системные объекты), рекурсивно помечаются все достижимые объекты.
-
Sweep (очистка): Все непомеченные объекты считаются недостижимыми и удаляются из памяти.
-
Compact (уплотнение): Опциональный этап, при котором оставшиеся объекты могут быть перемещены для уменьшения фрагментации памяти.
Планирование сборок мусора
Сборки запускаются автоматически на основе пороговых значений:
- Поколение 0: сборка запускается после определённого количества аллокаций
- Поколение 1: сборка запускается после нескольких сборок поколения 0
- Поколение 2: сборка запускается после нескольких сборок поколения 1
Такой подход оптимизирует производительность, реже проверяя долгоживущие объекты.
Проблема циклических ссылок
Циклические ссылки возникают, когда объекты ссылаются друг на друга, образуя замкнутый цикл. Reference counting не может обработать такие случаи, поскольку счётчик ссылок каждого объекта никогда не достигнет нуля. Циклический сборщик мусора периодически обнаруживает и разрывает такие циклы.
Слабые ссылки (Weak References)
Для ситуаций, когда нужно избежать создания циклических ссылок, Python предоставляет механизм weak references. Слабая ссылка не увеличивает счётчик ссылок объекта, позволяя ему быть удалённым, если на него нет сильных (обычных) ссылок.
Ручное управление
Хотя сборка мусора в Python автоматическая, разработчики могут при необходимости:
- Вызывать принудительную сборку мусора:
import gc
collected = gc.collect() # Возвращает количество собранных объектов
gc.collect(0) # Только поколение 0
gc.collect(1) # Поколения 0 и 1
gc.collect(2) # Все поколения (по умолчанию)
- Включать или отключать циклический сборщик мусора:
gc.disable() # Отключить автоматическую сборку
gc.enable() # Включить обратно
- Настраивать пороговые значения для сборок:
gc.set_threshold(500, 10, 10) # Установить пороги для поколений 0, 1, 2
- Получать статистику по выполненным сборкам:
print(gc.get_count()) # Текущие счётчики для поколений
print(gc.get_threshold()) # Текущие пороги
print(gc.get_stats()) # Подробная статистика сборок
Когда использовать ручной вызов сборки?
- Перед измерением производительности** критического участка кода
- После освобождения больших структур данных
- При работе с циклическими ссылками для гарантии их очистки
- В долгоживущих процессах для контроля использования памяти
Статьи на хабре - CPython — сборка мусора изнутри ч.1
Всё, что нужно знать о сборщике мусора в Python
Что такое GIL?
GIL (Global Interpreter Lock) - это механизм, который обеспечивает потокобезопасность интерпретатора. Это особенно важно в контексте многопоточности, поскольку GIL ограничивает выполнение байткода Python только одним потоком за раз. Другими словами, в любой момент времени только один поток байткода Python может выполняться в одном процессе. Это сделано для предотвращения гонок данных и других проблем, связанных с параллельным доступом к общим ресурсам. GIL гарантирует каждому потоку эксклюзивный доступ к переменным интерпретатора (и соответствующие вызовы C-расширений работают правильно).
Принцип работы заключается в том, что когда поток начинает выполнение, он захватывает GIL, а освобождает его при операциях ввода-вывода (таких как чтение файлов, сетевые запросы) или при истечении выделенного временного интервала. Это позволяет другим готовым потокам получить GIL и продолжить работу. Однако такая модель создаёт проблемы с производительностью на многоядерных системах, где несколько потоков могли бы выполняться параллельно. Для задач, требующих истинного параллелизма, в Python рекомендуется использовать многопроцессорность (модуль multiprocessing), которая обходит ограничения GIL за счёт запуска отдельных процессов с собственными интерпретаторами.
В Python же при использовании потоков для вычислительных задач производительность практически не улучшается, а в некоторых случаях может даже ухудшиться из-за накладных расходов на переключение между потоками и управление GIL.
Важно понимать, что GIL ограничивает только выполнение байт-кода Python. Это означает, что сам интерпретатор Python в любой момент времени может исполнять байт-код только в одном потоке, независимо от количества доступных ядер процессора.
В нативных расширениях, написанных на C, C++ или Rust, разработчики могут временно освобождать GIL, когда код не взаимодействует с объектами Python. Это позволяет достичь реального параллелизма для вычислительных операций, реализованных на уровне нативного кода. I/O операции. Наиболее распространенный пример — операции ввода-вывода. Когда поток выполняет сетевые запросы, чтение файлов или другие I/O операции, GIL автоматически освобождается, позволяя другим потокам выполнять Python-код во время ожидания завершения этих операций.
Библиотеки типа NumPy, Pandas и SciPy активно используют эту возможность. Их внутренние вычисления, реализованные на C/Fortran, выполняются с освобожденным GIL. Это означает, что:
- Матричные операции в NumPy
- Агрегации и группировки в Pandas
- Сложные математические вычисления в SciPy могут выполняться параллельно в нескольких потоках, поскольку основная вычислительная нагрузка приходится на оптимизированный нативный код, а не на байт-код Python.
Основные изменения, связанные с GIL в Python 3.14:
- Отказ от обязательного GIL: в Python 3.14 появилась возможность отключать GIL, позволяя потокам выполняться параллельно;
- Субинтерпретаторы: введена возможность создавать субинтерпретаторы, каждый из которых имеет свой собственный GIL. Это позволяет запускать потоки с разными блокировками, обеспечивая настоящий параллелизм;
- API для управления GIL: доступ к этой функциональности также можно получить через переменную окружения PYTHON_GIL или опцию командной строки -X gil;
- Внутренние изменения: наряду с внешними изменениями, произошли и внутренние реформы, такие как новый интерпретатор байт-кода, который улучшает производительность, устранение глобального состояния и потокобезопасный механизм управления памятью, что заложило основу для отключения GIL и принесло общее повышение производительности.
Как GIL влияет на Асинхронность, Многопоточность и Многопроцессорность? * Многопоточность: GIL не позволяет эффективно использовать несколько ядер процессора для CPU-bound задач. Потоки выполняются конкурентно, а не параллельно. * Асинхронность: GIL не мешает I/O-bound задачам, так как при ожидании ввода-вывода GIL освобождается. * Многопроцессорность: Это основной способ обхода GIL. Каждый процесс имеет свой собственный интерпретатор и свой GIL, что позволяет выполнять задачи параллельно.
Статья на хабре - Пул интерпретаторов в Python 3.14. Что, зачем и почему?
Что такое состояние гонки (race condition)?
Состояние гонки (race condition) — это ошибка в многопоточных программах, когда результат зависит от того, какой поток "победит" в гонке за доступ к общим данным.
Пример Два потока одновременно увеличивают счётчик x = 0:
python
# Поток 1: x = x + 1
# 1. читает x=0
# 2. x=1
# 3. пишет x=1
# Поток 2: x = x + 1 (параллельно)
# 1. читает x=0 (потому что Поток 1 ещё не записал!)
# 2. x=1
# 3. пишет x=1 (перезаписывает Поток 1)
Результат: x=1 вместо ожидаемых x=2. Кто успел записать последним — тот и победил.
Почему возникает * Несколько потоков читают+пишут в общую переменную/файл/БД без синхронизации. * ОС планировщик может переключить потоки в любой момент между чтением и записью. * Результат непредсказуем и зависит от тайминга (классический гейзенбаг).
Как избежать * Блокировки (threading.Lock, RLock): один поток за раз работает с данными. * Атомарные операции (queue.Queue). * Immutable объекты или копии данных для каждого потока. * В Python GIL частично помогает (защищает refcount), но для пользовательских данных нужны явные Lock'и.
Как GiL влияет на состояние гонки?
GIL предотвращает состояния гонки для внутренних структур Python, но НЕ предотвращает их для пользовательских данных.
Что GIL защищает от race condition
GIL делает атомарным выполнение байт-кода: пока один поток держит GIL, другой не может вмешиваться.
Это спасает:
* Refcount (счётчик ссылок): obj.refcnt +=1 или -=1 — без GIL два потока могли бы сломать память.
* Встроенные структуры: списки, словари, множества — их внутренние массивы/хэш-таблицы не блокируются, но GIL гарантирует, что только один поток их меняет.
Исключения
Обработка исключений в Python реализуется с помощью конструкции try—except—finally:
try:
# Python пробует выполнить эту часть кода
except:
# к этому блоку переходит, если не получилось выполнить try
finally:
# этот блок выполняется всегда
Ветка else в конструкции try…except…else будет выполнена только в том случае, если исключения не было возбуждено в блоке try. Если в блоке try произошло исключение, то выполнение программы переходит к соответствующему блоку except, и ветка else пропускается. Если блок except не указан, то исключение будет возбуждено дальше, а программа завершится с сообщением об ошибке.
Пример, в котором будет выполнена ветка else
try:
# some code here
except:
# code to handle the exception
else:
# code to execute if there is no exception
Если в блоке try не возникает исключений, то выполняется код в блоке else.
Статья на хабре: Полное руководство по обработке ошибок в Python
Разница между is и ==
== проверяет, одинаковые ли значения у переменных (проверка равенства значений), используя метод __eq__
(Когда вы используете a == b, Python вызывает метод a.__eq__(b))
is проверяет, указывают ли переменные на один и тот же объект (проверка идентичности), сравнивая адреса в памяти
(Python сравнивает идентификаторы объектов: id(a) == id(b))
a = [1, 2]
b = [1, 2]
print(a == b) #True
print(a is b) #False
Копия и глубокая копия
Метод copy() создает поверхностную копию объекта, то есть создает новый объект, который содержит ссылки на те же объекты, что и исходный объект. Если вы измените какой-либо из этих объектов, изменения отразятся и на копии, и на исходном объекте.
Метод deepcopy() создает глубокую копию объекта, то есть создает новый объект, который содержит копии всех объектов, на которые ссылаются элементы исходного объекта. Если вы измените какой-либо из этих объектов, изменения не отразятся на копии или на исходном объекте.
import copy
# создание копии объекта
new_list = old_list.copy()
# создание глубокой копии объекта
new_list = copy.deepcopy(old_list)
где old_list - исходный список, а new_list - его копия.
Примечание: для выполнения глубокого копирования объектов, сами объекты также должны поддерживать копирование. Если объекты в ваших данных не поддерживают копирование, deepcopy() вернет исходный объект, а не его копию.
Управление копированием через __copy__ и __deepcopy__
Метод __copy__
Позволяет кастомизировать поверхностное копирование. Когда вы определяете этот метод в классе, copy.copy() будет использовать вашу реализацию вместо стандартной.
Что можно контролировать:
- Какие атрибуты копировать, а какие нет
- Как именно создавать копии отдельных полей
- Какие данные сбрасывать/обновлять в копии
Метод __deepcopy__
Позволяет кастомизировать глубокое копирование. Принимает дополнительный параметр memo — словарь для отслеживания уже скопированных объектов.
Что можно контролировать:
- Глубину копирования для разных атрибутов
- Обработку циклических ссылок
- Оптимизацию процесса (например, не копировать неизменяемые объекты)
Слабые стороны Python
У языка программирования Python есть несколько слабых сторон, которые связаны с особенностями производительности, потребления памяти, ограничений в области видимости и ограниченности стандартных библиотек для решения некоторых задач.
Производительность
Интерпретируемая природа языка — код выполняется построчно интерпретатором, что добавляет накладные расходы по сравнению с компилируемыми языками.
* Динамическая типизация — проверка типов происходит во время выполнения, что требует дополнительных вычислительных ресурсов.
* Глобальная блокировка интерпретатора (GIL) — ограничивает выполнение только одной инструкции Python в любой момент времени на одном процессоре, что может стать узким местом в многопоточных приложениях. С версии 3.14 есть обход GIL.
* Медленные операции ввода-вывода* — если программа выполняет много операций ввода-вывода (например, чтение/запись файлов, работа с сетью), производительность может снизиться из-за ожидания завершения этих операций.
Память * Python использует больше памяти по сравнению с жёстко типизированными языками, такими как C или C++. Это может стать ограничением при работе с большими данными или в средах с ограниченной памятью. * Утечки памяти — память выделяется для конкретной задачи, но не освобождается после завершения процесса. Это особенно вредно, если утечка находится в часто выполняемом участке кода.
Область видимости
По умолчанию все имена, значения которым присваиваются внутри функции, ассоциируются с пространством имён этой функции.
Это означает, что:
Имена, определяемые внутри инструкции def, видны только программному коду внутри этой инструкции, к ним нельзя обратиться за пределами функции.
* Область видимости переменной определяется местом, где ей было присвоено значение, а не местом, откуда была вызвана функция.
Библиотеки
Некоторые библиотеки Python не всегда эффективны для решения некоторых задач. Например, некоторые библиотеки не освобождают память при завершении задачи или непрерывно запускают ненужные фоновые процессы, что напрягает доступные ресурсы. К таким библиотекам относится:
1. Библиотеки с агрессивным кэшированием (для производительности)
TensorFlow / PyTorch — преднамеренно кэшируют память на GPU/CPU между операциями для ускорения вычислений. Это может выглядеть как «утечка», но на самом деле — оптимизация.
Решение: torch.cuda.empty_cache(), tf.keras.backend.clear_session().
NumPy — при работе с большими массивами и срезами (views) может удерживать ссылки на исходные данные дольше ожидаемого.
Решение: использовать .copy() при необходимости.
2. Библиотеки с глобальным состоянием
Matplotlib — сохраняет все созданные фигуры в памяти до явного вызова plt.close() или plt.clf(). При генерации тысяч графиков в цикле это приводит к утечкам.
Решение: всегда вызывать plt.close(fig) после сохранения.
OpenCV (cv2) — при работе с видео (cv2.VideoCapture) требует явного освобождения ресурсов через .release().
3. ORM и кэши сессий
SQLAlchemy — identity map и кэш сессии могут удерживать объекты в памяти при долгоживущих сессиях.
Решение: использовать session.expunge_all(), session.close() или паттерн «сессия на запрос».
* Конфликты имён* — например, в модуле math есть функция sqrt(), но и в библиотеке cmath (для комплексных чисел) есть такая же функция. Это может привести к ошибкам, так как не ясно, из какого модуля используется функция.
Кто или что создает объект в Python?
В Python объекты создаются при вызове класса как функции (с круглыми скобками), что запускает встроенный механизм интерпретатора: сначала метод __new__ (статический, по умолчанию из базового класса object) аллоцирует память и возвращает сырой экземпляр, а затем автоматически вызывается __init__ для инициализации атрибутов.
Что такое "слабая" ссылка?
"Слабая ссылка" (англ. weak reference) — это специальный тип ссылки на объект в программировании, который не препятствует удалению этого объекта сборщиком мусора.
Обычная (сильная) ссылка "удерживает" объект в памяти: пока на него есть хотя бы одна такая ссылка — он не будет удалён. А слабая ссылка не удерживает объект. Если все обычные ссылки на объект исчезли, то даже если есть слабые — объект будет удалён из памяти.
Где применяется? * Кэширование * Системы наблюдения (observer pattern) * Деревья с родительскими ссылками (например, DOM) * Оптимизация памяти в длительных процессах
Модуль weakref — это встроенный модуль Python, который позволяет создавать слабые ссылки (weak references) на объекты.
import weakref
class Well:
def __init__(self, name):
self.name = name
well = Well("WELL-101")
weak_ref = weakref.ref(well) # слабая ссылка
print(weak_ref()) # <__main__.Well object> — объект жив
del well # последняя сильная ссылка удалена
print(weak_ref()) # None — объект уничтожен сборщиком мусора