Python: Урок 3 — Классы и ООП
ООП (объектно-ориентированное программирование) — это способ организации кода через объекты, которые объединяют данные и поведение. 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
💡 Совет: ООП — это инструмент, а не цель. Используйте классы там, где они упрощают код, а не усложняют его. Простую скрипт-утилиту не нужно оборачивать в класс.