Структура PythonGo-проектаГлава 2.5
02 / 05 Один проект · следующий шаг

Собираем пакет и запускаем его

Группируем связанные модули, добавляем __init__.py и разбираем команду python -m.

Разбираем, что такое пакет и internal, и собираем проект командами go build и go run.

Результат главыapp/ → services/ + utils/internal/ → services/ + utils/
Ваш терминал
Для работы вместе с преподавателем

Когда становится тесно в корне

Выполните в своём проекте
Где
Explorer в той же папке student-tools.
Действие
Проверьте раскладку из главы 2.4: main.go в корне, internal/services/calculator.go, internal/utils/formatter.go.
Результат
В корне остались только main.go, go.mod и go.sum; прикладной код — в internal.
Проверка
Раскройте папки и сопоставьте пути с деревом ниже. Затем выполните go build ./... — сборка должна пройти молча.

Три файла легко найти. Когда модулей становится тридцать, связанные части объединяют в папки. Пакет — это папка с модулями, которую можно импортировать: путь app.services.calculator сообщает, где лежит файл и за что он отвечает.

По какому принципу файл попадает в свою папку

Раскладку определяет ответственность файла — то, что он делает. В проекте student-tools ответственностей четыре, и каждой соответствует своё место:

Что делает файлКуда попадаетНаш пример
запускает программу: берёт данные, вызывает расчёт, печатает результатточка запуска в корне пакетаapp/main.py
решает задачу предметной области: считает средний баллservices — правила этого проектаapp/services/calculator.py
выполняет мелкую вспомогательную работу: превращает число в строкуutils — функции, переносимые в другой проект без измененийapp/utils/formatter.py
проверяет кодtests — отдельная папка в корне проектаtests/test_calculator.py (глава 2.6)

Проверить раскладку можно одним вопросом: «что придётся открыть, чтобы изменить формулу расчёта?» В правильной структуре ответ — один файл, calculator.py. Если для этого нужно править ещё и точку запуска, ответственности перемешаны.

Окружение .venv в эту схему не входит: это не код проекта, а локальный Python с установленными библиотеками. Оно остаётся в корне рядом с app.

Что меняется в папке

Переместите main.py в app, calculator.py в app/services, formatter.py в app/utils. Старые копии в корне удалите после переноса. Слева — как было после главы 2.4, справа — как станет:

После главы 2.4
student-tools/
├ .venv/
├ main.py
├ calculator.py
└ formatter.py
После главы 2.5
student-tools/
├ .venv/
└ app/
  ├ __init__.py
  ├ main.py
  ├ services/
  │ ├ __init__.py
  │ └ calculator.py
  └ utils/
    ├ __init__.py
    └ formatter.py
Терминал: до переноса команда ls показывает три файла в корне; после переноса find показывает app с подпапками services и utils.
Та же раскладка в терминале · macOS / Python 3.12

Сверху состояние после главы 2.4: три файла лежат рядом в корне. Снизу состояние после переноса: все файлы кода находятся внутри app, в каждой папке появился __init__.py. Папка .venv из вывода исключена — в структуру проекта она не входит.

Нажмите на изображение, чтобы увеличить.
Выполните в своём проекте
Где
Explorer в той же папке student-tools.
Действие
Создайте app, внутри services и utils. Перетащите main.py в app, calculator.py в services, formatter.py в utils.
Результат
В корне не осталось прежних .py-файлов; .venv остаётся в корне.
Проверка
Сопоставьте свои папки с деревом выше. Пока не запускайте: импорты обновим через два шага.

Три файла легко найти. Когда пакетов становится тридцать, связанные части объединяют в каталоги. Путь internal/services/calculator.go говорит больше, чем случайное file3.go, а имя internal вдобавок сообщает, что эти пакеты не предназначены для внешнего использования.

В Go пакеты собраны ещё в главе 2.4: каждая папка стала отдельным пакетом в момент создания. В этой главе мы разбираем, что именно из этого следует, и учимся собирать проект целиком, а не только запускать его.

Назначение каждой строки · нажмите на файл или папку
student-tools/

Корень проекта. Отсюда выполняются команды запуска.

В главе 2.6 в корень добавятся папка tests, файлы .gitignore, requirements.txt и README.md.

В главе 2.6 рядом с кодом появится файл теста, а в корне — .gitignore и README.md.

__init__.py: обычный пакет

Выполните в своём проекте
Где
Explorer: папки app, app/services и app/utils.
Действие
В каждой папке: New File → __init__.py → сохранить пустым.
Результат
Появились три отдельных пустых файла с одинаковым именем.
Проверка
Проверьте два подчёркивания до и после init и расширение .py.

Создайте пустой __init__.py в app, services и utils. В имени по два подчёркивания с обеих сторон init. Ниже — что это за файл и почему он пустой.

Модуль, пакет и точки в импорте

Два слова, которые дальше встречаются постоянно. Модуль — это один файл .py, например calculator.py. Пакет — это папка с модулями, которую тоже можно импортировать, например app. Точки в записи app.services.calculator обозначают вложенность: папка app → папка services → файл calculator.py. Читая импорт, вы читаете путь по дереву проекта.

Что делает сам файл __init__.py

Он играет две роли сразу.

  • Объявляет папку обычным пакетом. По этому файлу Python понимает, что папку нужно считать пакетом.
  • Является телом пакета. Код внутри __init__.py выполняется в момент первого импорта пакета — так же, как код верхнего уровня обычного модуля из главы 2.4. Пустой файл просто ничего не делает.

Причём выполняется он не только при import app. Импортируя что-то в глубине, Python проходит весь путь по дереву и выполняет __init__.py каждого пакета по дороге — сверху вниз.

Терминал: при импорте calculate_average печатаются строки о выполнении app/__init__.py, app/services/__init__.py и calculator.py в этом порядке.
Один импорт — три файла по дороге · macOS / Python 3.12

Для наглядности в каждый __init__.py временно добавлен print. Команда импортирует одну функцию, но Python сначала выполнил app/__init__.py, затем app/services/__init__.py и только потом сам calculator.py. В вашем проекте эти файлы пустые, поэтому вы ничего не замечаете — но проходит Python по ним точно так же.

Скачать текст вывода ↓ · Нажмите на изображение, чтобы увеличить.

Почему в учебном проекте он пустой

В __init__.py обычно складывают то, что относится к пакету целиком: короткие псевдонимы для импортов, версию, общие настройки. Этот код выполнится при любом импорте из пакета, даже когда нужна одна функция: импорт app.services.calculator сначала выполнит app/__init__.py. Поэтому в __init__.py не кладут расчёты, чтение файлов и обращения к сети, а в учебном проекте оставляют его пустым.

Почему иногда пакет работает и без него

Вы можете удалить __init__.py, и импорт всё равно сработает. Начиная с Python 3.3 папка без этого файла тоже импортируется — как namespace package. Разница видна, если посмотреть на сам объект пакета.

Терминал: import app с __init__.py показывает module app from app/__init__.py, без файла — module app (namespace) и __file__: None.
Одна и та же папка app, с файлом и без · macOS / Python 3.12

Сверху обычный пакет: Python показывает, из какого файла он собран, и __file__ указывает на app/__init__.py. Снизу та же папка без файла: в скобках стоит слово namespace, а __file__ равно None — тела у такого пакета просто нет. Механизм namespace нужен для редкого случая, когда один пакет собирают из нескольких папок в разных местах диска.

Скачать текст вывода ↓ · Нажмите на изображение, чтобы увеличить.

В своих проектах создавайте __init__.py явно. Пакет получает тело, поведение импорта становится предсказуемым, а инструменты и старые версии библиотек надёжнее видят вашу папку как пакет. При этом отсутствие файла в чужом проекте само по себе не является ошибкой.

Папка — это и есть пакет

Выполните в своём проекте
Где
Терминал в student-tools.
Действие
Выполните go list ./... — команда перечислит все пакеты модуля.
Результат
Три строки: сам модуль, internal/services и internal/utils.
Проверка
Число строк совпадает с числом папок, в которых лежат .go-файлы.
ТЕРМИНАЛ · student-tools/
go list ./...

Никакого файла-маркера вроде __init__.py в Go нет. Папка становится пакетом сама, как только в ней появляется .go-файл с объявлением package. Создавать что-то дополнительно не нужно — и забыть тоже нечего.

Файл, пакет и путь импорта

Три слова, которые дальше встречаются постоянно. Файл — одна единица текста, calculator.go. Пакет — папка целиком: все файлы в ней с одинаковой строкой package services образуют один пакет и видят имена друг друга без импортов. Путь импорта — адрес папки от имени модуля: example.com/student-tools/internal/services.

Отсюда следствие, непривычное после Python: разбивать пакет на несколько файлов можно свободно. Можно держать calculator.go и average.go в одной папке, и это по-прежнему один пакет services с общим набором имён. Новая граница возникает только вместе с новой папкой.

Все файлы одной папки — один пакет

Из этого следует жёсткое правило: в одной папке не может быть двух разных значений package. Попытка положить рядом файл с package services и файл с package calculator закончится ошибкой сборки found packages services and calculator in .... Компилятор не выбирает между ними: сборка останавливается.

Имя пакета при этом не обязано совпадать с именем папки: язык этого не требует. Но в путях импорта видно именно имя папки, а в вызовах — имя пакета, поэтому расхождение сбивает читателя. Договорённость такая: называйте пакет так же, как папку; расхождение допускают в отдельных случаях и объясняют его в README.

Зачем нужна папка internal

Имя internal Go понимает особым образом. Пакеты внутри неё доступны только коду того же модуля — точнее, коду, лежащему рядом с папкой internal или ниже. Чужой модуль, подключивший ваш как библиотеку, получит на импорте ошибку use of internal package not allowed.

Это единственный способ в Go ограничить видимость на уровне пакетов, и работает он на компиляции, а не по договорённости. Заглавная буква решает, что видно за пределами пакета; internal решает, что видно за пределами модуля.

Импорт отражает новую структуру

Выполните в своём проекте
Где
Редактор: student-tools/main.go.
Действие
Сверьте два внутренних импорта с кодом ниже: они начинаются с имени модуля.
Результат
Пути импортов начинаются с example.com/student-tools/internal.
Проверка
calculator.go и formatter.go остались прежними. Следующим шагом соберём проект целиком.
Выполните в своём проекте
Где
Редактор: student-tools/app/main.py.
Действие
Обновите два локальных импорта как в коде ниже и сохраните файл.
Результат
Пути импортов начинаются с app.services и app.utils.
Проверка
calculator.py и formatter.py остались прежними. Следующим шагом выполните запуск через -m.
student-tools/main.go
package main

import (
	"github.com/fatih/color"

	"example.com/student-tools/internal/services"
	"example.com/student-tools/internal/utils"
)

func main() {
	values := []int{5, 4, 5, 3, 5}
	average, err := services.CalculateAverage(values)
	if err != nil {
		color.Red("Ошибка: %v", err)
		return
	}
	color.Green(utils.FormatAverage(average))
}
student-tools/app/main.pyСкачать файл ↓
from rich.console import Console

from app.services.calculator import calculate_average
from app.utils.formatter import format_average


def main():
    values = [5, 4, 5, 3, 5]
    average = calculate_average(values)
    console = Console()
    console.print(f"[bold green]{format_average(average)}[/bold green]")


if __name__ == "__main__":
    main()

Содержимое calculator.py и formatter.py остаётся прежним. Меняются два импорта в main.py: добавляются имена пакетов.

Содержимое calculator.go и formatter.go остаётся прежним. Внутренние импорты — это всегда полный путь от имени модуля из go.mod; сокращённой и относительной записи в Go нет, поэтому по любому импорту сразу видно, из какого модуля пришёл код.

Разберите импорт по шагам
..→
//→

Нажмите на часть пути: от пакета приложения до конкретной функции.

Форма импорта и команда запуска связаны

Выполните в своём проекте
Где
Терминал: student-tools/, рядом с app.
Действие
Выполните python -m app.main, затем команду проверки импорта в конце раздела.
Результат
Запуск печатает 4.40; импорт app.main не печатает ничего.
Проверка
Если Python сообщает No module named app, проверьте текущую папку: она должна содержать app, а не находиться внутри неё.
ТЕРМИНАЛ · student-tools/
python -m app.main

Флаг -m просит Python найти модуль по имени и выполнить его как главный. Имя app.main читается как путь: папка app, файл main.py.

Откуда Python ищет пакеты

Перед первым импортом Python составляет список папок, в которых будет искать модули, — sys.path. Первая запись в этом списке зависит от способа запуска:

  • python -m app.main — в начало списка попадает текущая папка, то есть корень проекта. Из него видна папка app;
  • python app/main.py — в начало списка попадает папка запускаемого файла, то есть app. Из неё видны services и utils, а сам пакет app — нет.

Отсюда правило: команда запуска должна соответствовать тому, с чего начинаются импорты в файлах проекта.

Импорты в main.pyКоманда, которая работаетЧто будет при другой команде
from app.services.calculator import …
путь от корня проекта
python -m app.main
из корня проекта
python app/main.py → ModuleNotFoundError: No module named 'app'
from services.calculator import …
путь внутри пакета
python app/main.pypython -m app.main → ModuleNotFoundError: No module named 'services'
Терминал: с импортами от корня работает python -m app.main, а python app/main.py даёт No module named app; с импортами внутри пакета — наоборот.
Обе комбинации на одном проекте · macOS / Python 3.12

Сверху импорты записаны от корня: запуск через -m печатает результат, запуск файлом не находит пакет app. Снизу те же файлы с импортами внутри пакета: работает запуск файлом, а -m не находит services. Ошибка в обоих случаях одна и та же по смыслу — начало пути импорта не совпало с первой папкой поиска.

Нажмите на изображение, чтобы увеличить.

Какой вариант берём в проекте

Дальше в курсе используется первая строка таблицы: импорты от корня и запуск python -m app.main. Причина в тестах из главы 2.6. Файлы тестов лежат в папке tests рядом с app и запускаются из корня проекта, поэтому импортировать расчёт они могут только как app.services.calculator. Если в main.py написать импорт внутри пакета, в проекте окажутся две разные записи одного и того же пути.

Вариант с запуском файла остаётся рабочим — его используют для маленьких скриптов из одного-двух файлов, где пакета ещё нет.

Интерактивная схема · откуда Python ищет пакеты
student-tools/ ← корень проекта
└ app/ ← папка пакета
   └ main.py

Python начинает поиск с текущей папки — корня проекта. Внутри него виден пакет app, поэтому импорт from app.services.calculator работает.

Проверка: импорт не запускает программу

Убедитесь, что в выводе запуска осталось Средний результат: 4.40. Затем импортируйте точку запуска: команда должна завершиться без отчёта об оценках — за это отвечает условие __name__ из главы 2.4.

ТЕРМИНАЛ · student-tools/
python -c "import app.main"
Успешный запуск пакета: Средний результат 4.40; импорт точки входа проходит без вывода.
Результат реального прогона · macOS / Python 3.12

После python -m app.main появляется отчёт. После import app.main вывода нет: условие __name__ не пустило программу в запуск.

Скачать текст вывода ↓ · Нажмите на изображение, чтобы увеличить.

Собирайте и запускайте из корня

Выполните в своём проекте
Где
Терминал: student-tools/, рядом с go.mod.
Действие
Выполните три команды ниже по очереди.
Результат
go vet и go build молчат, go run . печатает 4.40.
Проверка
Если Go сообщает про go.mod, проверьте текущую папку: в ней должен лежать go.mod.
ТЕРМИНАЛ · student-tools/
go build ./...
go vet ./...
go run .

Разберём по частям. go build компилирует код и сообщает об ошибках; ./... означает «текущая папка и все вложенные», то есть все пакеты модуля сразу. Если команда ничего не напечатала, собрались все пакеты. go vet ищет подозрительные места, которые формально компилируются: например неверное число аргументов в Printf. go run . собирает и тут же запускает пакет из текущей папки.

Проверяются все пакеты, включая те, которые никто не импортирует. Ошибка в internal/utils обнаружится сразу, даже если main.go до него не добирается. В Python аналогичная опечатка дождалась бы первого импорта во время работы программы.

Почему нет ошибки «пакет не найден»

В Python стартовая папка решала, будет ли виден пакет app, — отсюда разница между python -m app.main и python app/main.py. В Go этого класса проблем нет: пути импорта абсолютны и отсчитываются от имени модуля в go.mod, а не от текущего каталога. Поэтому команду можно выполнять из любой папки проекта:

ТЕРМИНАЛ · student-tools/internal/services/
go build ./...
go test .

Go поднимется вверх до go.mod, определит модуль и соберёт указанные пакеты. Единственное требование — находиться внутри модуля. Снаружи вы получите go.mod file not found in current directory or any parent directory, другой ошибки такого рода здесь не возникает.

Собранная программа — это один файл

Ещё одно отличие, которое пригодится в главе 2.6. go build без аргументов кладёт рядом исполняемый файл — student-tools на macOS и Linux, student-tools.exe на Windows. Он не требует ни установленного Go, ни исходников, ни зависимостей: всё нужное вкомпилировано внутрь. Запускается он напрямую:

ТЕРМИНАЛ · student-tools/
go build -o student-tools .
./student-tools

На Windows последняя команда записывается как .\student-tools.exe. Этот файл — результат сборки, а не исходный код, поэтому в Git он не попадает: в следующей главе мы исключим его через .gitignore.

Структура и архитектура

Структура отвечает на вопрос «где лежит файл?». Архитектура — «какая у части ответственность и от кого она зависит?». Папка services сама не делает программу правильно устроенной: важно, чтобы вычисления не зависели от интерфейса вывода.

Имена app, src, core зависят от команды и проекта. Здесь services хранит прикладной расчёт, utils — небольшую функцию оформления. Создавать пустые папки «на будущее» не требуется.

Имена internal, cmd, pkg — распространённые соглашения сообщества, а не требования языка: обязательное значение есть только у internal. Здесь services хранит прикладной расчёт, utils — небольшую функцию оформления. Создавать пустые папки «на будущее» не требуется: пакет без файлов для Go не существует.

Направление зависимостей

Правило этой структуры — направление зависимостей. Точка запуска знает про расчёт и оформление, а они про неё — нет. Расчёт вообще ни от чего не зависит: ему всё равно, кто его вызвал и куда пойдёт результат.

Интерактивная схема · кто от кого зависит
app/main.pymain.go · package mainточка запуска
↓ импортирует
services/calculator.pyinternal/servicesтолько вычисления
utils/formatter.pyinternal/utilsтолько оформление строки

Стрелка одна и вниз: точка запуска импортирует расчёт и оформление. Калькулятор можно вызвать из тестов, из другой программы, из будущего веб-интерфейса — он ничего не знает о том, кто его использует.

Если стрелка появляется в обратную сторону — расчёт импортирует точку запуска, — получается цикл: два модуля ждут друг друга, и Python сообщает об этом ошибкой partially initialized module. Данные передают вниз аргументами, а результат возвращают вверх через return; обратный импорт для этого не нужен.

Если стрелка появляется в обратную сторону — расчёт импортирует точку запуска, — получается цикл. Go в этом месте строже Python: такой проект вообще не собирается, компилятор отвечает import cycle not allowed и перечисляет участников цикла. Данные передают вниз аргументами, а результат возвращают вверх через return; обратный импорт для этого не нужен.

Проверьте себя · Что изменилось после переноса функций в пакеты?

Расположение файлов, пути двух импортов и команда запуска. Формула расчёта и ожидаемый результат остались прежними.

Проверьте себя · Чем go build ./... отличается от go run .?

Первая команда проверяет и собирает все пакеты модуля, включая те, которые сейчас никто не импортирует, и ничего не запускает. Вторая собирает только пакет из текущей папки и сразу его выполняет. Перед сдачей работы выполняют обе: сборку — чтобы не осталось непроверенного кода, запуск — чтобы увидеть результат.

Документация к теме

Go: организация кода · Go: файл go.mod · Go: команда go · Go: пакеты internal

Python: venv · Python: модули и пакеты · VS Code: Python environments · pip freeze