Структура пакета: __init__.py и вложенные модули
Мы уже знаем, что модуль — это файл, а пакет — папка. Но что делает папку «настоящим» пакетом? Как управлять тем, что видно снаружи? И как складывать пакеты внутрь пакетов? Разберём всё до последней детали.
📦 Что такое пакет — объясняем на пальцах
В прошлом уроке мы научились создавать отдельные файлы-модули. Это здорово, но представьте: у вас уже 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 ЧтоНужно.