Организация Python проекта: main.py и модульная архитектура
Основной подход: модульная структура с main.py
Как создать типовую структуру Python проекта с точкой входа main.py для удобства разработки и тестирования?
Наиболее распространённая и эффективная организация кода предполагает размещение исполняемого файла main.py в корне проекта, а всю логику – в отдельном каталоге src (или app). Такой подход разделяет ответственность: main.py служит только точкой входа, а остальные модули содержат бизнес-логику, утилиты и тесты.
Пример базовой структуры
project/
├── main.py
├── src/
│ ├── __init__.py
│ ├── core.py
│ └── utils.py
├── tests/
│ ├── __init__.py
│ └── test_core.py
├── requirements.txt
├── README.md
└── .gitignore
Python сделать модуль (создание модуля в python)
Шаги создания:
- Создайте корневую директорию project/.
- Внутри создайте main.py – он будет запускать приложение.
- Создайте папку src/ с файлом __init__.py (пустым или с инициализацией пакета).
- В src/ разместите модули: core.py (основная логика) и utils.py (вспомогательные функции).
- Папка tests/ для тестов с собственным __init__.py.
- Добавьте requirements.txt и README.md для описания проекта.
Пример кода main.py
import sys
from src.core import run_app
from src.utils import setup_logging
def main():
setup_logging()
config = {"mode": "production"}
result = run_app(config)
print(f"Приложение завершено с результатом: {result}")
if __name__ == "__main__":
main()
Python project main py (структура проекта python с main.py)
Пояснения
- Импорты из src работают благодаря наличию __init__.py и нахождению корня проекта в sys.path (по умолчанию при запуске main.py корень добавляется автоматически).
- Блок if __name__ == "__main__": гарантирует, что код выполняется только при прямом запуске файла, а не при импорте.
- Функция main() инкапсулирует логику запуска, что упрощает тестирование.
Типичная ошибка: ImportError при попытке импортировать модуль из src. Причина: запуск main.py из другой директории или отсутствие файла __init__.py. Решение: запускать скрипт из корня проекта (python main.py) и убедиться, что все папки-пакеты содержат __init__.py.
Альтернативные варианты организации
Вариант 1. Всё в одном файле main.py (для мелких скриптов)
Как сделать простой однофайловый скрипт без разделения на модули?
Подходит для скриптов, состоящих из 50–100 строк. Логика, функции и точка входа находятся в одном файле.
Пример
#!/usr/bin/env python3
# main.py (единственный файл)
import sys
import requests
def fetch_data(url):
response = requests.get(url)
response.raise_for_status()
return response.json()
def process_data(data):
return [item["name"] for item in data if "name" in item]
def main():
url = sys.argv[1] if len(sys.argv) > 1 else "https://api.example.com/data"
data = fetch_data(url)
names = process_data(data)
for name in names:
print(name)
if __name__ == "__main__":
main()
Проблемы: при увеличении размера кода (более 200 строк) такой файл становится трудно читать и поддерживать. Невозможно переиспользовать части кода в других проектах без копирования. Тестирование отдельных функций затруднено, так как все зависит от глобального состояния.
Вариант 2. main.py + модули в той же папке (для небольших проектов)
Как организовать код, если проект вырос до нескольких файлов, но пока не требует глубокой иерархии?
Файлы раскладываются в корне проекта, main.py вызывает функции из соседних .py-файлов.
Структура
project/
├── main.py
├── helpers.py
├── config.py
├── requirements.txt
main.py:
from config import load_config
from helpers import greet
def main():
cfg = load_config()
msg = greet(cfg.get("user", "Гость"))
print(msg)
if __name__ == "__main__":
main()
Недостаток: по мере роста количество файлов в корне увеличивается, что усложняет навигацию. Отсутствие чёткой структуры приводит к путанице между модулями разного назначения. Также могут возникать конфликты имён с библиотеками.
Вариант 3. Использование пакетов с __init__.py (стандартный для крупных проектов)
Как структурировать большой проект с подмодулями и избежать проблем с импортами?
Включает в себя иерархию пакетов: src/core/, src/utils/ и т.д. Каждый подкаталог является пакетом благодаря __init__.py.
Пример структуры
project/
├── main.py
├── src/
│ ├── __init__.py
│ ├── core/
│ │ ├── __init__.py
│ │ ├── engine.py
│ │ └── models.py
│ └── utils/
│ ├── __init__.py
│ ├── io_helpers.py
│ └── logging_setup.py
├── tests/
│ ├── __init__.py
│ └── test_engine.py
├── config/
│ ├── default.yaml
│ └── production.yaml
main.py:
from src.core.engine import start_engine
from src.utils.logging_setup import configure_logging
def main():
configure_logging("config/default.yaml")
start_engine()
if __name__ == "__main__":
main()
Распространённая ошибка: относительные импорты внутри пакетов (например, from ..core import something) могут вызвать ImportError при запуске main.py, если не использовать -m. Решение: всегда запускать main.py как python -m src.core.engine или с абсолютными импортами, начиная с корня проекта.
Вариант 4. Установка проекта как пакета с entry_points (для CLI-приложений)
Как сделать проект устанавливаемым и вызываемым из командной строки через одну команду?
Добавляется setup.py (или pyproject.toml) с указанием точек входа. Файл main.py может отсутствовать, его заменяет функция, указанная в console_scripts.
Пример setup.py
from setuptools import setup, find_packages
setup(
name="myapp",
version="0.1.0",
packages=find_packages(),
entry_points={
"console_scripts": [
"myapp = src.cli:main",
],
},
install_requires=[
"requests>=2.25",
],
)
В src/cli.py определяется функция main(), которая обрабатывает аргументы командной строки. После установки (pip install .) можно запускать приложение командой myapp из любого места.
Проблема: при разработке необходимо переустанавливать пакет после каждого изменения. Решение – использовать pip install -e . (режим редактирования).
Расширенные примеры и пояснения
Пример 1. Полноценная структура с обработкой аргументов, логированием и тестами
Рассмотрим проект простого веб-скрапера, организованный по модульному принципу с пакетами.
scraper/
├── main.py
├── src/
│ ├── __init__.py
│ ├── fetcher.py
│ ├── parser.py
│ └── output.py
├── tests/
│ ├── __init__.py
│ ├── test_fetcher.py
│ └── test_parser.py
├── config/
│ └── settings.ini
├── requirements.txt
└── README.md
main.py – точка входа:
#!/usr/bin/env python3
"""Главный модуль для запуска скрапера."""
import argparse
import sys
from src.fetcher import fetch_page
from src.parser import extract_links
from src.output import save_to_file
def parse_args():
parser = argparse.ArgumentParser(description="Веб-скрапер для извлечения ссылок")
parser.add_argument("url", help="URL страницы для анализа")
parser.add_argument("-o", "--output", default="links.txt", help="Файл для сохранения ссылок")
parser.add_argument("--timeout", type=int, default=10, help="Таймаут запроса в секундах")
return parser.parse_args()
def main():
args = parse_args()
print(f"Загрузка {args.url}...")
html = fetch_page(args.url, timeout=args.timeout)
links = extract_links(html, base_url=args.url)
save_to_file(links, args.output)
print(f"Найдено {len(links)} ссылок. Результат сохранён в {args.output}.")
if __name__ == "__main__":
main()
src/fetcher.py:
import requests
from requests.exceptions import RequestException
def fetch_page(url, timeout=10):
"""Получает HTML-содержимое страницы."""
try:
response = requests.get(url, timeout=timeout)
response.raise_for_status()
return response.text
except RequestException as e:
print(f"Ошибка при запросе {url}: {e}", file=sys.stderr)
raise
src/parser.py:
from urllib.parse import urljoin
from bs4 import BeautifulSoup
def extract_links(html, base_url):
"""Извлекает все ссылки (теги <a>) из HTML."""
soup = BeautifulSoup(html, "html.parser")
links = []
for a in soup.find_all("a", href=True):
href = a["href"]
absolute_url = urljoin(base_url, href)
links.append(absolute_url)
return links
src/output.py:
def save_to_file(links, filename):
"""Сохраняет список ссылок в текстовый файл."""
with open(filename, "w", encoding="utf-8") as f:
for link in links:
f.write(link + "\n")
tests/test_parser.py:
import pytest
from src.parser import extract_links
HTML_SAMPLE = '''
<html>
<body>
<a href="/page1">Page 1</a>
<a href="https://example.com/page2">Page 2</a>
<a href="#section">Section</a>
</body>
</html>
'''
def test_extract_links():
base_url = "https://example.com"
links = extract_links(HTML_SAMPLE, base_url)
assert len(links) == 3
assert links[0] == "https://example.com/page1"
assert links[1] == "https://example.com/page2"
assert links[2] == "https://example.com/#section" # Якорь присоединяется к base_url
Результат выполнения теста (при запуске pytest tests/):
============================= test session starts ============================== collected 1 item tests/test_parser.py . [100%] ============================== 1 passed in 0.12s ===============================
Пример 2. Использование относительных импортов внутри пакета
При разработке глубоко вложенных пакетов иногда удобно обращаться к соседним модулям через относительные импорты. Однако такая практика требует запуска через -m. Рассмотрим структуру:
project/
├── main.py
├── pkg/
│ ├── __init__.py
│ ├── subpkg/
│ │ ├── __init__.py
│ │ ├── a.py
│ │ └── b.py
pkg/subpkg/a.py:
def func_a():
return "A"
pkg/subpkg/b.py (использует относительный импорт):
from .a import func_a
def func_b():
return func_a() + "B"
main.py:
from pkg.subpkg.b import func_b
print(func_b())
Запуск:
python -m main
Результат:
AB
Если запустить python main.py (без -m), то возникнет ошибка ImportError: attempted relative import with no known parent package. Поэтому при использовании относительных импортов необходимо всегда запускать скрипт как модуль: python -m project.main (если проект установлен как пакет) или python -m main при нахождении в корне.
Пример 3. Динамическая загрузка конфигурации из YAML
В больших проектах полезно выносить параметры в конфигурационные файлы. Покажем, как загрузить настройки в main.py.
# config/default.yaml
debug: false
port: 8080
database:
host: localhost
port: 5432
name: mydb
# src/config_loader.py
import yaml
from pathlib import Path
def load_config(path="config/default.yaml"):
config_path = Path(__file__).parent.parent / path
with open(config_path, "r", encoding="utf-8") as f:
config = yaml.safe_load(f)
return config
# main.py
from src.config_loader import load_config
def main():
config = load_config()
print(f"Порт: {config['port']}")
print(f"База данных: {config['database']['name']}")
if __name__ == "__main__":
main()
Результат выполнения:
Порт: 8080 База данных: mydb
Примечание: для работы с YAML требуется библиотека PyYAML (pip install PyYAML). В данном примере путь к конфигу формируется относительно расположения скрипта, что позволяет не зависеть от рабочей директории.