Классы и ООП в Python: __init__, self, методы, наследование и dataclass
ООП в Python строится на классах: класс описывает поля и методы, а объекты (экземпляры) создаются вызовом класса, при этом __init__ заполняет поля, а self указывает на текущий объект. Наследование позволяет взять поведение родителя и переопределить часть методов, а декоратор @dataclass генерирует __init__, __repr__ и __eq__ для классов, которые в основном хранят данные.
Класс в Python — это чертёж: он описывает, какие данные хранит объект и что с ними можно делать. Объект (экземпляр) — конкретное изделие по этому чертежу. Python ООП держится на трёх вещах: классах с методами, наследовании и специальных методах вроде __init__ и __repr__. Разберём всё по порядку на одном сквозном примере — учёте трат.
Зачем нужны классы, если есть словари
Данные можно хранить в словаре: {"title": "Кофе", "amount": 250}. Проблема в том, что словарь не знает, какие ключи правильные. Опечатка expense.get("amout", 0) не вызовет ошибку — вернётся ноль, и отчёт тихо начнёт врать.
У объекта класса поля объявлены заранее. Обращение к несуществующему атрибуту сразу падает с AttributeError, а современный Python ещё и подсказывает правильное имя. Кроме того, класс собирает данные и действия над ними в одном месте: трата сама умеет посчитать сумму с налогом, отчёт сам считает итог.
Как создать класс в Python
Класс объявляют словом class, имя по соглашению PEP 8 пишут в стиле CamelCase. Простейший пример класса:
class Expense:
def __init__(self, title, amount):
self.title = title
self.amount = amount
coffee = Expense("Кофе", 250)
taxi = Expense("Такси", 300)
print(coffee.title, coffee.amount) # Кофе 250
Вызов Expense("Кофе", 250) создаёт новый экземпляр класса. coffee и taxi — два разных объекта одного типа, у каждого свои значения полей. Проверить принадлежность можно через isinstance(coffee, Expense) — вернёт True.
Конструктор __init__: инициализация класса
__init__ часто называют конструктором класса, хотя точнее — инициализатором. Сам объект создаёт метод __new__, а __init__ вызывается сразу после этого и раскладывает переданные аргументы по полям. Возвращать из него ничего не нужно (точнее, он обязан вернуть None).
В __init__ удобно проверять входные данные, чтобы в программе не появлялось заведомо неправильных объектов:
class Expense:
def __init__(self, title, amount):
if amount < 0:
raise ValueError("Сумма не может быть отрицательной")
self.title = title
self.amount = amount
Что такое self в Python
self — ссылка на тот самый объект, с которым сейчас работает метод. Когда вы пишете coffee.with_tax(), Python фактически вызывает Expense.with_tax(coffee) и передаёт объект первым аргументом. Поэтому в объявлении метода self стоит первым, а при вызове его не пишут.
self — не ключевое слово, а соглашение: технически параметр можно назвать как угодно, но так никто не делает. Частая ошибка новичков — забыть self в объявлении метода. Тогда вызов падает с TypeError: Expense.with_tax() takes 0 positional arguments but 1 was given.
Методы класса и специальные методы
Метод — это функция, объявленная внутри класса. Он видит поля объекта через self:
class Expense:
def __init__(self, title, amount):
self.title = title
self.amount = amount
def with_tax(self, rate=0.2):
return self.amount * (1 + rate)
def __repr__(self):
return f"Expense({self.title!r}, {self.amount})"
coffee = Expense("Кофе", 250)
print(coffee.with_tax()) # 300.0
print(coffee) # Expense('Кофе', 250)
Без __repr__ вывод выглядел бы как <__main__.Expense object at 0x7f...> — бесполезно для отладки. Методы с двумя подчёркиваниями с обеих сторон называют специальными (dunder-методами): Python вызывает их сам. __str__ отвечает за «красивый» вывод для пользователя, __eq__ — за сравнение через ==, __len__ — за len(obj).
@classmethod и @staticmethod
Обычный метод работает с экземпляром. Метод класса с декоратором @classmethod получает первым аргументом сам класс (cls) — его часто используют как альтернативный конструктор. Статический метод @staticmethod не получает ни объект, ни класс: это обычная функция, которая логически относится к классу.
class Expense:
def __init__(self, title, amount):
self.title = title
self.amount = amount
@classmethod
def from_csv(cls, line):
title, amount = line.split(";")
return cls(title, float(amount))
@staticmethod
def is_valid_amount(value):
return value >= 0
e = Expense.from_csv("Обед;520")
print(e.amount) # 520.0
print(Expense.is_valid_amount(-1)) # False
Деструктор класса: __del__
У Python есть метод __del__, который вызывается перед уничтожением объекта сборщиком мусора. Полагаться на него для освобождения ресурсов не стоит: момент вызова не гарантирован. Для файлов и соединений используйте менеджер контекста with или явный метод close().
Атрибуты класса и атрибуты экземпляра
Поля, заданные через self.name = ..., принадлежат конкретному объекту. Переменные, объявленные прямо в теле класса, — атрибуты класса: они общие для всех экземпляров. Так обычно хранят константы класса.
class Expense:
CURRENCY = "RUB" # атрибут класса, константа
def __init__(self, title, amount):
self.title = title # атрибуты экземпляра
self.amount = amount
a = Expense("Кофе", 250)
b = Expense("Чай", 150)
print(a.CURRENCY, Expense.CURRENCY) # RUB RUB
Опасная ловушка — изменяемый атрибут класса. Если объявить в теле класса rows = [] и добавлять в него через self.rows.append(...), список окажется общим для всех объектов. Изменяемые поля всегда создавайте в __init__.
Посмотреть поля объекта можно через vars(obj) или obj.__dict__, а прочитать атрибут по имени из строки — через getattr(obj, "amount").
Свойства класса: @property
Свойство — метод, который снаружи читается как обычное поле, без скобок. Это удобно для вычисляемых значений: итог отчёта не нужно хранить отдельно и не забудешь его обновить.
class Report:
def __init__(self):
self.rows = []
def add(self, expense):
self.rows.append(expense)
@property
def total(self):
return sum(e.amount for e in self.rows)
r = Report()
r.add(Expense("Кофе", 250))
r.add(Expense("Такси", 300))
print(r.total) # 550
Если нужно контролировать запись, к свойству добавляют сеттер через @total.setter. Без сеттера присваивание r.total = 0 вызовет AttributeError.
Отдельно об инкапсуляции. Приватных полей в строгом смысле в Python нет. Одно подчёркивание (_cache) — договорённость «не трогайте снаружи», два (__secret) — включают искажение имени (name mangling), чтобы поле случайно не перекрыл наследник.
Наследование классов в Python
Наследование позволяет создать дочерний класс на базе родительского: потомок получает все поля и методы, а нужные — переопределяет. Родительский класс указывают в скобках.
class MonthlyReport(Report):
def __init__(self, month):
super().__init__() # вызвать __init__ родителя
self.month = month
def add(self, expense): # переопределение метода
print(f"[{self.month}] + {expense}")
super().add(expense) # сохранить поведение родителя
m = MonthlyReport("октябрь")
m.add(Expense("Кофе", 250)) # [октябрь] + Expense('Кофе', 250)
print(m.total) # 250 — свойство унаследовано
super() возвращает объект-посредник для вызова методов родителя. Забытый super().__init__() в дочернем __init__ — классическая ошибка: поле rows не создаётся, и первый же add падает с AttributeError.
Проверить родство можно через issubclass(MonthlyReport, Report). Все классы в Python 3 неявно наследуются от object — отсюда базовые __repr__, __eq__ и другие методы. Python поддерживает и множественное наследование (class C(A, B)); порядок поиска методов показывает C.__mro__.
Абстрактный класс в Python
Абстрактный класс задаёт интерфейс: какие методы обязаны реализовать подклассы. Создать его экземпляр нельзя. Для этого служит модуль abc:
from abc import ABC, abstractmethod
class Exporter(ABC):
@abstractmethod
def export(self, report): ...
class CsvExporter(Exporter):
def export(self, report):
return "\n".join(f"{e.title};{e.amount}" for e in report.rows)
Exporter() # TypeError: Can't instantiate abstract class Exporter ...
Подкласс, который не реализовал все абстрактные методы, тоже остаётся абстрактным и при создании объекта выдаст ту же ошибку.
Композиция вместо глубоких иерархий
После знакомства с наследованием хочется строить лестницы: базовая трата, денежная трата, трата в рублях — и так на пять уровней. В Python так пишут редко. Наследование описывает отношение «является», а композиция — «содержит»: отчёт не является списком трат, он содержит его в поле rows.
Практическое правило: если нужно лишь использовать умение другого объекта, положите его в поле, а не наследуйтесь. Для данных часто хватает dataclass, а логику можно держать в обычных функциях.
Dataclasses в Python: класс без шаблонного кода
Для классов, которые в основном хранят данные, писать __init__, __repr__ и __eq__ вручную утомительно. Декоратор @dataclass из модуля dataclasses генерирует их по аннотациям полей:
from dataclasses import dataclass, field
@dataclass
class Expense:
title: str
amount: float
tags: list[str] = field(default_factory=list)
def __post_init__(self):
if self.amount < 0:
raise ValueError("Сумма не может быть отрицательной")
e = Expense("Кофе", 250)
print(e) # Expense(title='Кофе', amount=250, tags=[])
print(e == Expense("Кофе", 250)) # True
Проверку значений, которую раньше писали в __init__, переносят в __post_init__ — он вызывается сразу после сгенерированного инициализатора. Изменяемые значения по умолчанию задают только через field(default_factory=list): запись tags: list = [] dataclass запрещает и выдаёт ValueError.
frozen и slots
Параметр frozen=True делает объект неизменяемым, а slots=True (с Python 3.10) создаёт __slots__ и экономит память. Попытка изменить поле замороженного объекта вызывает FrozenInstanceError. Чтобы получить объект с новым значением, создают копию:
from dataclasses import dataclass, replace
@dataclass(frozen=True, slots=True)
class Expense:
title: str
amount: float
e = Expense("Кофе", 250)
# e.amount = 300 -> dataclasses.FrozenInstanceError
e2 = replace(e, amount=300)
print(e, e2) # Expense(title='Кофе', amount=250) Expense(title='Кофе', amount=300)
С Python 3.13 то же самое умеет универсальная функция copy.replace(). Замороженные dataclass-объекты можно класть в множества и использовать как ключи словаря — у них есть __hash__.
Что нового в классах: Self, @override, __slots__
Для типизации классов есть два полезных инструмента. typing.Self (Python 3.11, PEP 673) означает «вернётся объект того же класса» — это работает и в унаследованных методах. Декоратор @override (Python 3.12, PEP 698) помечает метод, который обязан переопределять родительский: если вы опечатались в имени, mypy или pyright сообщат, что базового метода с таким именем нет.
from typing import Self, override
class Report:
def __init__(self):
self.rows = []
def add(self, e) -> Self:
self.rows.append(e)
return self # можно вызывать цепочкой
class MonthlyReport(Report):
@override
def add(self, e) -> Self:
return super().add(e)
В Python 3.15 сняли нишевое ограничение: подклассам tuple разрешены непустые __slots__, а __dict__ и __weakref__ можно указывать в __slots__ любого класса. В 3.14 такое объявление у наследника кортежа падало с TypeError: nonempty __slots__ not supported for subtype of 'tuple'.
Частые ошибки при работе с классами
| Ошибка | Симптом | Как исправить |
|---|---|---|
Забыт self в методе | TypeError: ... takes 0 positional arguments but 1 was given | Первым параметром метода всегда self |
Поле без self. в __init__ | Значение пропадает после __init__ | Писать self.amount = amount |
| Список как атрибут класса | Данные «перетекают» между объектами | Создавать списки в __init__ или через default_factory |
Нет super().__init__() | AttributeError на полях родителя | Вызывать инициализатор родителя |
| Вызов свойства со скобками | TypeError: 'int' object is not callable | Читать свойство без скобок: r.total |
Итог
Python ООП начинается с простого: класс описывает поля и методы, __init__ заполняет поля, self указывает на текущий объект. Наследование и super() позволяют расширять поведение, абстрактные классы — задавать интерфейс, а @property — делать вычисляемые поля. Для классов-данных берите @dataclass, а глубоким иерархиям предпочитайте композицию. На практике классы и dataclasses разобраны в лекции курса по Python на примере трекера расходов.
Частые вопросы
Чем класс отличается от объекта в Python?
Класс — это описание типа: какие поля и методы у него есть. Объект, или экземпляр, создаётся вызовом класса, например Expense("Кофе", 250), и хранит конкретные значения полей.
Обязательно ли писать __init__ в классе?
Нет. Если __init__ не объявлен, используется унаследованный от object, который не принимает аргументов. В dataclass его генерирует декоратор.
Чем __str__ отличается от __repr__?
__repr__ — однозначное представление для разработчика и отладчика, __str__ — понятный вывод для пользователя в print и f-строках. Если __str__ нет, Python использует __repr__.
Когда выбрать dataclass, а когда обычный класс?
Dataclass подходит, когда класс в основном хранит данные и нужны готовые __init__, __repr__ и сравнение. Обычный класс удобнее, когда важна сложная логика инициализации или поведение важнее данных.
Есть ли в Python приватные поля?
Строгой приватности нет. Одно подчёркивание в начале имени — соглашение «не для внешнего использования», два подчёркивания включают искажение имени, которое защищает поле от случайного переопределения в подклассе.
Эта тема подробно разобрана с практикой в курсе «Python 3.15: от новичка до профи».
Перейти к курсу