$ sudo teach IT
МОДУЛЬ 9 · УРОК 2

Создание модуля и импорт классов

Мы разбили код на файлы — отлично. Но теперь один файл должен «взять» класс из другого. Именно для этого существует механизм import. В этом уроке разберём все способы импорта, подводные камни и магическую конструкцию if __name__ == "__main__".

⏱ ~40 минут 🎓 Для новичков 🐍 Python 3

📄 Что такое модуль — проще некуда

Прежде чем говорить про импорт, разберёмся с главным термином: что такое «модуль»?

Модуль — это просто файл с расширением .py.
Любой файл Python — уже модуль. Всё!

Серьёзно — это всё. Нет ничего специального, что нужно добавить или сделать. Если у вас есть файл geometry.py — у вас уже есть модуль geometry. Если есть user.py — есть модуль user.

Имя модуля = имя файла без расширения .py. Всегда маленькими буквами, через подчёркивание если несколько слов: my_module.py, email_sender.py.

📦 Пакет — это папка с файлами .py. Про пакеты подробнее поговорим в следующем уроке. Сейчас сосредоточимся на модулях — одиночных файлах.

✏️ Создаём модуль geometry.py

Давайте создадим конкретный модуль, с которым будем работать в этом уроке. Это файл geometry.py — в нём два класса для работы с геометрическими фигурами: Circle (круг) и Rectangle (прямоугольник).

Файл geometry.py:

# geometry.py
# Модуль с геометрическими фигурами

import math  # встроенный модуль Python, нужен для math.pi

class Circle:
    """Класс для работы с кругом."""

    def __init__(self, radius):
        self.radius = radius  # радиус круга

    def area(self):
        """Площадь круга: π × r²"""
        return math.pi * self.radius ** 2

    def perimeter(self):
        """Длина окружности: 2 × π × r"""
        return 2 * math.pi * self.radius

    def __str__(self):
        return f"Круг с радиусом {self.radius}"


class Rectangle:
    """Класс для работы с прямоугольником."""

    def __init__(self, width, height):
        self.width = width    # ширина
        self.height = height  # высота

    def area(self):
        """Площадь прямоугольника: ширина × высота"""
        return self.width * self.height

    def perimeter(self):
        """Периметр прямоугольника: 2 × (ширина + высота)"""
        return 2 * (self.width + self.height)

    def __str__(self):
        return f"Прямоугольник {self.width}×{self.height}"

Этот файл мы сохраняем как geometry.py. Теперь у нас есть модуль geometry с двумя классами. Посмотрим, как импортировать из него нужные нам вещи.

📁 Важно: файл main.py (откуда мы будем делать импорт) должен лежать в той же папке, что и geometry.py. Пока просто держите их рядом.

📦 Способ 1: import module — импорт всего модуля

Первый и самый простой способ — импортировать весь модуль целиком. После этого вы обращаетесь к его содержимому через точку: module.ClassName.

# main.py — способ 1: import module

import geometry  # импортируем весь модуль geometry

# Чтобы использовать классы, пишем: geometry.ИмяКласса
circle = geometry.Circle(5)       # круг с радиусом 5
rect = geometry.Rectangle(4, 6)   # прямоугольник 4×6

print(circle)           # Круг с радиусом 5
print(circle.area())    # 78.53981633974483

print(rect)             # Прямоугольник 4×6
print(rect.perimeter()) # 20

✅

Плюсы

Сразу видно, откуда пришёл класс: geometry.Circle — явно говорит «класс Circle из модуля geometry». Никакой путаницы, если одинаковые имена есть в разных модулях.

⚠️

Минусы

Чуть длиннее писать: каждый раз geometry.Circle вместо просто Circle. Когда имя модуля длинное — это может раздражать.

💡 Когда использовать: когда важна «подпись» откуда пришёл класс. Особенно полезно в больших файлах, где много разных импортов — сразу ясно, что к чему относится.

🎯 Способ 2: from module import Class — импорт конкретного класса

Второй способ — импортировать конкретный класс напрямую. После этого класс доступен по имени без приставки модуля.

# main.py — способ 2: from module import Class

from geometry import Circle     # берём только Circle
from geometry import Rectangle  # берём только Rectangle

# Теперь можно писать просто Circle и Rectangle — без "geometry."
circle = Circle(5)
rect = Rectangle(4, 6)

print(circle.area())     # 78.53981633974483
print(rect.perimeter())  # 20

Можно импортировать несколько имён сразу через запятую:

# Импорт нескольких классов одной строкой
from geometry import Circle, Rectangle

circle = Circle(3)
rect = Rectangle(10, 5)
print(circle, rect)

✅

Плюсы

Короче писать: просто Circle(5) вместо geometry.Circle(5). Видно, что именно используется из модуля — перечислено в строке import.

⚠️

Минусы

Если в двух модулях есть класс с одинаковым именем — будет конфликт. Например, если и geometry, и svg содержат Circle — второй импорт перезапишет первый.

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

🏷️ Способ 3: from module import Class as MyClass — переименование (alias)

Иногда нужно переименовать класс при импорте. Это называется псевдоним или alias. Ключевое слово as.

Зачем это нужно? Две типичные ситуации:

🔴 Ситуация 1: конфликт имён

В модуле geometry есть класс Circle, и в модуле svg тоже есть Circle. Без алиаса второй импорт затрёт первый. С алиасом — оба живут мирно.

🔵 Ситуация 2: длинное имя

Класс называется VeryLongAndDescriptiveCircleClass. Писать это каждый раз утомительно. Алиас позволяет дать ему удобное короткое имя внутри вашего файла.

# main.py — способ 3: import с алиасом

# Пример 1: решаем конфликт имён
from geometry import Circle as GeomCircle
from svg import Circle as SvgCircle      # тоже называется Circle!

geom = GeomCircle(5)    # геометрический круг
icon = SvgCircle(10)    # SVG-иконка круга
# теперь нет путаницы — оба класса доступны под разными именами

# Пример 2: сокращаем длинное имя
from geometry import Rectangle as Rect

r = Rect(4, 6)   # гораздо удобнее, чем Rectangle(4, 6)
print(r.area())  # 24

💡 Важно: алиас — это локальное переименование только внутри вашего файла. Оригинальный класс в модуле geometry по-прежнему называется Rectangle. Вы просто создаёте новое имя-ярлык для него у себя.

📌 Способ 4: import module as alias — псевдоним для модуля

Алиас можно дать не только классу, но и целому модулю. Именно так вы уже встречали в мире Python самый известный псевдоним:

import numpy as np        # стандартная практика в мире Python
import pandas as pd       # тоже классика
import matplotlib.pyplot as plt  # и это тоже

# Теперь вместо numpy.array пишем np.array
arr = np.array([1, 2, 3])
print(arr)

Наш модуль geometry тоже можно импортировать с псевдонимом:

# main.py — алиас для всего модуля

import geometry as geo   # теперь обращаемся через "geo"

circle = geo.Circle(5)
rect = geo.Rectangle(4, 6)

print(circle.area())    # 78.53...
print(rect.perimeter()) # 20

🌍 Почему import numpy as np — это стандарт?

Библиотеку NumPy используют миллионы программистов. Если все пишут np, то код становится понятным сразу — любой Python-разработчик в мире видит np.array и знает: это NumPy. Это соглашение, которое облегчает чтение чужого кода.

🚫 Способ 5: from module import * — почему так делать НЕ стоит

Звёздочка * означает «импортировать всё, что есть в модуле». Это выглядит удобно — написал одну строчку и получил всё сразу!

# Выглядит удобно...
from geometry import *

circle = Circle(5)       # работает!
rect = Rectangle(4, 6)   # работает!

Но давайте посмотрим, что происходит на практике — представьте, что у вас три модуля:

# shapes.py — содержит: Circle, Rectangle, Color
# colors.py — содержит: Color, Palette, Theme
# utils.py  — содержит: Circle, format_number, Color

# Импортируем всё из всех трёх модулей
from shapes import *   # добавляет: Circle, Rectangle, Color
from colors import *   # добавляет: Color (ПЕРЕЗАПИСЫВАЕТ предыдущий!), Palette, Theme
from utils import *    # добавляет: Circle (ПЕРЕЗАПИСЫВАЕТ!), format_number, Color (СНОВА!)

# Теперь попробуй угадать: какой Circle здесь?
c = Circle(5)
# Это Circle из utils.py — он импортировался последним и перезаписал остальных.
# Но это НЕ очевидно при чтении кода!

💥 Засорение пространства имён — что это такое?

Пространство имён — это как список всех «слов» (имён переменных, классов, функций), которые Python знает в вашем файле. Когда вы пишете from geometry import *, все имена из модуля мгновенно оказываются в вашем пространстве имён.

Если в разных модулях есть имена с одинаковым названием — они молча перезапишут друг друга. Никакой ошибки не будет. Просто последний импорт выигрывает. И потом вы часами ищете баг: «почему мой Circle ведёт себя не так?»

❌

Проблемы import *

  • Невозможно понять, откуда пришло имя
  • Скрытые конфликты имён
  • IDE не может подсказать автодополнение
  • Тяжело читать и поддерживать

✅

Правило хорошего тона

Всегда явно указывай, что импортируешь: from geometry import Circle, Rectangle. Это занимает пару символов больше, но делает код прозрачным и безопасным.

🔥 Единственное исключение: иногда import * встречается в __init__.py пакетов для «реэкспорта» — но это отдельная тема. Для обычного кода: никогда не используй import *.

⚙️ Что физически происходит при импорте: sys.modules

Когда вы пишете import geometry, Python делает несколько шагов под капотом. Давайте разберём их — это поможет понять одно важное поведение.

1️⃣

Поиск файла

Python ищет файл geometry.py в нескольких местах: сначала в текущей папке, потом в стандартных папках библиотек. Если не нашёл — ошибка ModuleNotFoundError.

2️⃣

Выполнение кода модуля

Python выполняет весь код файла geometry.py сверху вниз. Именно поэтому классы и функции становятся доступными — Python прочитал определения и «выучил» их.

3️⃣

Кеширование в sys.modules

Загруженный модуль сохраняется в специальный словарь sys.modules. Если тот же модуль импортируется ещё раз — Python просто берёт его из кеша. Код модуля НЕ выполняется повторно.

Посмотрим на это поведение в коде:

import sys

# Первый импорт — Python найдёт файл и выполнит его код
import geometry
print("geometry" in sys.modules)  # True — модуль в кеше

# Второй импорт — Python просто возьмёт из кеша, ничего не перевыполняет
import geometry  # это строка НИЧЕГО нового не делает!

# Можно посмотреть на все загруженные модули
# print(sys.modules)  # большой словарь — не советую выводить на экран 😄

🎯 Практическое значение кеша

Если в нескольких файлах вашего проекта написано import geometry — это нормально и эффективно. Python загрузит модуль один раз при первом импорте, а все последующие просто получат кешированную версию. Скорость работы не страдает, память расходуется один раз.

🔑 Магия if __name__ == "__main__" — разбираемся раз и навсегда

Вы наверняка видели эту конструкцию в чужом коде и думали: «Что это такое и зачем нужно?» Сейчас разберём — она не такая страшная, как выглядит.

Что такое __name__?

У каждого Python-файла есть специальная встроенная переменная __name__ (с двумя подчёркиваниями с каждой стороны). Она автоматически устанавливается Python и содержит имя модуля.

Но! Есть одно особое правило: если файл запускается напрямую (python geometry.py), то __name__ равно строке "__main__". Если файл импортируется другим файлом — __name__ равно имени файла ("geometry").

Покажем это на примере. Обновим наш geometry.py:

# geometry.py — с конструкцией if __name__ == "__main__"

import math

class Circle:
    def __init__(self, radius):
        self.radius = radius

    def area(self):
        return math.pi * self.radius ** 2

    def perimeter(self):
        return 2 * math.pi * self.radius

    def __str__(self):
        return f"Круг с радиусом {self.radius}"


class Rectangle:
    def __init__(self, width, height):
        self.width = width
        self.height = height

    def area(self):
        return self.width * self.height

    def perimeter(self):
        return 2 * (self.width + self.height)

    def __str__(self):
        return f"Прямоугольник {self.width}×{self.height}"


# Этот блок выполняется ТОЛЬКО при прямом запуске файла:
# python geometry.py
# При импорте (import geometry) этот блок НЕ выполняется!

if __name__ == "__main__":
    print("=== Тестируем наш модуль ===")
    c = Circle(5)
    print(c)
    print(f"Площадь: {c.area():.2f}")
    print(f"Длина окружности: {c.perimeter():.2f}")

    r = Rectangle(4, 6)
    print(r)
    print(f"Площадь: {r.area()}")
    print(f"Периметр: {r.perimeter()}")

Теперь разберём, что происходит в двух сценариях:

🖥️ Сценарий А: прямой запуск

python geometry.py

Python устанавливает __name__ = "__main__". Условие __name__ == "__main__" истинно → блок выполняется → тесты запускаются. Вы видите вывод на экране.

📦 Сценарий Б: импорт модуля

import geometry

Python устанавливает __name__ = "geometry". Условие __name__ == "__main__" ложно → блок НЕ выполняется → никакого лишнего вывода нет. Только классы загружаются.

🤔 Зачем это нужно на практике?

Представьте: вы написали модуль geometry.py и хотите его протестировать. Без if __name__ == "__main__" тестовый код запускается ВСЕГДА — и при прямом запуске, и при любом импорте. Это значит, каждый раз когда кто-то делает import geometry, на экран вылетает куча вывода. Это очень раздражает!

С конструкцией if __name__ == "__main__" тесты запускаются только когда вы явно запускаете файл. При импорте — тихо, только классы.

Типичная структура файла с этой конструкцией:

# любой_модуль.py

# 1. Импорты
import math

# 2. Классы и функции
class MyClass:
    pass

def my_function():
    pass

# 3. Точка входа — выполняется только при прямом запуске
if __name__ == "__main__":
    # тесты, демонстрация работы, отладка
    obj = MyClass()
    my_function()
    print("Всё работает!")

⭐ Хорошая привычка: добавляй if __name__ == "__main__": в каждый файл с классами. Это признак профессионально написанного кода. IDE и линтеры часто даже подсказывают это автоматически.

📊 Все способы импорта: сводная таблица

Теперь у нас есть все способы. Запомнить их поможет таблица:

import geometry

Весь модуль. Доступ через точку: geometry.Circle(). Источник виден сразу.

from geometry import Circle

Конкретный класс напрямую. Писать просто Circle(). Самый частый способ.

from geometry import Circle as C

Класс с псевдонимом. Используем C(). Помогает при конфликтах имён или длинных названиях.

import geometry as geo

Весь модуль с псевдонимом. Доступ: geo.Circle(). Стандарт для numpy, pandas.

from geometry import *

❌ Всё подряд. Засоряет пространство имён. Скрытые конфликты. Не использовать.

✅ Итоги урока

  • Модуль — это просто файл .py. Любой файл Python уже является модулем — ничего дополнительно делать не нужно.
  • import module — импортирует весь модуль; доступ через точку: module.ClassName. Источник всегда виден.
  • from module import Class — импортирует конкретный класс напрямую. Самый распространённый способ в реальном коде.
  • as — псевдоним для модуля или класса. Решает конфликты имён и сокращает длинные названия. Стандарт для numpy (as np), pandas (as pd).
  • from module import * — не использовать! Засоряет пространство имён, создаёт скрытые конфликты, делает код нечитаемым.
  • При импорте Python выполняет код модуля один раз и кеширует его в sys.modules. Повторный import работает мгновенно из кеша.
  • if __name__ == "__main__": — блок, который выполняется только при прямом запуске файла, но не при импорте. Используй для тестов и демонстрации кода внутри модуля.

👉 В следующем уроке мы разберём пакеты — папки с несколькими модулями. Узнаем зачем нужен __init__.py, как строить иерархию папок и импортировать из вложенных пакетов. Это последний кирпич для построения настоящих структурированных Python-проектов.