Глава 9. Модули и основы ООП

84 просмотров
0 лайков
0 в избранном

Модули и импорт

Модуль — это файл .py, который можно импортировать в другой файл. Один файл — один модуль. Имя модуля совпадает с именем файла без расширения.

Создадим файл math_utils.py:

# math_utils.py
PI = 3.14159

def circle_area(radius):
    return PI * radius ** 2

def circle_perimeter(radius):
    return 2 * PI * radius

Теперь в другом файле того же каталога импортируем его:

# main.py
import math_utils

print(math_utils.PI)                  # 3.14159
print(math_utils.circle_area(5))      # 78.53975

Варианты импорта

# Импорт всего модуля — обращение через имя модуля
import math_utils
math_utils.circle_area(5)

# Импорт конкретных имён — короче, но может конфликтовать
from math_utils import circle_area, PI
circle_area(5)

# Импорт со псевдонимом
import math_utils as mu
mu.circle_area(5)

# Импорт всего (звёздочка) — НЕ рекомендуется: засоряет пространство имён
# from math_utils import *

Стандартная библиотека

В Python встроена богатая стандартная библиотека — «батарейки в комплекте». Несколько ходовых модулей:

math — математика

import math

print(math.sqrt(16))    # 4.0 — квадратный корень
print(math.pi)          # 3.141592653589793
print(math.ceil(2.1))   # 3 — округление вверх
print(math.floor(2.9))  # 2 — вниз
print(math.log(100, 10))  # 2.0 — логарифм

random — случайные числа

import random

print(random.randint(1, 100))   # случайное целое от 1 до 100
print(random.random())          # случайный float от 0 до 1
print(random.choice(["А", "Б", "В"]))  # случайный элемент списка

# shuffle перемешивает список НА МЕСТЕ и возвращает None —
# поэтому печатаем сам список, а не результат вызова
my_list = [1, 2, 3, 4, 5]
random.shuffle(my_list)
print(my_list)                  # например, [3, 1, 5, 2, 4]

# sample — выбрать несколько разных элементов, не меняя исходный список
print(random.sample(range(1, 50), 6))   # 6 уникальных чисел для лото

datetime — дата и время

from datetime import datetime, date, timedelta

now = datetime.now()
print(now)                  # 2024-01-15 12:34:56.789
print(now.strftime("%d.%m.%Y"))   # 15.01.2024 — форматирование

today = date.today()
week_later = today + timedelta(days=7)
print(week_later)           # через неделю

os и pathlib — работа с путями и ОС

from pathlib import Path

# Path — объектно-ориентированный путь
p = Path("data/users.json")
print(p.name)       # users.json
print(p.suffix)     # .json
print(p.parent)     # data
print(p.exists())   # True/False — существует ли файл

# Создать папки
Path("output/logs").mkdir(parents=True, exist_ok=True)

Модуль os отвечает за всё остальное, что связано с операционной системой: переменные окружения, текущий каталог, список файлов:

import os

print(os.getcwd())                  # текущий рабочий каталог
print(os.listdir("."))              # список файлов в каталоге

# Переменные окружения — так передают настройки и секреты,
# чтобы не хранить их в коде
db_url = os.environ.get("DATABASE_URL")          # None, если не задана
todo_file = os.environ.get("TODO_FILE", "tasks.json")   # со значением по умолчанию
print(todo_file)

Правило простое: пути — через pathlib.Path, окружение и процессы — через os.

Свой модуль для проекта

По мере роста проекта логику раскладывают по модулям. Пример структуры:

my-app/
├── main.py            # точка входа
├── tasks.py           # логика работы с задачами
├── storage.py         # сохранение/загрузка в файл
└── utils.py           # вспомогательные функции

В main.py импортируем свои модули так же, как стандартные:

from tasks import add_task, list_tasks
from storage import save, load

Зачем нужно ООП

Объектно-ориентированное программирование (ООП) — способ организации кода, при котором данные и действия над ними объединяются в объекты. Класс — это «чертёж» объекта, а объект — конкретный экземпляр.

ООП полезно, когда у вас есть несколько сущностей с одинаковым поведением: пользователи, товары, заказы. Вместо набора разрозненных функций и словарей вы создаёте класс и работаете с объектами.

Объявление класса

class User:
    def __init__(self, name, age):
        self.name = name
        self.age = age

    def greet(self):
        return f"Привет, я {self.name}!"

# Создаём объекты — экземпляры класса
anna = User("Анна", 28)
boris = User("Борис", 22)

print(anna.name)      # Анна
print(anna.greet())   # Привет, я Анна!
print(boris.greet())  # Привет, я Борис!

Что здесь происходит

  • class User: — объявление класса (CamelCase по соглашению).
  • __init__конструктор, вызывается при создании объекта.
  • self — ссылка на текущий объект. Первый параметр любого метода.
  • self.name = name — создаём атрибут объекта.
  • User("Анна", 28) — создаём объект (вызывается __init__).

Атрибуты и методы

Атрибут — данные объекта, метод — функция внутри класса. Доступ — через точку:

class Counter:
    def __init__(self):
        self.value = 0       # атрибут

    def increment(self):     # метод
        self.value += 1

    def reset(self):
        self.value = 0

c = Counter()
c.increment()
c.increment()
print(c.value)   # 2
c.reset()
print(c.value)   # 0

Магические методы (dunder)

Методы вида __name__ («dunder» от double underscore) определяют поведение объекта в стандартных ситуациях. __init__ мы уже знаем. Полезные:

class Product:
    def __init__(self, title, price):
        self.title = title
        self.price = price

    def __str__(self):
        '''Что покажет print().'''
        return f"{self.title} — {self.price} руб."

    def __repr__(self):
        '''Техническое представление для отладки.'''
        return f"Product({self.title!r}, {self.price})"

    def __eq__(self, other):
        '''Поведение оператора ==.'''
        return self.title == other.title and self.price == other.price

p = Product("Ноутбук", 89990)
print(p)         # Ноутбук — 89990 руб. (через __str__)
print(repr(p))   # Product('Ноутбук', 89990) (через __repr__)

Наследование

Один класс может наследовать атрибуты и методы другого. Это позволяет переиспользовать код и строить иерархии:

class Animal:
    def __init__(self, name):
        self.name = name

    def speak(self):
        return "Какой-то звук"

class Dog(Animal):           # Dog наследует Animal
    def speak(self):         # переопределение метода
        return "Гав!"

class Cat(Animal):
    def speak(self):
        return "Мяу!"

animals = [Dog("Шарик"), Cat("Мурка"), Animal("Существо")]
for a in animals:
    print(f"{a.name}: {a.speak()}")
# Шарик: Гав!
# Мурка: Мяу!
# Существо: Какой-то звук

Это полиморфизм: мы вызываем один метод speak(), а каждый объект отвечает по-своему.

super() — вызов родителя

Через super() можно вызвать метод родительского класса:

class Animal:
    def __init__(self, name):
        self.name = name

class Dog(Animal):
    def __init__(self, name, breed):
        super().__init__(name)   # вызов конструктора родителя
        self.breed = breed

d = Dog("Шарик", "Дворняга")
print(d.name, d.breed)   # Шарик Дворняга

Когда класс, а когда функция

Новички часто переоценивают ООП и создают класс для всего. Простое правило:

  • Функция — действие, не требующее состояния (посчитать, отформатировать, проверить).
  • Класс — сущность с состоянием и связанным поведением (пользователь, заказ, сессия).

Если у класса только статические методы и нет self — это просто модуль с функциями, класс тут лишний.

Кейс из реального проекта: класс Task

Менеджер задач — классический пример. Задача имеет состояние (название, статус выполнения), и с ней связаны действия (отметить выполненной). Создадим каркас, который в финальной главе превратится в CLI-приложение:

from datetime import datetime

class Task:
    def __init__(self, title: str, done: bool = False):
        self.title = title
        self.done = done
        self.created_at = datetime.now()

    def mark_done(self) -> None:
        self.done = True

    def __str__(self) -> str:
        status = "[x]" if self.done else "[ ]"
        return f"{status} {self.title}"

    def to_dict(self) -> dict:
        return {
            "title": self.title,
            "done": self.done,
        }


t1 = Task("Выучить Python")
t2 = Task("Написать CLI-утилиту")
t2.mark_done()

print(t1)   # [ ] Выучить Python
print(t2)   # [x] Написать CLI-утилиту

Заметьте: класс хранит состояние и предоставляет действия. Сериализация в словарь (to_dict) позволит легко сохранить задачу в JSON.

Типичные ошибки

1. Забыли self

class User:
    def __init__(name, age):   # ошибка! забыли self
        self.name = name

class User:
    def __init__(self, name, age):   # правильно
        self.name = name

2. Изменяемый атрибут класса

class User:
    tags = []          # общий для ВСЕХ объектов — ловушка!

u1 = User()
u2 = User()
u1.tags.append("admin")
print(u2.tags)   # ['admin'] — сюрприз!

# Правильно — создавать в __init__
class User:
    def __init__(self):
        self.tags = []   # у каждого свой

3. Класс ради класса

# Слишком сложно для простой операции
class Adder:
    def add(self, a, b):
        return a + b

# Достаточно функции
def add(a, b):
    return a + b

Практика

  1. Создайте модуль math_utils.py с функциями площади и периметра круга. Импортируйте в другой файл.
  2. Используйте random, чтобы сгенерировать 6 уникальных чисел от 1 до 49 (лото).
  3. Создайте класс BankAccount с методами deposit, withdraw и balance (атрибут).
  4. Реализуйте __str__ для класса Book с полями title и author.
  5. Сделайте иерархию ShapeCircle, Square с методом area().

Итог

Мы научились импортировать стандартные модули и создавать свои, посмотрели на полезные инструменты стандартной библиотеки (math, random, datetime, pathlib, os), изучили основы ООП: классы, объекты, __init__, self, атрибуты и методы, магические методы и наследование. Всё это пригодится в финальной главе, где мы соберём полноценное CLI-приложение.

Комментарии 0

Для добавления комментариев необходимо войти или зарегистрироваться.

Пока нет комментариев. Станьте первым!