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

URL: https://skillaria.top/ru/guides/python/klassy-python

ООП в 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 на примере трекера расходов.

## FAQ

**Чем класс отличается от объекта в Python?**

Класс — это описание типа: какие поля и методы у него есть. Объект, или экземпляр, создаётся вызовом класса, например Expense("Кофе", 250), и хранит конкретные значения полей.

**Обязательно ли писать __init__ в классе?**

Нет. Если __init__ не объявлен, используется унаследованный от object, который не принимает аргументов. В dataclass его генерирует декоратор.

**Чем __str__ отличается от __repr__?**

__repr__ — однозначное представление для разработчика и отладчика, __str__ — понятный вывод для пользователя в print и f-строках. Если __str__ нет, Python использует __repr__.

**Когда выбрать dataclass, а когда обычный класс?**

Dataclass подходит, когда класс в основном хранит данные и нужны готовые __init__, __repr__ и сравнение. Обычный класс удобнее, когда важна сложная логика инициализации или поведение важнее данных.

**Есть ли в Python приватные поля?**

Строгой приватности нет. Одно подчёркивание в начале имени — соглашение «не для внешнего использования», два подчёркивания включают искажение имени, которое защищает поле от случайного переопределения в подклассе.


---
Source: Skillaria.Top (https://skillaria.top)
