W3docs

Пакеты 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.py

shapes.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 * height

conversions.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 по порядку:

  1. Директория запускаемого скрипта (или текущая директория в интерактивном режиме)
  2. Директории в переменной окружения PYTHONPATH
  3. Директории стандартной библиотеки
  4. Директория 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 для изоляции зависимостей проекта.

Практика

Практика
What makes a directory a Python package (in Python versions before 3.3)?
What makes a directory a Python package (in Python versions before 3.3)?
Was this page helpful?