Пакеты Python и система импорта
Как работают пакеты Python: создание пакета с __init__.py, абсолютные и относительные импорты, публичный API и типичные ошибки.
Пакет — это директория Python модулей, которую вы используете как единую импортируемую единицу. Если модуль — это один файл .py, то пакет — это папка, потенциально содержащая множество модулей и подпакетов, по которой система импорта Python может перемещаться как по дереву. В этой главе объясняется, как создавать пакеты, управлять тем, что они предоставляют, писать абсолютные и относительные импорты правильно, а также избегать ошибок, на которые часто натыкаются новички.
Модули и пакеты — ключевое различие
Модуль — это один файл .py:
greetings.py ← moduleПакет — это директория, содержащая хотя бы один специальный файл __init__.py:
greetings/ ← package
__init__.py
english.py
spanish.pyОба импортируются с помощью одного и того же ключевого слова import, но пакет даёт вам иерархию пространств имён: greetings.english и greetings.spanish — это отдельные модули, но они разделяют пространство имён greetings.
Когда использовать модуль, а когда пакет:
| Ситуация | Использование |
|---|---|
| Небольшая самодостаточная утилита | Модуль (один файл .py) |
| Несколько связанных модулей под одним именем | Пакет (директория) |
| Библиотека для публикации на PyPI | Пакет (с раскладкой src/) |
Файл __init__.py
__init__.py — это то, что превращает директорию в пакет. Python выполняет его при первом импорте пакета (или любого из его модулей). Он может быть пустым или:
- импортировать имена из подмодулей, делая их доступными на уровне пакета;
- выполнять инициализацию уровня пакета (настройка логирования, проверки версий и т. д.);
- определять
__all__для управления поведениемfrom package import *.
Минимальная структура пакета
myapp/
__init__.py ← can be empty
utils.py
config.py# myapp/__init__.py (empty — that is fine)# myapp/utils.py
def greet(name):
return f"Hello, {name}!"Импорт из-за пределов пакета:
from myapp.utils import greet
print(greet("Alice")) # Hello, Alice!Вывод имён на уровень пакета
Распространённый паттерн — импортировать наиболее используемые имена в __init__.py, чтобы вызывающий код мог писать from myapp import greet вместо from myapp.utils import greet.
# myapp/__init__.py
from .utils import greet
from .config import MAX_RETRIESТеперь оба имени доступны непосредственно через пакет:
import myapp
print(myapp.greet("Bob")) # Hello, Bob!
print(myapp.MAX_RETRIES) # whatever config.py definesАбсолютные импорты
Абсолютный импорт всегда начинается с пакета верхнего уровня или из директории в sys.path. Он не зависит от местоположения файла, выполняющего импорт.
project/
myapp/
__init__.py
utils.py
services/
__init__.py
email.pyВнутри email.py абсолютный импорт выглядит так:
# myapp/services/email.py
from myapp.utils import greet # absolute — starts from the top-level package
def send_welcome(user):
message = greet(user)
print(f"Sending: {message}")Абсолютные импорты — стиль по умолчанию и рекомендуемый (PEP 8). Они однозначны независимо от того, как запускается Python.
Относительные импорты
Относительный импорт использует точки (.) для навигации по дереву пакетов относительно расположения текущего файла.
.означает текущий пакет..означает родительский пакет...означает пакет «дедушки» и так далее
# myapp/services/email.py
# One dot — import from myapp.services (same directory)
from . import sms
# Two dots — import from myapp (parent directory)
from ..utils import greetКогда использовать относительные импорты
Относительные импорты полезны внутри пакета, когда нужно явно показать, что greet происходит из этого пакета, а не из какой-то внешней библиотеки с тем же именем. Они также облегчают рефакторинг, потому что импорты перемещаются вместе с пакетом при переименовании директории верхнего уровня.
Ловушка: относительные импорты работают только внутри пакета. Если запустить python myapp/utils.py напрямую, Python воспринимает его как самостоятельный скрипт, а не часть пакета, и относительный импорт вызовет ImportError: attempted relative import with no known parent package. Вместо этого запускайте пакет с помощью python -m myapp.utils.
# Wrong — runs utils.py as a script, breaking relative imports
$ python myapp/utils.py
# Right — runs utils.py as part of the myapp package
$ python -m myapp.utilsУправление публичным API с помощью __all__
__all__ — это список имён, которые экспортирует from package import *. Он также документирует, что пакет считает публичным.
# myapp/__init__.py
from .utils import greet, farewell
from .config import MAX_RETRIES
__all__ = ["greet", "MAX_RETRIES"] # farewell is intentionally not exportedТеперь from myapp import * импортирует только greet и MAX_RETRIES. Функция farewell по-прежнему существует, она просто не является частью объявленного публичного интерфейса. Имена с префиксом из одного подчёркивания (_private) также исключаются из import * по соглашению.
Вложенные пакеты (подпакеты)
Пакеты могут содержать другие пакеты. Каждая подпапка нуждается в собственном __init__.py.
analytics/
__init__.py
reports/
__init__.py
daily.py
weekly.py
charts/
__init__.py
bar.py
pie.pyИмпортируйте глубоко вложенный модуль с помощью полного пути через точку:
from analytics.reports.daily import generate_report
from analytics.charts.bar import BarChartИли, если analytics/__init__.py предоставляет их:
# analytics/__init__.py
from .reports.daily import generate_report# caller
from analytics import generate_reportНасколько глубокой должна быть вложенность?
Пакет с тремя-четырьмя уровнями вложенности — обычно признак того, что он стал слишком большим и его следует разделить на отдельные пакеты верхнего уровня (устанавливаемые по отдельности). Для большинства проектов достаточно двух уровней (package.module).
Практический пример: создание пакета geometry
Давайте пошагово создадим небольшой, но реалистичный пакет.
Структура директорий
geometry/
__init__.py
shapes.py
conversions.pyshapes.py
# geometry/shapes.py
import math
def circle_area(radius):
"""Return the area of a circle with the given radius."""
if radius < 0:
raise ValueError("radius must be non-negative")
return math.pi * radius ** 2
def rectangle_area(width, height):
"""Return the area of a rectangle."""
return width * height
def triangle_area(base, height):
"""Return the area of a triangle."""
return 0.5 * base * heightconversions.py
# geometry/conversions.py
def degrees_to_radians(degrees):
"""Convert degrees to radians."""
import math
return degrees * math.pi / 180
def radians_to_degrees(radians):
"""Convert radians to degrees."""
import math
return radians * 180 / math.pi__init__.py — предоставление ключевых имён
# geometry/__init__.py
"""
geometry — simple 2-D geometry utilities.
Public API:
circle_area(radius) -> float
rectangle_area(width, height) -> float
triangle_area(base, height) -> float
degrees_to_radians(degrees) -> float
radians_to_degrees(radians) -> float
"""
from .shapes import circle_area, rectangle_area, triangle_area
from .conversions import degrees_to_radians, radians_to_degrees
__all__ = [
"circle_area",
"rectangle_area",
"triangle_area",
"degrees_to_radians",
"radians_to_degrees",
]Использование пакета
# main.py (sits next to the geometry/ directory)
import geometry
print(geometry.circle_area(5)) # 78.53981633974483
print(geometry.rectangle_area(4, 6)) # 24
print(geometry.degrees_to_radians(90)) # 1.5707963267948966Или с помощью выборочных импортов:
from geometry import circle_area, degrees_to_radians
print(circle_area(3)) # 28.274333882308138
print(degrees_to_radians(180)) # 3.141592653589793Пакеты пространств имён (Python 3.3+)
Начиная с Python 3.3, директория без __init__.py является пакетом пространства имён. Python объединяет все директории с одинаковым именем в sys.path в один логический пакет. Это в основном полезно для крупных организаций, которые распределяют один пакет по нескольким репозиториям или директориям установки.
Для повседневной разработки всегда включайте __init__.py. Это делает ваши намерения однозначными и работает во всех версиях Python.
Как Python находит пакеты
Когда вы пишете import geometry, Python ищет в sys.path по порядку:
- Директория запускаемого скрипта (или текущая директория в интерактивном режиме)
- Директории в переменной окружения
PYTHONPATH - Директории стандартной библиотеки
- Директория
site-packages(где хранятся пакеты, установленные через pip)
import sys
print(sys.path)Директория пакета должна находиться непосредственно в одном из этих мест. Если geometry/ находится в /home/alice/projects/, Python не найдёт его, пока /home/alice/projects/ не будет добавлен в sys.path.
Совет: используйте виртуальное окружение и установите пакет в режиме разработки (pip install -e .), чтобы Python всегда находил его без ручных манипуляций с sys.path.
Распространение пакета
Чтобы поделиться пакетом с другими (или установить его через pip), вам нужен файл pyproject.toml в корне проекта:
my_project/
pyproject.toml ← build metadata
src/
geometry/
__init__.py
shapes.py
conversions.pyМинимальный pyproject.toml:
[build-system]
requires = ["setuptools>=68", "wheel"]
build-backend = "setuptools.backends.legacy:build"
[project]
name = "geometry"
version = "0.1.0"
description = "Simple 2-D geometry utilities"
requires-python = ">=3.9"Локальная установка в редактируемом режиме во время разработки:
pip install -e .Теперь import geometry работает в любом месте виртуального окружения независимо от текущей директории.
Типичные ошибки
Отсутствие __init__.py
Если вы забудете добавить __init__.py, Python 3 воспринимает директорию как пакет пространства имён (что обычно всё равно работает), но Python 2 полностью игнорирует её. Будьте явными: всегда добавляйте __init__.py.
Именование пакета так же, как модуль стандартной библиотеки
Избегайте имён вроде math/, json/, os/, email/. Python может импортировать ваш пакет вместо модуля стандартной библиотеки, ломая несвязанный код.
Запуск модуля пакета как скрипт
Как отмечалось выше, запуск python myapp/services/email.py напрямую нарушает относительные импорты. Вместо этого используйте python -m myapp.services.email.
Циклические импорты между модулями одного пакета
Если shapes.py импортирует из conversions.py, а conversions.py импортирует из shapes.py, возникает циклический импорт. Симптомы — ImportError или имена, неожиданно равные None. Решение обычно состоит в том, чтобы переместить общую логику в третий модуль или отложить импорт внутрь тела функции.
# Delayed import — breaks the cycle at module load time
def some_function():
from .shapes import circle_area # imported only when the function is called
...ImportError при использовании относительных импортов вне пакета
# Will raise: ImportError: attempted relative import with no known parent package
# if run as: python myapp/utils.py
from . import config # relative import inside utils.pyЗапустите как python -m myapp.utils или реструктурируйте так, чтобы точка входа была отдельным скриптом, импортирующим пакет.
Итоги
| Понятие | Кратко |
|---|---|
| Пакет | Директория с __init__.py, содержащая модули |
__init__.py | Делает директорию пакетом; выполняется при первом импорте |
| Абсолютный импорт | from myapp.utils import greet — всегда от корня |
| Относительный импорт | from ..utils import greet — относительно текущего файла |
__all__ | Список имён, экспортируемых через from package import * |
| Пакет пространства имён | Директория без __init__.py; только Python 3.3+ |
| Установка в режиме разработки | pip install -e . — пакет доступен везде в venv |
Смотрите Модули Python для организации кода в отдельных файлах, Python pip для установки сторонних пакетов и Виртуальные окружения Python для изоляции зависимостей проекта.