Класс — это описание того, из чего состоит объект и что он умеет. Экземпляр — конкретный объект, сделанный по этому описанию. Один класс Cart, сколько угодно корзин, у каждой свой владелец и свой список товаров.
class Cart:
def __init__(self, owner):
self.owner = owner
self.items = []
def add(self, item):
self.items.append(item)
cart = Cart("Аня")
cart.add("мышь")
print(cart.owner, cart.items)
Аня ['мышь']
self — не магия, а первый аргумент
self смущает потому, что при вызове его не пишут. Но он никуда не девается: Python подставляет его сам, и в этом легко убедиться — вызвать метод можно обоими способами, результат одинаковый.
class Cart:
def __init__(self, owner):
self.owner = owner
self.items = []
def add(self, item):
self.items.append(item)
cart = Cart("Аня")
cart.add("мышь") # обычный вызов
Cart.add(cart, "клавиатура") # то же самое, но self передан руками
print(cart.items)
['мышь', 'клавиатура']
Разница видна, если напечатать сам метод:
class Cart:
def add(self, item):
pass
cart = Cart()
print(type(cart.add).__name__)
print(type(Cart.add).__name__)
method
function
Из класса add — обычная функция. Из экземпляра — «связанный метод»: та же функция, к которой уже привязан первый аргумент. Поэтому self не ключевое слово: его можно назвать как угодно, просто все договорились называть self, и отступать от этого не стоит.
Где живут атрибуты
Атрибут, присвоенный через self внутри метода, принадлежит экземпляру. Атрибут, записанный прямо в теле класса, принадлежит классу и общий для всех.
Два экземпляра — два списка покупок
a.vat_rate находит общее значение, а a.owner — своё.Общий атрибут на месте — это удобно для констант:
class Order:
vat_rate = 0.20
def __init__(self, amount):
self.amount = amount
def with_vat(self):
return round(self.amount * (1 + self.vat_rate), 2)
print(Order(1000).with_vat(), Order(2500).with_vat())
1200.0 3000.0
Ловушка: изменяемый атрибут класса
А вот если общим по недосмотру окажется список, начнётся странное:
class Basket:
items = [] # ← в теле класса, а не в __init__
def __init__(self, owner):
self.owner = owner
def add(self, item):
self.items.append(item)
a, b = Basket("Аня"), Basket("Борис")
a.add("мышь")
print("у Ани: ", a.items)
print("у Бориса:", b.items)
print("это один и тот же список:", a.items is b.items)
у Ани: ['мышь']
у Бориса: ['мышь']
это один и тот же список: True
Борис ничего не клал, но товар у него есть: список создан один раз при описании класса, и self.items.append(...) меняет именно его. Тот же класс ошибок, что и изменяемое значение по умолчанию у функции — и та же причина: изменяемый объект создан один раз, а используется многими.
Увидеть, где что лежит, можно напрямую:
class Basket:
items = []
def __init__(self, owner):
self.owner = owner
a = Basket("Аня")
print("в экземпляре:", a.__dict__)
print("в классе: ", "items" in Basket.__dict__)
в экземпляре: {'owner': 'Аня'}
в классе: True
Чинится переносом в __init__ — тогда список создаётся заново на каждый экземпляр:
class Basket:
def __init__(self, owner):
self.owner = owner
self.items = [] # ← своё для каждого
def add(self, item):
self.items.append(item)
a, b = Basket("Аня"), Basket("Борис")
a.add("мышь")
print("у Ани: ", a.items)
print("у Бориса:", b.items)
у Ани: ['мышь']
у Бориса: []
Правило простое: в теле класса — только неизменяемое (числа, строки, кортежи). Всё, у чего есть методы изменения на месте, создавай в __init__.
init — не конструктор, а настройщик
__init__ вызывается уже после того, как объект создан: его задача — разложить по местам начальные значения, а не вернуть объект. Поэтому return в нём не пишут.
Атрибуты можно добавлять и потом, в любой момент, и Python не возразит:
class Point:
def __init__(self, x):
self.x = x
p, q = Point(1), Point(2)
p.color = "красный"
print(p.__dict__)
print("у q такого атрибута нет:", hasattr(q, "color"))
{'x': 1, 'color': 'красный'}
у q такого атрибута нет: False
Гибко, но опасно: опечатка в имени атрибута создаст новый вместо того, чтобы упасть. Если такое поведение мешает, помогает __slots__ или @dataclass(slots=True). Обычный @dataclass без slots=True не запрещает новые атрибуты.
Печать объекта
По умолчанию объект печатается неинформативно:
class Cart:
def __init__(self, owner):
self.owner = owner
print(Cart("Аня"))
<__main__.Cart object at 0x7fad5f290d70>
Шестнадцатеричное число — адрес в памяти, у тебя будет другое. Чтобы объект печатался по-человечески, добавь __repr__:
class Cart:
def __init__(self, owner):
self.owner = owner
def __repr__(self):
return f"Cart(owner={self.owner!r})"
print(Cart("Аня"))
print([Cart("Аня"), Cart("Борис")])
Cart(owner='Аня')
[Cart(owner='Аня'), Cart(owner='Борис')]
__repr__ работает и внутри списков, и в отладчике, и в логах — одна строка кода экономит часы на диагностике. Модификатор !r в f-строке добавляет кавычки, чтобы было видно, что это строка.
Три вида методов
class Temperature:
def __init__(self, celsius):
self.celsius = celsius
def to_fahrenheit(self): # обычный: работает с объектом
return self.celsius * 9 / 5 + 32
@classmethod
def from_fahrenheit(cls, f): # классовый: создаёт объект
return cls((f - 32) * 5 / 9)
@staticmethod
def is_freezing(celsius): # статический: просто функция рядом
return celsius < 0
print(Temperature(20).to_fahrenheit())
print(Temperature.from_fahrenheit(68).celsius)
print(Temperature.is_freezing(-5))
68.0
20.0
True
Обычный метод получает self и работает с конкретным объектом. @classmethod получает сам класс и чаще всего служит альтернативным способом создать объект. @staticmethod не получает ничего — это функция, положенная внутрь класса из соображений порядка. Про то, как устроены сами @-обёртки, есть разбор декораторов.
Когда класс не нужен
- Данные без поведения — не классЕсли у объекта только поля и ни одного осмысленного метода, это словарь, кортеж или dataclass. Класс с одним
__init__ничего не добавляет. - Одна операция — это функцияКласс с единственным методом и без состояния между вызовами — переодетая функция. Признак: экземпляр создают и тут же выбрасывают.
- Разобранный JSON — уже словарьОборачивать в класс данные, которые пришли извне и уходят наружу без обработки, незачем. Подробнее — в разборе работы с JSON.
- Класс оправдан состояниемКогда между вызовами что-то нужно помнить — накопленные товары, открытое соединение, позиция в файле — вот тогда класс на месте.
Частые вопросы
Что такое self и можно ли его не писать
self — это ссылка на сам объект, первый параметр каждого обычного метода. При вызове через точку Python подставляет его автоматически, но в определении писать обязательно, иначе аргументы сдвинутся и вызов упадёт с TypeError.
Чем класс отличается от экземпляра
Класс — описание, экземпляр — сделанный по нему объект. Cart один, Cart("Аня") и Cart("Борис") — два разных объекта со своими данными.
Почему у всех объектов один и тот же список
Потому что он создан в теле класса, а не в __init__. Проверить: "items" in Basket.__dict__ вернёт True, если атрибут на классе. Перенеси создание списка в __init__.
Зачем нужен init, если атрибуты можно присвоить потом
Чтобы объект сразу был готов к работе и нельзя было забыть обязательное поле. Присваивание задним числом молча создаёт новый атрибут даже при опечатке, а __init__ задаёт минимальный набор один раз и в одном месте.
Что печатается вместо объекта
<__main__.Cart object at 0x…> — имя класса и адрес в памяти. Так выглядит объект без __repr__. Добавь метод __repr__, возвращающий строку, и объект станет читаемым везде, включая списки и логи.
Где потренироваться
Блоки на этой странице запускаются прямо здесь — поменяй items = [] местами с __init__ и посмотри, как меняется поведение. На живых задачах классы разбираются в пути «ООП на Python», а следующий шаг после ручных классов — dataclass, который пишет __init__ и __repr__ за тебя.