$ sudo teach IT
МОДУЛЬ 9 · УРОК 3

Структура пакета: __init__.py и вложенные модули

Мы уже знаем, что модуль — это файл, а пакет — папка. Но что делает папку «настоящим» пакетом? Как управлять тем, что видно снаружи? И как складывать пакеты внутрь пакетов? Разберём всё до последней детали.

⏱ ~45 минут 🎓 Для новичков 🐍 Python 3

📦 Что такое пакет — объясняем на пальцах

В прошлом уроке мы научились создавать отдельные файлы-модули. Это здорово, но представьте: у вас уже 20 файлов .py, и всё это лежит в одной папке кучей. Найти нужный файл становится мучением.

Решение — пакет. Пакет позволяет сгруппировать связанные модули в одну папку и работать с ними как с единым целым.

Пакет — это папка с файлами .py, которая сама является модулем.
То есть вы можете делать import папка точно так же, как import файл.

Вот простая аналогия: модуль — это книга, а пакет — полка с книгами. Вы можете взять книгу с полки (from shapes import circle), или всю полку сразу (import shapes).

⚠️ Важно: не любая папка автоматически становится пакетом! Чтобы Python «признал» папку пакетом, нужен специальный файл — __init__.py. Сейчас разберём, зачем он нужен.

🔑 Файл __init__.py — «удостоверение» пакета

__init__.py (читается: «дандер инит», от англ. double underscore — двойное подчёркивание) — это специальный файл, который Python ищет внутри папки, чтобы понять: «это пакет, а не просто папка».

❌ Просто папка

Папка shapes/ без __init__.py — Python не знает, что это пакет. Импорт не сработает (в старых версиях).

✅ Настоящий пакет

Папка shapes/ с файлом __init__.py внутри — Python видит пакет. Можно делать import shapes.

История: обязателен или нет?

До Python 3.3 файл __init__.py был обязателен — без него импорт не работал вообще. Начиная с Python 3.3 появились так называемые namespace packages («пространство имён пакетов»): папка без __init__.py тоже может быть пакетом.

💡 Рекомендация для новичков: всегда создавайте __init__.py, даже пустой. Это «классический» стиль, который работает в 100% случаев и понятен всем разработчикам. Namespace packages — для продвинутых сценариев, которые вам пока не нужны.

Что происходит при импорте пакета?

Когда вы пишете import shapes, Python находит папку shapes/, видит внутри __init__.py и выполняет его. Именно поэтому этот файл так важен — он запускается первым, как «вступление» пакета.

🌳 Реальная структура пакета

Давайте создадим настоящий пакет shapes («фигуры»), который содержит классы для работы с геометрическими фигурами. Вот как выглядит структура файлов:

my_project/
├── main.py              ← наш главный файл, с которого всё запускается
└── shapes/              ← папка-пакет
    ├── __init__.py      ← обязательный «паспорт» пакета
    ├── circle.py        ← модуль с классом Circle
    ├── rectangle.py     ← модуль с классом Rectangle
    └── triangle.py      ← модуль с классом Triangle

Теперь посмотрим, что находится в каждом файле. Начнём с модулей:

# shapes/circle.py
import math

class Circle:
    def __init__(self, radius):
        self.radius = radius

    def area(self):
        return math.pi * self.radius ** 2

    def perimeter(self):
        return 2 * math.pi * self.radius

    def __repr__(self):
        return f"Circle(radius={self.radius})"
# shapes/rectangle.py

class Rectangle:
    def __init__(self, width, height):
        self.width = width
        self.height = height

    def area(self):
        return self.width * self.height

    def perimeter(self):
        return 2 * (self.width + self.height)

    def __repr__(self):
        return f"Rectangle(width={self.width}, height={self.height})"

⚙️ Пустой __init__.py vs умный — в чём разница?

Это ключевой момент урока. Посмотрим на конкретные примеры того, как меняется работа с пакетом в зависимости от содержимого __init__.py.

Вариант 1: пустой __init__.py

Самый простой вариант — файл вообще ничего не содержит (или содержит только комментарий):

# shapes/__init__.py
# (файл пустой)

Тогда, чтобы использовать класс Circle, пользователю нужно писать полный путь:

# main.py — при ПУСТОМ __init__.py
from shapes.circle import Circle    # нужно знать точное расположение класса
from shapes.rectangle import Rectangle

c = Circle(5)
r = Rectangle(3, 4)
print(c.area())  # 78.53...
print(r.area())  # 12

Это работает, но неудобно: пользователь пакета должен знать, в каком именно файле живёт каждый класс. При большом пакете это превращается в боль.

Вариант 2: «умный» __init__.py с удобными импортами

Хороший тон — поместить в __init__.py импорты всего, что нужно пользователям вашего пакета. Тогда они смогут импортировать напрямую из пакета, не зная внутренней структуры:

# shapes/__init__.py — «умная» версия
from shapes.circle import Circle
from shapes.rectangle import Rectangle
from shapes.triangle import Triangle

# Теперь пользователь может писать просто:
# from shapes import Circle
# вместо: from shapes.circle import Circle

Смотрите, насколько проще становится main.py:

# main.py — при УМНОМ __init__.py
from shapes import Circle, Rectangle  # просто! не нужно знать внутренние файлы

c = Circle(5)
r = Rectangle(3, 4)
print(c.area())     # 78.53...
print(r.area())     # 12
print(r.perimeter()) # 14

✅ Правило хорошего пакета: пользователь не должен лезть внутрь и разбираться, где что лежит. Задача __init__.py — предоставить удобный «фасад», через который открыт доступ ко всему нужному.

🪆 Вложенные пакеты — пакет внутри пакета

Пакеты можно вкладывать друг в друга — это называется вложенные пакеты (nested packages). Это позволяет строить иерархическую структуру, как папки на компьютере.

Представьте, что наш пакет shapes вырос и теперь нужно разделить фигуры на двумерные (2D) и трёхмерные (3D):

my_project/
├── main.py
└── shapes/
    ├── __init__.py          ← пакет shapes
    ├── flat/                ← вложенный пакет (2D фигуры)
    │   ├── __init__.py      ← ОБЯЗАТЕЛЕН для вложенного пакета тоже!
    │   ├── circle.py
    │   └── rectangle.py
    └── solid/               ← ещё один вложенный пакет (3D фигуры)
        ├── __init__.py
        ├── sphere.py
        └── cube.py

Каждый вложенный пакет тоже должен иметь свой __init__.py! Вот как выглядят импорты:

# Способ 1: полный путь через точки
from shapes.flat.circle import Circle
from shapes.solid.sphere import Sphere

# Способ 2: если __init__.py настроен удобно
# В shapes/flat/__init__.py написано: from shapes.flat.circle import Circle
# Тогда можно:
from shapes.flat import Circle

# Способ 3: если и shapes/__init__.py настроен
# В shapes/__init__.py написано: from shapes.flat import Circle, Rectangle
# Тогда совсем просто:
from shapes import Circle

🔗 Точка — это разделитель уровней. shapes.flat.circle означает: «в пакете shapes, в подпакете flat, в модуле circle». Каждая точка — это один уровень вложенности.

🎛️ Переменная __all__ — контроль над импортом

Есть ещё одна важная вещь в __init__.py — переменная __all__. Она управляет тем, что происходит при «импорте звёздочкой»: from shapes import *.

Что такое from package import *?

Звёздочка * означает «импортируй всё». Это удобно, но опасно — можно случайно затащить лишние имена. Именно поэтому нужна __all__:

# shapes/__init__.py с __all__
from shapes.circle import Circle
from shapes.rectangle import Rectangle
from shapes.triangle import Triangle

# __all__ — список строк с именами, которые будут импортированы при "import *"
__all__ = ["Circle", "Rectangle", "Triangle"]

# Теперь при "from shapes import *" в программу попадут только эти три класса

Посмотрим, как это работает на практике:

# main.py
from shapes import *   # импортируем всё из __all__

# Теперь Circle, Rectangle, Triangle доступны напрямую
c = Circle(7)
r = Rectangle(4, 6)
print(c.area())     # 153.93...
print(r.perimeter()) # 20

⚠️ Без __all__

from shapes import * импортирует всё подряд, включая служебные переменные и импорты — это мусор в вашем пространстве имён.

✅ С __all__

from shapes import * импортирует только то, что вы явно разрешили. Чисто, предсказуемо, безопасно.

💡 Совет: from package import * в серьёзном коде используют редко — лучше всегда указывать явно, что именно вы импортируете. Но __all__ всё равно стоит определять — это документация для других разработчиков: «вот что является публичным API этого пакета».

🚀 Собираем всё вместе: полный пример

Давайте напишем финальную версию всех файлов и посмотрим, как они работают вместе. Вот полная структура:

my_project/
├── main.py
└── shapes/
    ├── __init__.py
    ├── circle.py
    └── rectangle.py
# shapes/circle.py
import math

class Circle:
    def __init__(self, radius):
        self.radius = radius

    def area(self):
        return round(math.pi * self.radius ** 2, 2)

    def perimeter(self):
        return round(2 * math.pi * self.radius, 2)

    def __str__(self):
        return f"Круг с радиусом {self.radius}"
# shapes/rectangle.py

class Rectangle:
    def __init__(self, width, height):
        self.width = width
        self.height = height

    def area(self):
        return self.width * self.height

    def perimeter(self):
        return 2 * (self.width + self.height)

    def __str__(self):
        return f"Прямоугольник {self.width}x{self.height}"
# shapes/__init__.py
from shapes.circle import Circle
from shapes.rectangle import Rectangle

__all__ = ["Circle", "Rectangle"]

# Можно добавить метаданные пакета:
__version__ = "1.0.0"
__author__ = "Ваше имя"
# main.py
from shapes import Circle, Rectangle   # удобный импорт благодаря __init__.py

# Создаём фигуры
c = Circle(5)
r = Rectangle(3, 4)

# Используем методы
print(c)              # Круг с радиусом 5
print(f"Площадь: {c.area()}")       # Площадь: 78.54
print(f"Периметр: {c.perimeter()}") # Периметр: 31.42

print(r)              # Прямоугольник 3x4
print(f"Площадь: {r.area()}")       # Площадь: 12
print(f"Периметр: {r.perimeter()}") # Периметр: 14

# Можно узнать версию пакета:
import shapes
print(shapes.__version__)  # 1.0.0

🏁 Итоги урока

📦 Пакет

Папка с __init__.py внутри. Позволяет объединять модули в логическую группу.

🔑 __init__.py

Выполняется при импорте пакета. Хранит удобные переиспользуемые импорты для пользователей пакета.

🪆 Вложенность

Пакеты можно вкладывать. Уровни разделяются точкой: shapes.flat.circle. У каждого уровня — свой __init__.py.

🎛️ __all__

Список строк — имён, доступных при from package import *. Это публичный API вашего пакета.

Главное правило: хороший __init__.py — это «лицо» пакета. Пользователь не должен знать его внутреннюю структуру. Всё необходимое должно быть доступно напрямую через from package import ЧтоНужно.