Основы

Классы и ООП в Python: __init__, self, методы, наследование и dataclass

Обновлено: · Skillaria.Top

ООП в 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: от новичка до профи».

Перейти к курсу