Python: Урок 3 — Классы и ООП

Python: Урок 3 — Классы и ООП.md

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


1. Зачем нужны классы?

Представьте, что нужно описать 100 пользователей. Без классов:

# Неудобно — разрозненные переменные
user1_name = "Алекс"
user1_age = 25
user1_email = "alex@mail.ru"

user2_name = "Мария"
user2_age = 30
user2_email = "maria@mail.ru"

С классом — чисто и масштабируемо:

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

user1 = User("Алекс", 25, "alex@mail.ru")
user2 = User("Мария", 30, "maria@mail.ru")

2. Создание класса и метод init

__init__ — это конструктор: он вызывается автоматически при создании объекта.

class Dog:
    def __init__(self, name, breed, age):
        self.name = name    # атрибуты экземпляра
        self.breed = breed
        self.age = age

    def bark(self):
        print(f"{self.name} говорит: Гав!")

    def info(self):
        print(f"{self.name} ({self.breed}), {self.age} лет")


# Создаём объекты (экземпляры класса)
dog1 = Dog("Бобик", "Лабрадор", 3)
dog2 = Dog("Рекс", "Овчарка", 5)

dog1.bark()   # Бобик говорит: Гав!
dog2.info()   # Рекс (Овчарка), 5 лет

# Доступ к атрибутам
print(dog1.name)   # Бобик
print(dog2.age)    # 5

self — это ссылка на сам объект. Первым параметром он есть в каждом методе, но при вызове его не передают.


3. Атрибуты класса vs атрибуты экземпляра

class Counter:
    count = 0  # атрибут КЛАССА — общий для всех объектов

    def __init__(self, name):
        self.name = name          # атрибут ЭКЗЕМПЛЯРА — у каждого свой
        Counter.count += 1

    def show(self):
        print(f"{self.name}, всего объектов: {Counter.count}")


c1 = Counter("Первый")
c2 = Counter("Второй")
c3 = Counter("Третий")

c1.show()  # Первый, всего объектов: 3
c2.show()  # Второй, всего объектов: 3
print(Counter.count)  # 3

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

Методы вида __имя__ называют магическими или dunder-методами. Они позволяют управлять поведением объектов.

class Vector:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __str__(self):
        """Что выводит print()"""
        return f"Vector({self.x}, {self.y})"

    def __repr__(self):
        """Техническое представление объекта"""
        return f"Vector(x={self.x}, y={self.y})"

    def __add__(self, other):
        """Оператор + между двумя векторами"""
        return Vector(self.x + other.x, self.y + other.y)

    def __len__(self):
        """Длина — количество координат"""
        return 2

    def __eq__(self, other):
        """Оператор == """
        return self.x == other.x and self.y == other.y


v1 = Vector(1, 2)
v2 = Vector(3, 4)

print(v1)          # Vector(1, 2)
print(v1 + v2)     # Vector(4, 6)
print(len(v1))     # 2
print(v1 == v2)    # False
print(v1 == Vector(1, 2))  # True

5. Свойства: @property

@property позволяет вызывать метод как атрибут и добавлять проверки при установке значений:

class Temperature:
    def __init__(self, celsius):
        self._celsius = celsius  # "_" — соглашение "приватный" атрибут

    @property
    def celsius(self):
        return self._celsius

    @celsius.setter
    def celsius(self, value):
        if value < -273.15:
            raise ValueError("Температура ниже абсолютного нуля!")
        self._celsius = value

    @property
    def fahrenheit(self):
        return self._celsius * 9 / 5 + 32


t = Temperature(100)
print(t.celsius)     # 100
print(t.fahrenheit)  # 212.0

t.celsius = 0
print(t.fahrenheit)  # 32.0

t.celsius = -300     # ValueError: Температура ниже абсолютного нуля!

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

Наследование позволяет создать новый класс на основе существующего, переиспользуя его код:

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

    def speak(self):
        print(f"{self.name} говорит: {self.sound}!")

    def __str__(self):
        return f"Животное: {self.name}"


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

    def purr(self):
        print(f"{self.name} мурлычет...")


class Dog(Animal):
    def __init__(self, name, breed):
        super().__init__(name, "Гав")
        self.breed = breed

    def fetch(self):
        print(f"{self.name} приносит мяч!")


cat = Cat("Мурка")
dog = Dog("Бобик", "Лабрадор")

cat.speak()   # Мурка говорит: Мяу!  (унаследован от Animal)
cat.purr()    # Мурка мурлычет...    (собственный метод)

dog.speak()   # Бобик говорит: Гав!
dog.fetch()   # Бобик приносит мяч!

print(cat)    # Животное: Мурка       (унаследован __str__)

7. Переопределение методов (override)

Дочерний класс может изменить поведение родительского метода:

class Shape:
    def area(self):
        return 0

    def describe(self):
        print(f"Площадь фигуры: {self.area()}")


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

    def area(self):  # переопределяем метод родителя
        return 3.14159 * self.radius ** 2


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

    def area(self):  # переопределяем метод родителя
        return self.width * self.height


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

circle.describe()   # Площадь фигуры: 78.53975
rect.describe()     # Площадь фигуры: 24

8. isinstance() и issubclass()

print(isinstance(circle, Circle))   # True
print(isinstance(circle, Shape))    # True — Circle наследует Shape
print(isinstance(circle, Rectangle)) # False

print(issubclass(Circle, Shape))    # True
print(issubclass(Shape, Circle))    # False

9. Статические методы и методы класса

class MathUtils:

    @staticmethod
    def add(a, b):
        """Не нужен ни self, ни cls — просто утилита"""
        return a + b

    @classmethod
    def create_zero_vector(cls):
        """cls — это сам класс, а не экземпляр"""
        return cls()  # создаёт экземпляр класса


class Pizza:
    def __init__(self, size, toppings):
        self.size = size
        self.toppings = toppings

    @classmethod
    def margherita(cls):
        """Фабричный метод — удобный способ создать объект"""
        return cls("средняя", ["томат", "моцарелла", "базилик"])

    @classmethod
    def pepperoni(cls):
        return cls("большая", ["томат", "пепперони", "сыр"])

    def __str__(self):
        return f"Пицца {self.size}: {', '.join(self.toppings)}"


print(MathUtils.add(3, 7))       # 10

p1 = Pizza.margherita()
p2 = Pizza.pepperoni()
print(p1)  # Пицца средняя: томат, моцарелла, базилик
print(p2)  # Пицца большая: томат, пепперони, сыр

10. Мини-проект: Банковский счёт

Применим всё изученное в одном проекте:

class BankAccount:
    _total_accounts = 0

    def __init__(self, owner, balance=0):
        self.owner = owner
        self._balance = balance
        BankAccount._total_accounts += 1
        self._history = []

    @property
    def balance(self):
        return self._balance

    def deposit(self, amount):
        if amount <= 0:
            raise ValueError("Сумма должна быть положительной")
        self._balance += amount
        self._history.append(f"+ {amount} руб.")
        print(f"Пополнено: {amount} руб. Баланс: {self._balance} руб.")

    def withdraw(self, amount):
        if amount <= 0:
            raise ValueError("Сумма должна быть положительной")
        if amount > self._balance:
            raise ValueError("Недостаточно средств")
        self._balance -= amount
        self._history.append(f"- {amount} руб.")
        print(f"Снято: {amount} руб. Баланс: {self._balance} руб.")

    def show_history(self):
        print(f"\nИстория операций ({self.owner}):")
        for record in self._history:
            print(f"  {record}")

    def __str__(self):
        return f"Счёт {self.owner}: {self._balance} руб."

    @classmethod
    def total_accounts(cls):
        print(f"Всего счетов в банке: {cls._total_accounts}")


class SavingsAccount(BankAccount):
    """Накопительный счёт с процентами"""

    def __init__(self, owner, balance=0, rate=0.05):
        super().__init__(owner, balance)
        self.rate = rate

    def add_interest(self):
        interest = self._balance * self.rate
        self.deposit(interest)
        print(f"Начислены проценты: {interest:.2f} руб.")


# Использование
acc1 = BankAccount("Алекс", 1000)
acc2 = SavingsAccount("Мария", 5000, rate=0.1)

acc1.deposit(500)
acc1.withdraw(200)
acc1.show_history()

print()
acc2.add_interest()
print(acc2)

BankAccount.total_accounts()

Вывод:

Пополнено: 500 руб. Баланс: 1500 руб.
Снято: 200 руб. Баланс: 1300 руб.

История операций (Алекс):
  + 500 руб.
  - 200 руб.

Пополнено: 500.0 руб. Баланс: 5500.0 руб.
Начислены проценты: 500.00 руб.
Счёт Мария: 5500.0 руб.
Всего счетов в банке: 2

Итоги урока

Концепция Синтаксис
Создание класса class MyClass:
Конструктор def __init__(self, ...)
Атрибут класса MyClass.attr = value
Магические методы __str__, __add__, __eq__
Свойство @property, @prop.setter
Наследование class Child(Parent):
Вызов родителя super().__init__(...)
Статический метод @staticmethod
Метод класса @classmethod

Что дальше?

  • Урок 4 — Модули: os, pathlib, datetime, re
  • Урок 5 — Библиотеки: requests, работа с внешними API
  • Урок 6 — Генераторы, декораторы и продвинутый Python

💡 Совет: ООП — это инструмент, а не цель. Используйте классы там, где они упрощают код, а не усложняют его. Простую скрипт-утилиту не нужно оборачивать в класс.