Skip to content

Устройство Python

Чем отличается модуль от пакета?

Модуль — это файл, содержащий код Python, который может быть повторно использован в других программах.

Пакет — это директория, содержащая один или несколько модулей (или пакетов внутри пакетов), а также специальный файл __init__.py, который выполняется при импорте пакета. Он может содержать код, который инициализирует переменные, функции и классы, и становится доступным для использования внутри модулей, находящихся внутри этого пакета.

Таким образом, основная разница между модулем и пакетом заключается в том, что модуль — это файл с кодом, который можно использовать повторно, а пакет — это директория, которая может содержать один или несколько модулей. Код, находящийся в файле __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)

Статья на хабре 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-компиляция (в экспериментальной версии 3.13): Для ускорения работы программы, в экспериментальной версии Python 3.13 был добавлен встроенный JIT-компилятор. Он компилирует части кода в машинный код «на лету», прямо во время выполнения, что может значительно повысить производительность.

Весь этот сложный процесс скрыт от пользователя, что делает работу с Python простой и интуитивно понятной.


Сборщик мусора

В Python автоматическая сборка мусора. Это значит, что разработчику не надо явно следить за выделением и освобождением памяти. Система сама управляет памятью объектов без ссылок. Сборщик мусора работает, отслеживая ссылки на объекты в памяти, используя механизм подсчета ссылок. Каждый раз, когда создается новая ссылка на объект, счетчик ссылок для этого объекта увеличивается. Точно так же, когда ссылка удаляется, счетчик ссылок уменьшается. Когда счетчик ссылок уменьшается до 0, объект удаляется из памяти.

Иногда несколько объектов могут ссылаться друг на друга. В этих случаях Python использует механизм обнаружения циклических ссылок, который находит такие объекты и удаляет зацикленные ссылки. Этот механизм запускается переодически.

Статьи на хабре - CPython — сборка мусора изнутри ч.1

Всё, что нужно знать о сборщике мусора в Python


Что такое GIL?

GIL (Global Interpreter Lock) - это механизм, который обеспечивает потокобезопасность интерпретатора. Это особенно важно в контексте многопоточности, поскольку GIL ограничивает выполнение байткода Python только одним потоком за раз. Другими словами, в любой момент времени только один поток может выполняться в одном процессе. Это сделано для предотвращения гонок данных и других проблем, связанных с параллельным доступом к общим ресурсам. В любой момент может выполняться только один поток байткода Python. Глобальная блокировка интерпретатора — GIL — тщательно контролирует выполнение тредов. GIL гарантирует каждому потоку эксклюзивный доступ к переменным интерпретатора (и соответствующие вызовы C-расширений работают правильно).

Принцип работы прост: потоки удерживают GIL, пока выполняются. Однако они освобождают его при блокировании для операций ввода-вывода. Каждый раз, когда поток вынужден ждать, другие, готовые к выполнению потоки, используют свой шанс запуститься. Когда поток начинает работу, он выполняет захват GIL. Спустя какое-то время планировщик процессов решает, что текущий поток поработал достаточно, и передает управление следующему потоку. Поток №2 видит, что GIL захвачен, так что он не продолжает работу, а погружает себя в сон, уступая процессор потоку №1. Однако, такое поведение GIL может стать причиной проблем с производительностью, особенно на многоядерных системах, где несколько потоков могли бы выполняться параллельно.

Глобальная блокировка интерпретатора (GIL) создает фундаментальное ограничение: невозможность истинного параллельного выполнения байт-кода Python на нескольких ядрах процессора. Это особенно критично для CPU-интенсивных задач, где в других языках программирования многопоточность позволяет практически линейно ускорить вычисления за счет распределения нагрузки на все доступные ядра.

В 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. Практические следствия

На практике это означает, что разработчики могут эффективно использовать многопоточность для:

  • I/O-bound задач (веб-серверы, работа с базами данных)
  • Вычислений через специализированные библиотеки (NumPy, Pandas)
  • Собственных C-расширений, освобождающих GIL

Однако для "чистых" CPU-интенсивных вычислений на чистом Python многопоточность не дает преимуществ, и в таких случаях следует использовать многопроцессорность, где каждый процесс имеет собственный интерпретатор и, соответственно, собственный GIL.

Основные изменения, связанные с GIL в Python 3.14:

  • Отказ от обязательного GIL: в Python 3.14 появилась возможность отключать GIL, позволяя потокам выполняться параллельно;
  • Субинтерпретаторы: введена возможность создавать субинтерпретаторы, каждый из которых имеет свой собственный GIL. Это позволяет запускать потоки с разными блокировками, обеспечивая настоящий параллелизм;
  • API для управления GIL: доступ к этой функциональности также можно получить через переменную окружения PYTHON_GIL или опцию командной строки -X gil;
  • Внутренние изменения: наряду с внешними изменениями, произошли и внутренние реформы, такие как новый интерпретатор байт-кода, который улучшает производительность, устранение глобального состояния и потокобезопасный механизм управления памятью, что заложило основу для отключения GIL и принесло общее повышение производительности.

Статья на хабре - Пул интерпретаторов в Python 3.14. Что, зачем и почему?


Исключения

Обработка исключений в 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 — словарь для отслеживания уже скопированных объектов.

Что можно контролировать:

  • Глубину копирования для разных атрибутов
  • Обработку циклических ссылок
  • Оптимизацию процесса (например, не копировать неизменяемые объекты)