Модульное программирование: разбиение на модули и импорт в Python

Модульное программирование: разбиение на модули и импорт в Python Полезное

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

Ниже разберем, что считать модулем, чем интерфейс отличается от реализации, что такое связность и связанность (cohesion и coupling) и как все это выглядит в Python на примере import и файла __init__.py.

Модуль: интерфейс и реализация

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

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

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

Разделение этих двух сторон — главная идея, которую в 1972 году сформулировал Дэвид Парнас под названием «сокрытие информации» (information hiding): модулю про соседей достаточно знать только их интерфейс, а не внутренности. Именно это делает большие программы управляемыми.

Связность и связанность: cohesion vs coupling

Два близких по звучанию русских термина легко перепутать, поэтому разведем их сразу. Это разные метрики, и цель у них противоположная.

  • Связность (cohesion) — насколько содержимое одного модуля посвящено одной задаче. Хотим высокую: модуль pricing считает только цены, а не заодно шлет письма.
  • Связанность (coupling) — насколько сильно модули зависят друг от друга. Хотим низкую: модуль orders вызывает у pricing пару функций и ничего не знает про его внутренние переменные.

Мнемоника: высокая связность ВНУТРИ модуля, низкая связанность МЕЖДУ модулями. Один модуль собран вокруг своей темы, а связи с соседями узкие и проходят через интерфейс.

Свойство Что измеряет Хорошо, когда Симптом плохого
Связность (cohesion) фокус одного модуля на одной задаче высокая: модуль про одно «модуль-свалка»: и парсинг, и почта, и логи вместе
Связанность (coupling) зависимость модулей друг от друга низкая: связь через узкий интерфейс правка в одном модуле ломает три соседних

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

Зачем разбивать программу на модули

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

Что дает модульность За счет чего
Переиспользование готовый модуль подключается в другом проекте одной строкой импорта
Тестируемость узкий интерфейс легко покрыть автотестами, не поднимая всю программу
Командная работа разные люди правят разные модули, не мешая друг другу
Изоляция ошибок сбой локализован в одном модуле, а не расползается по коду
Нет конфликта имен у каждого модуля свое пространство имен

Обратная сторона тоже есть, и это не «только плюсы»: слишком мелкое дробление плодит лишние связи и запутанные вызовы между модулями, а неудачно проведенные границы дают высокую связанность. Модульность — инструмент, а не самоцель; выигрыш появляется при разумных границах.

Модули и пакеты в Python

В Python модуль — это просто файл .py. Его имя (без расширения) и есть имя модуля. Соберем маленький пример из двух файлов в одной папке.

Файл pricing.py — модуль с высокой связностью, в нем только про цену:

# pricing.py
VAT = 0.20  # ставка НДС

def with_vat(base):
    return round(base * (1 + VAT), 2)

def apply_discount(price, percent):
    return round(price * (1 - percent / 100), 2)

Файл main.py подключает нужные функции через import и пользуется только интерфейсом, не зная про переменную VAT внутри:

# main.py
from pricing import with_vat, apply_discount

full = with_vat(1000)
final = apply_discount(full, 10)
print(full, final)

Запуск python3 main.py печатает результат:

1200.0 1080.0

Здесь with_vat и apply_discount — интерфейс модуля pricing, а формула с VAT — его реализация. Связанность низкая: main обращается к соседу через две функции.

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

# shop/__init__.py
from .pricing import with_vat, apply_discount

Теперь снаружи пакет используется как единое целое:

# use_pkg.py
import shop

print(shop.with_vat(500))
print(shop.apply_discount(500, 25))

Вывод:

600.0
375.0

Оговорка по границе: с Python 3.3 работают и «неявные» пакеты без __init__.py (namespace packages), но обычный файл __init__.py остается стандартным способом объявить пакет и задать его интерфейс.

Частая ошибка при импорте

Импортируя имена по одному, легко опечататься. Пусть в pricing.py функция называется apply_discount, а в импорте написано apply_tax:

# badmain.py
from pricing import with_vat, apply_tax
print(with_vat(1000))

Запуск падает еще до первой строки логики:

ImportError: cannot import name 'apply_tax' from 'pricing'

Python сообщает не «нет модуля», а «нет такого имени В модуле»: сам файл pricing.py найден, но имени apply_tax в нем нет. Исправление — импортировать существующее имя (apply_discount) или добавить функцию в модуль. Отличайте это от ModuleNotFoundError, который означает, что не найден сам файл или пакет.

Выводы

  • Модульное программирование — это принцип разбиения программы на части с одной ответственностью, общающиеся через узкий интерфейс.
  • Интерфейс (что видно снаружи) и реализация (как устроено внутри) — разные вещи; менять реализацию можно, пока стабилен интерфейс.
  • Связность (cohesion) хотим высокую внутри модуля, связанность (coupling) — низкую между модулями; это разные метрики, не путать.
  • В Python модуль — это файл .py, пакет — каталог с __init__.py; подключение идет через import.
  • Модульность окупается при разумных границах; чрезмерное дробление добавляет лишних связей.

Где применяется / связь с практикой

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

Освойте тему на практике

Разобрать эти основы по шагам и на практике помогает курс Python Basic: синтаксис, функции, модули и пакеты. Посмотреть формат занятий и темы вживую можно на открытых уроках Otus — они бесплатные и проходят регулярно.

FAQ

Модуль, пакет и библиотека — это одно и то же?
Нет. Модуль — один файл .py, пакет — каталог модулей с __init__.py, а библиотека — это уже распространяемый набор пакетов и модулей, который ставят и подключают в разные проекты.

Чем модуль отличается от микросервиса?
Модуль работает внутри одного процесса программы и не запускается и не масштабируется отдельно. Микросервис — самостоятельно развернутый сервис, который живет и обновляется независимо и общается по сети.

В каких языках есть модульность?
Принцип поддерживают многие языки: Python, Ruby, Java (пакеты и модули), Rust, Go и другие. Это подход к организации кода, а не особенность одного языка, поэтому навык переносится между ними.

OTUS Журнал
Бесплатные открытые уроки (поп-ап)