Модули: зачем делить код на файлы, import, from и as

Цели урока
  • Объяснить, что такое модуль и зачем проект делят на несколько файлов
  • Создавать собственный модуль и импортировать из него функции и константы
  • Различать import x, from x import y и import x as z и знать, когда использовать каждый
  • Понимать, что код на верхнем уровне модуля выполняется в момент импорта

Что мы изучим?

  • Что такое модуль (Module).
  • Зачем проект делят на несколько файлов.
  • Три формы импорта: import, from ... import, as.
  • Что именно происходит, когда Python импортирует файл.

Зачем это нужно?

Мы уже пользовались модулями: import os, import logging, import socket. Сегодня напишем свои.

Настоящий проект — это сотни и тысячи строк. Один огромный файл:

  • Трудно ориентироваться — где функция, которая отправляет email? Строка 1840?
  • Трудно работать в команде — три разработчика редактируют один файл и мешают друг другу (в Git, в теме Git).
  • Трудно переиспользовать — хорошая функция проверки «застряла» в файле игры.

Деление на файлы — это разделение ответственности на уровне проекта. В темах бэкенда и баз данных сервер будет разделён именно так: main.py (маршруты), db.py (доступ к базе данных), models.py.

Объяснение с основ

Что такое модуль?

Модуль = любой файл .py. Вот и всё. Каждый файл Python, который вы написали, — уже модуль, и из него можно импортировать функции, константы и переменные в другой файл.

  • Имя модуля = имя файла без .py. Файл prices.py → модуль prices.
  • Поэтому имена файлов должны быть допустимыми именами Python: snake_case, без дефисов (my-utils.py не импортируется!) и не начинаться с цифры.
Аналогия: кухня с ящиками

Один огромный файл = один ящик со всеми инструментами: ножи, ложки, щипцы, ножницы. Чтобы что-то найти, перебираешь всё. Модули = отдельные ящики с подписями: «столовые приборы», «инструменты для выпечки», «специи». import = «принеси мне кое-что из этого ящика».

Три формы импорта

ФормаКак потом использоватьКогда
import pricesprices.add_vat(100)По умолчанию. Ясно, откуда берётся каждое имя.
from prices import add_vatadd_vat(100)Когда вы часто используете несколько конкретных имён.
import prices as p / from prices import add_vat as vatp.add_vat(100) / vat(100)Длинное имя или конфликт имён. Известные соглашения: import numpy as np.

Что происходит в момент import?

  1. Python ищет файл: сначала в папке запущенного скрипта, затем в библиотеках Python.
  2. Python выполняет весь файл сверху вниз один раз: создаёт функции и константы и выполняет любой код на верхнем уровне.
  3. Результат сохраняется. Повторный import того же модуля не выполняет его снова.

Пункт 2 очень важен, и его последствия мы увидим в типичной ошибке.

__pycache__

После первого импорта появится папка с именем __pycache__ с файлами .pyc. Это байт-код (Bytecode, вспомните тему о компьютере и операционной системе!) модулей, сохранённый, чтобы следующий импорт был быстрее. Не трогайте её, а в проектах Git игнорируйте (увидим её в .gitignore в теме Git).

Синтаксис

# file: prices.py   (the module)
VAT_RATE = 0.17

def add_vat(amount):
    return amount * (1 + VAT_RATE)
# file: main.py     (uses the module; same folder)
import prices
prices.add_vat(100)

from prices import add_vat, VAT_RATE
add_vat(100)

import prices as p
p.add_vat(100)

from prices import add_vat as with_vat
with_vat(100)

Принятый порядок в начале файла: сначала импорты из стандартной библиотеки (os, json), пустая строка, а затем импорты из ваших собственных модулей.

Первый пример кода

Создайте папку week-2/shop/ и в ней два файла.

week-2/shop/prices.py:

VAT_RATE = 0.17

def add_vat(amount):
    return round(amount * (1 + VAT_RATE), 2)

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

week-2/shop/main.py:

import prices

base_price = 200
discounted = prices.apply_discount(base_price, 10)
final_price = prices.add_vat(discounted)

print("Base:", base_price)
print("After 10% discount:", discounted)
print("With VAT (" + str(prices.VAT_RATE) + "):", final_price)

Запуск (из week-2/shop): python main.py

Вывод:

Base: 200
After 10% discount: 180.0
With VAT (0.17): 210.6

Разбор кода построчно

СтрокаОбъяснение
import pricesPython ищет prices.py в той же папке, что и main.py, выполняет его и создаёт имя prices, которое представляет модуль.
prices.apply_discount(...)К функции обращаются через имя модуля, через точку. Точно как os.getcwd().
prices.VAT_RATEК константам обращаются так же.
main.py не определяет ни одной функции ценВся логика цен — в одном месте. Если НДС изменится, вы меняете только prices.py.

Ещё один пример

Три формы импорта и второй модуль. Добавьте week-2/shop/validation.py:

MIN_NAME_LENGTH = 2

def is_valid_price(price):
    return isinstance(price, (int, float)) and price >= 0

def is_valid_name(name):
    return len(name.strip()) >= MIN_NAME_LENGTH

И обновите main.py:

import prices as p
from validation import is_valid_price, is_valid_name

products = [("Laptop", 3500), ("X", 20), ("Mouse", -5), ("Desk", 900)]

for name, price in products:
    if not is_valid_name(name):
        print("Skipping invalid name:", repr(name))
    elif not is_valid_price(price):
        print("Skipping invalid price for", name, ":", price)
    else:
        print(name, "->", p.add_vat(price), "NIS incl. VAT")

Вывод:

Laptop -> 4095.0 NIS incl. VAT
Skipping invalid name: 'X'
Skipping invalid price for Mouse : -5
Desk -> 1053.0 NIS incl. VAT

Разбор:

  • import prices as p — короткий псевдоним. Обращаются через p.add_vat.
  • from validation import is_valid_price, is_valid_name — импортирует только две функции и использует их напрямую, без префикса.
  • repr(name) — показывает значение в кавычках ('X'), как %r в логах.
  • Структура проекта: prices.py = логика цен. validation.py = правила допустимости. main.py = соединяет и запускает. У каждого файла одна ответственность.

Типичная ошибка

Ошибка 1: «тестовый» код на верхнем уровне модуля. Допустим, внизу prices.py вы добавили проверку:

print("Testing prices module:", add_vat(100))

Теперь каждый import prices из любого файла будет печатать эту строку, потому что импорт выполняет файл. В следующем уроке изучим решение: if __name__ == "__main__":.

Ошибка 2: имя файла, которое совпадает со стандартным модулем.

Вы создали файл с именем random.py (или logging.py, json.py), а в другом файле написали import random:

AttributeError: module 'random' has no attribute 'randint'

Python находит ваш random.py первым, в той же папке, и импортирует его вместо настоящего модуля. Никогда не называйте файлы именами стандартных модулей.

Ошибка 3: ModuleNotFoundError.

ModuleNotFoundError: No module named 'prices'

Частые причины: файл не в той же папке, что запущенный скрипт, опечатка в имени или имя файла с дефисом (my-prices.py).

Ошибка 4: from module import *. Импортирует все имена из модуля. Непонятно, откуда взялось каждое имя, и имена могут молча перезаписать друг друга. Импортируйте явно то, что нужно.

Практика

Упражнения опираются только на то, что пройдено в этом уроке и в предыдущих. Сначала попробуйте сами, и только потом открывайте решение.

Упражнение 1: Первый модуль

Создайте файл prices.py с константой VAT_RATE = 0.17 и двумя функциями: add_vat(amount) и apply_discount(amount, percent) (обе округляют до двух знаков). В файле main.py импортируйте модуль, вычислите цену 200 после скидки 10%, а затем добавьте к ней НДС.

Ожидаемый вывод (запуск main.py):

After discount: 180.0
With VAT: 210.6
Показать решение

prices.py

VAT_RATE = 0.17


def add_vat(amount):
    return round(amount * (1 + VAT_RATE), 2)


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

main.py

import prices

discounted = prices.apply_discount(200, 10)
final_price = prices.add_vat(discounted)
print("After discount:", discounted)
print("With VAT:", final_price)

import prices загружает файл prices.py из той же папки. К его содержимому обращаются через точку: prices.add_vat(...).

Упражнение 2: Три формы импорта

Напишите одну и ту же строку — вычисление НДС от 100 — в трёх формах импорта: import, from ... import и import ... as. Когда стоит использовать каждую?

Показать решение
import prices
print(prices.add_vat(100))

from prices import add_vat
print(add_vat(100))

import prices as p
print(p.add_vat(100))
  • import prices — всегда ясно, откуда берётся каждое имя (prices.add_vat). Подходит, когда вы используете из модуля много всего.
  • from prices import add_vat — пишете меньше. Подходит для нескольких конкретных имён, которые вы часто используете.
  • import prices as p — короткое имя для модуля с длинным именем.

Упражнение 3: Стандартная библиотека

Не создавая собственного модуля, воспользуйтесь встроенными модулями Python:

  1. math — квадратный корень из 81 и math.pi, округлённое до двух знаков.
  2. random — с random.seed(1) выберите случайное число от 1 до 6.

Ожидаемый вывод:

9.0
3.14
2
Показать решение
import math
import random

print(math.sqrt(81))
print(round(math.pi, 2))

random.seed(1)
print(random.randint(1, 6))
9.0
3.14
2

math и random входят в стандартную библиотеку (Standard Library) — они поставляются вместе с Python, и импортируют их точно так же, как модуль, написанный нами. random.seed(1) фиксирует случайный выбор, чтобы результат был одинаковым при каждом запуске.

Упражнение 4: Код, который выполняется при импорте

В файле helpers.py:

def shout(text):
    return text.upper() + "!"

print("helpers loaded")

В файле main.py:

from helpers import shout
print(shout("hi"))
  1. Что будет напечатано при запуске python main.py и почему?
  2. Как предотвратить первую строку?
Показать решение
  1. Будет напечатано:
helpers loaded
HI!

import выполняет весь файл — включая print на верхнем уровне. Даже from helpers import shout выполняет весь helpers.py, а не только функцию.

  1. В модуле, предназначенном для импорта, не должно быть кода на верхнем уровне — только определения. Удалите print. (В следующем уроке изучим if __name__ == "__main__":, который позволяет оставить тестовый код, выполняющийся только при прямом запуске файла.)

Упражнение 5: Модуль температуры

Создайте модуль temperature.py с c_to_f(celsius) и с describe(celsius), которая возвращает "hot" от 30 градусов, "cold" ниже 15 и "pleasant" в промежутке. В main.py напечатайте для каждого города температуру в обеих единицах и описание.

cities = ["Eilat", "Haifa", "Jerusalem"]
temperatures = [35, 22, 9]

Ожидаемый вывод:

Eilat: 35 C / 95.0 F - hot
Haifa: 22 C / 71.6 F - pleasant
Jerusalem: 9 C / 48.2 F - cold
Показать решение

temperature.py

HOT_FROM = 30
COLD_BELOW = 15


def c_to_f(celsius):
    return round(celsius * 9 / 5 + 32, 1)


def describe(celsius):
    if celsius >= HOT_FROM:
        return "hot"
    if celsius < COLD_BELOW:
        return "cold"
    return "pleasant"

main.py

from temperature import c_to_f, describe

cities = ["Eilat", "Haifa", "Jerusalem"]
temperatures = [35, 22, 9]

for i in range(len(cities)):
    celsius = temperatures[i]
    print(cities[i] + ":", celsius, "C /", c_to_f(celsius), "F -", describe(celsius))

Логика температуры — в модуле, а main.py только использует её и печатает.

Итоги

  • Модуль = файл .py. Имя модуля = имя файла без .py (snake_case, без дефисов).
  • import m → m.func(). from m import func → func(). import m as x → x.func().
  • Python ищет модули сначала в папке скрипта. Не называйте файлы random.py, json.py и т. п.
  • Импорт выполняет модуль один раз. Код на верхнем уровне выполняется даже при импорте.
  • Деление на файлы = разделение ответственности на уровне проекта. Никаких import *.

Официальная документация