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

Один файл становится тремя

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

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

Результат главыmain.py → calculator.py + formatter.pymain.go → internal/services + internal/utils
Ваш терминал
Для работы вместе с преподавателем

Сначала проблема, потом новый файл

На двадцати строках всё видно сразу. Когда добавится работа с пользователями, базой данных и отчётами, файл станет труднее читать и изменять. Поэтому код разделяют по ответственности: формула расчёта лежит отдельно от кода вывода, и при смене оформления её не нужно искать среди строк печати.

Модуль в нашем примере — файл .py. Он может содержать функции, классы и код верхнего уровня. Учебный модуль книги и Python-модуль — разные значения слова: в одной теме у нас несколько небольших файлов.

main.py
соединяет
→calculator.py
считает
+formatter.py
оформляет

Перенесите функции, сохранив поведение

Выполните в своём проекте
Где
Explorer: корень student-tools, рядом с main.py.
Действие
Создайте calculator.py и formatter.py. Перенесите функции. Замените main.py кодом ниже.
Результат
В корне три .py-файла; в main.py нет копий двух вынесенных функций.
Проверка
Сохраните все файлы и выполните python main.py: ожидается 4.40.

В корне student-tools создайте два файла. Перенесите в них соответствующие функции из main.py. Не оставляйте вторые копии: иначе при исправлении одной другая может остаться старой.

student-tools/calculator.pyСкачать файл ↓
def calculate_average(values: list[int]) -> float:
    if not values:
        raise ValueError("Список оценок не должен быть пустым")
    return sum(values) / len(values)
student-tools/formatter.pyСкачать файл ↓
def format_average(value: float) -> str:
    return f"Средний результат: {value:.2f}"
student-tools/main.pyСкачать файл ↓
from rich.console import Console

from calculator import calculate_average
from 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()
ТЕРМИНАЛ · student-tools/
python main.py

Результат снова Средний результат: 4.40. Изменилось расположение кода, а не задача программы. Если результат другой, сначала проверьте перенос функций и импорты.

Терминал: в папке три файла — calculator.py, formatter.py, main.py; запуск python main.py печатает Средний результат: 4.40.
Три файла вместо одного · macOS / Python 3.12

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

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

Как читать import

ДВЕ ФОРМЫ ИМПОРТА
import calculator
result = calculator.calculate_average([5, 4, 5])

from calculator import calculate_average
result = calculate_average([5, 4, 5])

Обе строки загружают один и тот же файл. Разница только в том, какое имя после этого появляется в вашем файле и как вы обращаетесь к функции.

Интерактивная схема · что появилось в вашем файле
появилось имяcalculator
этомодуль целиком
вызовcalculator.calculate_average([5, 4, 5])

Видно, откуда пришла функция: имя модуля остаётся в строке вызова. Удобно, когда из модуля нужно много всего или когда имена в разных модулях совпадают.

Расширение .py в импорте не пишется: там указывают имя модуля, а не файла. Не используйте from calculator import * — такая запись втягивает все имена сразу, и по коду становится не видно, откуда взялась функция.

Что делает строка import

Импорт выполняет файл целиком, сверху вниз. Результат зависит от того, что в этом файле написано:

  • def calculate_average(...): — создаёт функцию, но не выполняет её тело. Тело сработает, когда функцию вызовут.
  • print(...), main(), чтение файла, запрос в сеть на верхнем уровне (без отступа, не внутри функции) — выполнятся сразу, при первом же импорте.

Отсюда следует то, что разобрано в следующем разделе: вызов main(), оставленный в конце файла без условия, запускает программу при каждом импорте этого файла.

Повторный импорт файл не выполняет

Первый import calculator выполняет файл. Все последующие импорты того же модуля — в этом же запуске программы, в любом файле — берут уже загруженный модуль из словаря sys.modules.

Терминал: программа с тремя импортами calculator печатает сообщение о загрузке модуля только один раз.
Три импорта, одна загрузка · macOS / Python 3.12

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

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

Зачем нужна строка if __name__ == "__main__"

Эта строка решает одну задачу: не дать программе запуститься, когда файл импортируют. Ниже разобрано, откуда берётся такая ситуация, что за переменная __name__ и как условие читается целиком.

Шаг 1. Что происходит без условия

Пусть в конце main.py стоит просто вызов main(). При запуске файла программа посчитает средний балл и напечатает результат — этого мы и ждём. Но из предыдущего раздела известно: импорт выполняет файл сверху вниз. Значит, при import main Python дойдёт до строки main() и выполнит её тоже.

Терминал: команда import main_noguard печатает результат расчёта, хотя файл не запускали.
Тот же файл без условия · macOS / Python 3.12

В main_noguard.py вызов main() стоит на верхнем уровне. Файл только импортировали — программа всё равно посчитала и напечатала результат.

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

В главе 2.6 тесты будут импортировать ваши модули. Без условия каждый запуск тестов начинался бы с печати отчёта об оценках.

Шаг 2. Переменная __name__

Загружая файл, Python сам создаёт в нём несколько служебных переменных. Одна из них — __name__, обычная строка с именем модуля. Вы её не объявляете, значение подставляет Python:

  • файл, который вы запустили сами (python main.py или python -m app.main), получает __name__ == "__main__";
  • файл, попавший в программу через import, получает своё имя модуля: "calculator", "app.services.calculator".

Значение "__main__" сообщает одно: этот файл сейчас запущен напрямую. К тому, какой файл в проекте считается главным, оно отношения не имеет.

Проверить значение можно печатью — временно добавьте строку в начало файла:

ВРЕМЕННАЯ ПРОВЕРКА · можно вставить в любой файл
print("Модуль main загружается. __name__ =", __name__)
Терминал: при запуске main.py __name__ = __main__ и печатается результат; при импорте того же файла __name__ = main и результата нет.
Одна и та же пара файлов, два режима · macOS / Python 3.12

Сверху main.py запущен: __name__ равно __main__, поэтому вызвался main() и появился результат. Снизу тот же файл импортирован: __name__ стало main, расчёт не выполнялся. У calculator в обоих случаях имя calculator — он попал в программу через import.

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

Шаг 3. Как читается условие

ПОСЛЕДНИЕ СТРОКИ main.py
if __name__ == "__main__":
    main()

Дословно: «если этот файл сейчас запущен напрямую — вызови main()». При импорте условие ложно, вызова нет, а все функции из файла остаются доступными.

Правило: под условием оставляют только запуск. Строки def и import внутрь if не переносят — при импорте они перестанут выполняться, и файл потеряет свои функции.

Интерактивная схема · один файл, два режима
__name__"__main__"
условиевыполняется
main()вызывается

Файл запущен напрямую: определения созданы и программа стартовала. Это обычный запуск вашей программы.

Названия помогают импортам

Используйте короткие имена маленькими латинскими буквами, слова разделяйте подчёркиванием: user_service.py, calculator.py. Пробелы и дефисы мешают обычному синтаксису импорта. Латиница в именах файлов принята в командной работе: такие имена одинаково читаются в любой системе и в любом терминале.

Сначала проблема, потом новый пакет

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

Пакет в Go — это папка с .go-файлами, у которых в первой строке одно и то же package имя. Отличие от Python: единицей разделения служит папка, а не файл. Два файла в одной папке — один пакет, они видят друг друга без всяких импортов. Чтобы получилась отдельная единица, нужна отдельная папка.

main.go
соединяет
→services/
считает
+utils/
оформляет

Папки мы положим внутрь internal/. Это особое имя: пакеты из internal может импортировать только код того же модуля. Если кто-то подключит ваш модуль как библиотеку, до этих пакетов он не доберётся — Go запретит на компиляции. В учебном проекте это сразу показывает, что внутренняя логика не предназначена для внешнего использования.

Перенесите функции, сохранив поведение

Выполните в своём проекте
Где
Explorer: корень student-tools, рядом с main.go.
Действие
Создайте папки internal/services и internal/utils, в них — calculator.go и formatter.go. Перенесите функции. Замените main.go кодом ниже.
Результат
Три .go-файла в трёх папках; в main.go нет копий двух вынесенных функций.
Проверка
Сохраните все файлы и выполните go run . — ожидается 4.40.

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

student-tools/internal/services/calculator.go
package services

import "errors"

// CalculateAverage возвращает среднюю оценку или ошибку для пустого среза.
func CalculateAverage(values []int) (float64, error) {
	if len(values) == 0 {
		return 0, errors.New("список оценок не должен быть пустым")
	}
	sum := 0
	for _, value := range values {
		sum += value
	}
	return float64(sum) / float64(len(values)), nil
}
student-tools/internal/utils/formatter.go
package utils

import "fmt"

// FormatAverage превращает число в строку с двумя знаками после точки.
func FormatAverage(value float64) string {
	return fmt.Sprintf("Средний результат: %.2f", value)
}
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/
go run .

Результат снова Средний результат: 4.40. Изменилось расположение кода, а не задача программы. Если результат другой, сначала проверьте перенос функций и импорты.

Как читать import

ВИДИМОСТЬ ИМЕНИ В GO
import "example.com/student-tools/internal/services"

result, err := services.CalculateAverage([]int{5, 4, 5})

В импорте указывается путь к папке, а не к файлу: расширение .go и имена файлов там не появляются вообще. После импорта в вашем файле появляется одно имя — имя пакета (последняя строка package services, а не последний сегмент пути, хотя обычно они совпадают). Обращаться к функциям можно только через это имя: сокращённой формы вроде from … import … в Go нет, поэтому по строке вызова всегда видно, откуда пришла функция.

Видимость задаёт первая буква имени

В Python видимость — договорённость: подчёркивание в начале имени просит «не трогать снаружи», но не запрещает. В Go это правило языка. Имя с заглавной буквы видно из других пакетов, имя со строчной — нет. Никаких public и private не существует: решает первая буква.

Интерактивная схема · что будет видно снаружи
внутри пакета servicesдоступна
из пакета mainдоступна
вызовservices.CalculateAverage(values)

Имя начинается с заглавной буквы — функция экспортирована. Это часть публичного интерфейса пакета: её могут вызывать другие пакеты и тесты.

Поэтому при переносе функций мы переименовали их: calculate_average стало CalculateAverage. Заодно поменялся стиль записи — в Go слова в именах не разделяют подчёркиванием, а пишут слитно с заглавных букв.

Импорт не «выполняет» пакет

Строка import в Go — не исполнение файла, а указание компилятору на зависимость. Соединяет код компоновщик при сборке, а не интерпретатор по ходу работы. Отсюда два следствия. Во-первых, порядок импортов ни на что не влияет, и повторный импорт того же пакета невозможен по построению. Во-вторых, неиспользованный импорт — это ошибка компиляции, а не замечание: "fmt" imported and not used. Редактор с расширением Go убирает лишние импорты при сохранении файла.

Где здесь точка запуска

В Python один и тот же файл можно и запустить, и импортировать, поэтому в конце файла пишут условие if __name__ == "__main__". В Go такой двойственности нет, и условие не нужно: роль файла определяется ещё до запуска, его первой строкой.

ТОЧКА ЗАПУСКА В GO
package main

func main() {
	// Точка входа программы. Вызывается средой исполнения.
}

Правило состоит из двух частей, и обе обязательны. Пакет должен называться main — именно это делает его исполняемой программой, а не библиотекой. И в нём должна быть функция main() без аргументов и без возвращаемого значения — её вызывает среда исполнения при старте.

Любой другой пакет — services, utils — запустить нельзя: у него нет точки входа, и go run на него ответит ошибкой. Импортировать пакет main тоже нельзя. Роли разведены на уровне языка: импорт пакета не запускает расчёт.

Что это меняет на практике

Побочных эффектов при импорте в Go почти не бывает. Есть функция init(), которая выполняется при подключении пакета, но пользуются ей редко и осознанно. Тесты из главы 2.6 импортируют ваши пакеты, и расчёт от этого не запускается.

Названия помогают импортам

Имя папки становится частью пути импорта, а имя пакета — частью каждого вызова. Поэтому в Go имена пакетов делают короткими, в одно слово, маленькими латинскими буквами, без подчёркиваний и дефисов: services, utils, storage. Плохой пример — user_service_helpers: в коде это будет выглядеть как user_service_helpers.Get(...).

Имя пакета не повторяют в имени функции. В Go пишут services.CalculateAverage, а не services.ServicesCalculateAverage: имя пакета уже стоит перед точкой. По той же причине пакеты не называют util, common или base — такое имя ничего не сообщает о содержимом, и по нему нельзя понять, что лежит внутри.

Проверьте себя · Функция есть в файле, но пакет main её «не видит». Почему?

Скорее всего имя начинается со строчной буквы: такие имена не экспортируются и снаружи пакета не существуют. Переименуйте calculateAverage в CalculateAverage. Вторая возможная причина — файл лежит в другой папке, чем вы думаете: в Go пакетом является папка, и импорт ведёт именно к ней.

Проверьте себя · Почему калькулятор не должен импортировать main?

main уже использует калькулятор. Обратная связь создаёт цикл и связывает вычисления с запуском. Данные передаются функции аргументами, результат возвращается через return.

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

Go: организация кода · Go: имена пакетов · Go: экспортируемые имена · Effective Go

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