Классы и объекты
Зачем классы
Пока программа маленькая, хватает функций и словарей. Когда данных и действий над ними много, удобно объединить их в объекты: данные (атрибуты) + поведение (методы).
# без классов: данные и функции живут отдельно
user = {"name": "Анна", "balance": 100}
def deposit(user, amount):
user["balance"] += amount
# с классом: данные и поведение вместе
class User:
def __init__(self, name, balance=0):
self.name = name
self.balance = balance
def deposit(self, amount):
self.balance += amount
anna = User("Анна", 100)
anna.deposit(50)
print(anna.name, anna.balance)
- Класс — шаблон, описание: какие атрибуты и методы будут у объектов.
- Объект (экземпляр) — конкретная «вещь», созданная по шаблону:
User("Анна").
init и self
class Point:
def __init__(self, x, y):
print(f" __init__: self={self!r:.40}, x={x}, y={y}")
self.x = x # атрибут экземпляра
self.y = y
def distance_to_origin(self):
return (self.x ** 2 + self.y ** 2) ** 0.5
p = Point(3, 4)
print(p.distance_to_origin())
print(Point.distance_to_origin(p)) # то же самое: p передаётся как self
При Point(3, 4) Python:
- создаёт пустой объект (
Point.__new__); - вызывает
__init__(объект, 3, 4)— инициализатор заполняет атрибуты; - возвращает объект.
self — обычный первый параметр, в который попадает экземпляр. Запись p.method(arg) превращается в Point.method(p, arg).
Пройдите пример 👣 Пошагово: визуализатор покажет объект Point и его атрибуты x, y.
Атрибуты экземпляра и класса
class Car:
wheels = 4 # атрибут класса: общий для всех
def __init__(self, brand):
self.brand = brand # атрибут экземпляра: свой у каждого
a, b = Car("Lada"), Car("Kia")
print(a.wheels, b.wheels, Car.wheels)
Car.wheels = 6 # меняем у класса — видно всем
print(a.wheels, b.wheels)
a.wheels = 3 # присваивание через экземпляр создаёт СВОЙ атрибут
print(a.wheels, b.wheels, Car.wheels)
print(a.__dict__, b.__dict__)
Поиск атрибута
obj.attr ищется так: сначала в obj.__dict__ (атрибуты экземпляра), потом в классе, потом в родительских классах (по MRO). Присваивание obj.attr = value всегда пишет в obj.__dict__.
Изменяемый атрибут класса
class Team: members = [] — список общий для всех команд. Изменяемые атрибуты создавайте в __init__. Подробнее — в уроке «Классические ловушки».
Методы
class Stack:
def __init__(self):
self._items = []
def push(self, item):
self._items.append(item)
return self # возврат self позволяет цепочки вызовов
def pop(self):
if not self._items:
raise IndexError("стек пуст")
return self._items.pop()
def peek(self):
return self._items[-1] if self._items else None
def __len__(self):
return len(self._items)
s = Stack()
s.push(1).push(2).push(3)
print(s.pop(), s.peek(), len(s))
bound = s.push # связанный метод: функция + запомненный self
print(bound, bound.__self__ is s)
repr и str
class Product:
def __init__(self, name, price):
self.name, self.price = name, price
def __repr__(self): # для разработчика
return f"Product({self.name!r}, {self.price})"
def __str__(self): # для пользователя
return f"{self.name} — {self.price} ₽"
p = Product("Кофе", 199)
print(p) # __str__
print(repr(p)) # __repr__
print([p]) # в контейнерах — __repr__
print(f"{p} / {p!r}")
Без __repr__ объект выводится как <__main__.Product object at 0x...>. Всегда определяйте __repr__ — это сильно упрощает отладку.
Динамические атрибуты
class Box:
pass
b = Box()
b.color = "красный" # атрибут можно добавить в любой момент
setattr(b, "size", 10)
print(b.__dict__, getattr(b, "color"), getattr(b, "weight", "нет"), hasattr(b, "size"))
del b.size
print(vars(b))
Это удобно, но атрибуты, созданные вне __init__, затрудняют чтение кода — объявляйте все атрибуты в __init__.
Четыре принципа ООП
| Принцип | Суть | В Python |
|---|---|---|
| Инкапсуляция | данные и методы вместе; детали скрыты за интерфейсом | _protected, __private, property |
| Наследование | новый класс на основе существующего | class Admin(User) |
| Полиморфизм | один интерфейс — разные реализации | утиная типизация, переопределение методов |
| Абстракция | выделение существенного, сокрытие лишнего | ABC, Protocol, публичный API |
Каждому посвящён отдельный урок модуля.
Итог
- Класс — шаблон, объект — экземпляр;
__init__инициализирует атрибуты. self— экземпляр, передаваемый первым аргументом;obj.m(x)≡Class.m(obj, x).- Атрибуты экземпляра — в
obj.__dict__, атрибуты класса — общие; поиск: экземпляр → класс → родители. __repr__для разработчика,__str__для пользователя.
Практика решено 0 из 5
Создайте класс BankAccount:
- __init__(self, owner, balance=0) сохраняет владельца и баланс;
- deposit(amount) увеличивает баланс;
- withdraw(amount) уменьшает баланс, а если денег не хватает — бросает ValueError;
- __repr__ возвращает BankAccount('Анна', 100).
Какие тесты проверяют код (3)
a = BankAccount("Анна"); a.deposit(150); a.withdraw(50); assert a.balance == 100
assert repr(BankAccount("Анна", 100)) == "BankAccount(\x27Анна\x27, 100)"
a = BankAccount("Боря", 10)
try:
a.withdraw(20)
ok = False
except ValueError:
ok = True
assert ok and a.balance == 10, "При нехватке денег — ValueError, баланс не меняется"
class BankAccount:
def __init__(self, owner, balance=0):
self.owner = owner
self.balance = balance
def deposit(self, amount):
self.balance += amount
def withdraw(self, amount):
if amount > self.balance:
raise ValueError("Недостаточно средств")
self.balance -= amount
def __repr__(self):
return f"BankAccount({self.owner!r}, {self.balance})"
Что такое self?
Что выведет код?
class Dog:
species = "собака"
a, b = Dog(), Dog()
a.species = "волк"
print(a.species, b.species, Dog.species)
Создайте класс Counter с атрибутом класса total — общим числом созданных счётчиков.
Каждый новый экземпляр увеличивает Counter.total на 1 и получает свой номер self.number
(первый созданный — 1, второй — 2 и т. д.).
Какие тесты проверяют код (2)
Counter.total = 0; a = Counter(); b = Counter(); c = Counter(); assert (a.number, b.number, c.number) == (1, 2, 3) Counter.total = 0; Counter(); Counter(); assert Counter.total == 2
class Counter:
total = 0
def __init__(self):
Counter.total += 1
self.number = Counter.total
Чем __repr__ отличается от __str__?
Войдите, чтобы отмечать пройденные уроки.