Модули: зачем делить код на файлы, 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 prices | prices.add_vat(100) | По умолчанию. Ясно, откуда берётся каждое имя. |
from prices import add_vat | add_vat(100) | Когда вы часто используете несколько конкретных имён. |
import prices as p / from prices import add_vat as vat | p.add_vat(100) / vat(100) | Длинное имя или конфликт имён. Известные соглашения: import numpy as np. |
Что происходит в момент import?
- Python ищет файл: сначала в папке запущенного скрипта, затем в библиотеках Python.
- Python выполняет весь файл сверху вниз один раз: создаёт функции и константы и выполняет любой код на верхнем уровне.
- Результат сохраняется. Повторный
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 prices | Python ищет 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:
math— квадратный корень из 81 иmath.pi, округлённое до двух знаков.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
2math и 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"))- Что будет напечатано при запуске
python main.pyи почему? - Как предотвратить первую строку?
Показать решение
- Будет напечатано:
helpers loaded
HI!import выполняет весь файл — включая print на верхнем уровне. Даже from helpers import shout выполняет весь helpers.py, а не только функцию.
- В модуле, предназначенном для импорта, не должно быть кода на верхнем уровне — только определения. Удалите
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 *.