ООП в Python: классы и объекты на примерах

ООП в Python: классы и объекты на примерах Полезное

ООП в Python (объектно-ориентированное программирование) — способ писать программу из объектов, где данные и действия над ними собраны вместе. Класс описывает, какие данные и методы будут у объектов, а объект (экземпляр) — конкретная сущность, созданная по этому описанию: класс BankAccount один, а счетов Анны и Бориса может быть сколько угодно.

Ниже — рабочий пример, затем разбор атрибутов, self, трех видов методов, инкапсуляции, наследования и dataclass. Весь код проверен на Python 3.14 (CPython 3.14.6), 24.09.2026.

Словарь: класс, объект, атрибут, метод

Термин Что это Пример
Класс Описание будущих объектов: какие данные и методы у них будут. Сам класс в Python тоже объект class BankAccount:
Объект, экземпляр Конкретное значение, созданное вызовом класса. «Объект» и «экземпляр класса X» в Python — одно и то же anna = BankAccount("Анна")
Атрибут Имя, привязанное к объекту или классу (данные) anna.balance
Метод Функция, объявленная в классе и вызываемая через объект anna.deposit(300)

Аналогия для старта: класс — чертеж дома, объект — дом, построенный по чертежу. Аналогия упрощенная: в Python чертеж можно менять во время работы программы, и изменения увидят уже построенные «дома».

Минимальный рабочий пример

class BankAccount:
    """Счет: данные (атрибуты) + поведение (методы) в одном месте."""

    currency = "RUB"  # атрибут класса: общий для всех счетов

    def __init__(self, owner, balance=0):
        self.owner = owner      # атрибуты экземпляра: у каждого свои
        self.balance = balance

    def deposit(self, amount):
        if amount <= 0:
            raise ValueError("Сумма должна быть положительной")
        self.balance += amount
        return self.balance

    def __repr__(self):
        return f"BankAccount({self.owner!r}, {self.balance})"


anna = BankAccount("Анна")
boris = BankAccount("Борис", 500)

anna.deposit(300)
print(anna)                 # BankAccount('Анна', 300)
print(boris)                # BankAccount('Борис', 500)
print(anna.currency, BankAccount.currency)  # RUB RUB
print(type(anna) is BankAccount, isinstance(anna, BankAccount))  # True True

Вывод:

BankAccount('Анна', 300)
BankAccount('Борис', 500)
RUB RUB
True True

Что здесь происходит по шагам:

  1. class BankAccount: создает класс. В Python 3 любой класс неявно наследуется от object, писать class BankAccount(object) не нужно.
  2. BankAccount("Анна") создает объект и вызывает __init__, который заполняет атрибуты экземпляра. Это инициализатор (его часто называют конструктором); здесь он нужен только чтобы задать стартовые данные.
  3. anna.deposit(300) меняет состояние именно объекта anna: у boris баланс остался 500.
  4. __repr__ — «магический» метод: Python вызывает его, когда объект нужно показать строкой в print без __str__ или в консоли.

Что такое self

self — первый параметр метода, в который Python подставляет объект, через который метод вызвали. Это не ключевое слово, а соглашение об имени: код с другим именем заработает, но так никто не пишет.

class Account:
    def __init__(self, owner):
        self.owner = owner

    def greet(self):
        return f"Счет {self.owner}"

acc = Account("Анна")
print(acc.greet())            # обычный вызов
print(Account.greet(acc))     # то же самое: self = acc

Обе строки печатают Счет Анна. Запись acc.greet() — сокращение для Account.greet(acc): Python сам передает acc первым аргументом. Отсюда частая ошибка новичка: забыть self в объявлении метода и получить TypeError о лишнем аргументе при вызове.

Атрибуты класса и атрибуты экземпляра

Атрибут класса Атрибут экземпляра
Где объявлен В теле класса Обычно в __init__ через self.
Кому принадлежит Один на класс, виден всем объектам Свой у каждого объекта
Когда подходит Константы, настройки по умолчанию, счетчики Данные конкретного объекта

Поиск атрибута в упрощенном виде идет так: сначала объект, потом его класс, потом родители класса. Исключение из этого порядка — @property и другие дескрипторы данных, объявленные в классе: они срабатывают раньше атрибута объекта (так работает Thermometer ниже). Для обычных данных порядок верный, поэтому anna.currency находит RUB в классе. А вот присваивание через объект атрибут класса не меняет — оно создает новый атрибут у объекта:

class Counter:
    total = 0

c = Counter()
c.total = 5                     # создает атрибут экземпляра, класс не меняется
print(c.total, Counter.total)   # 5 0

Типичная ошибка: изменяемый атрибут класса

Неверно — список объявлен на уровне класса:

class Cart:
    items = []                  # ошибка: изменяемый атрибут класса

    def add(self, item):
        self.items.append(item)

a, b = Cart(), Cart()
a.add("книга")
print(b.items)                  # ['книга'] - у b корзина тоже "заполнилась"
print(a.items is b.items)       # True: это один и тот же список

Результат: append не присваивает, а меняет найденный в классе список, поэтому обе корзины видят одну и ту же ['книга']. Исправление — создавать изменяемые данные в __init__:

class Cart:
    def __init__(self):
        self.items = []         # у каждого экземпляра свой список

    def add(self, item):
        self.items.append(item)

a, b = Cart(), Cart()
a.add("книга")
print(b.items)                  # []
print(a.items is b.items)       # False

Три вида методов

from datetime import date

class Student:
    min_age = 16

    def __init__(self, name, birth_year):
        self.name = name
        self.birth_year = birth_year

    def age(self, today=None):                 # метод экземпляра: нужен self
        today = today or date.today()
        return today.year - self.birth_year

    @classmethod
    def from_string(cls, line):                # метод класса: получает cls
        name, year = line.split(";")
        return cls(name.strip(), int(year))

    @staticmethod
    def is_valid_name(name):                   # статический: ни self, ни cls
        return name.replace("-", "").isalpha()

s = Student.from_string("Иван; 2004")
print(s.name, s.age(date(2026, 9, 24)))        # Иван 22
print(Student.is_valid_name("Анна-Мария"))     # True
Вид Первый параметр Когда выбирать
Обычный метод self — объект Нужны данные конкретного объекта
@classmethod cls — класс Альтернативный способ создать объект, работа с атрибутами класса
@staticmethod нет Функция по смыслу относится к классу, но не трогает ни объект, ни класс

Возраст в примере считается упрощенно, по разнице лет без учета дня рождения — для демонстрации метода этого достаточно. @classmethod удобен тем, что в наследнике cls будет классом-наследником, и фабрика создаст объект правильного типа.

Инкапсуляция: соглашения и property

В Python нет модификаторов доступа private/public, как в Java или C#. Есть соглашения:

  • _name — «внутреннее, не трогай снаружи». Интерпретатор это не запрещает.
  • __name внутри класса — Python переименовывает атрибут в _ИмяКласса__name (name mangling). Это защита от случайного конфликта имен в наследниках, а не от доступа: у атрибута __token в классе Secret снаружи сохраняется доступ по имени s._Secret__token.

Чтобы проверять значения при присваивании, используют @property — атрибут, за которым стоят методы:

class Thermometer:
    def __init__(self, celsius=0.0):
        self.celsius = celsius          # пойдет через сеттер ниже

    @property
    def celsius(self):
        return self._celsius

    @celsius.setter
    def celsius(self, value):
        if value < -273.15:
            raise ValueError("Ниже абсолютного нуля")
        self._celsius = value

    @property
    def fahrenheit(self):               # вычисляемое свойство, только чтение
        return self._celsius * 9 / 5 + 32

t = Thermometer(25)
print(t.fahrenheit)                     # 77.0
try:
    t.celsius = -300
except ValueError as e:
    print("Ошибка:", e)                 # Ошибка: Ниже абсолютного нуля
t._celsius = -300                       # соглашение, а не запрет: так тоже можно
print(t.celsius)                        # -300

Последние две строки показывают границу: property проверяет только тот путь, который идет через него. Обход через _celsius возможен, поэтому инкапсуляция в Python держится на договоренности команды и ревью кода.

Наследование и полиморфизм

Наследование — создание класса на основе существующего: наследник получает атрибуты и методы родителя и может их дополнить или переопределить. Полиморфизм — один и тот же вызов работает с объектами разных классов, и каждый отвечает по-своему.

class Animal:
    def __init__(self, name):
        self.name = name

    def speak(self):
        return "..."

    def introduce(self):
        return f"{self.name}: {self.speak()}"


class Cat(Animal):
    def speak(self):                      # переопределение метода
        return "Мяу"


class Dog(Animal):
    def __init__(self, name, breed):
        super().__init__(name)            # родитель заполняет name
        self.breed = breed

    def speak(self):
        return "Гав"


animals = [Cat("Барсик"), Dog("Рекс", "овчарка"), Animal("Некто")]
for a in animals:
    print(a.introduce())                  # один вызов - разное поведение

print(isinstance(animals[0], Animal), issubclass(Dog, Animal))  # True True
print(Dog.__mro__)

Вывод:

Барсик: Мяу
Рекс: Гав
Некто: ...
True True
(<class '__main__.Dog'>, <class '__main__.Animal'>, <class 'object'>)

Метод introduce написан один раз в Animal, но self.speak() внутри него вызывает версию того класса, к которому относится объект. __mro__ (method resolution order) — порядок, в котором Python ищет метод: сначала Dog, потом Animal, потом object.

Ошибка: забыли super().init

Неверно:

class Animal:
    def __init__(self, name):
        self.name = name

class Dog(Animal):
    def __init__(self, name, breed):
        self.breed = breed              # забыли super().__init__(name)

rex = Dog("Рекс", "овчарка")
print(rex.name)

Результат (последняя строка трассировки, Python 3.14):

AttributeError: 'Dog' object has no attribute 'name'

Свой __init__ у наследника заменяет родительский, и тот не вызывается сам. Исправление — первой строкой в __init__ наследника вызвать super().__init__(name), как в классе Dog выше. Подробности инициализации (__init__ и __new__) — отдельная тема; здесь достаточно правила «переопределил __init__ — вызови родительский».

Наследование — не единственный способ переиспользовать код. Если связь звучит как «имеет» (у заказа есть список товаров), а не «является» (собака является животным), обычно лучше композиция: объект хранит другой объект в атрибуте.

dataclass: классы для данных без шаблонного кода

Когда класс в основном хранит поля, __init__, __repr__ и __eq__ можно не писать руками — их сгенерирует декоратор @dataclass из стандартной библиотеки (модуль dataclasses, есть с Python 3.7).

from dataclasses import dataclass, field

@dataclass
class Point:
    x: float
    y: float

@dataclass(frozen=True)
class Money:
    amount: int          # в копейках, чтобы не ловить ошибки округления float
    currency: str = "RUB"

@dataclass
class Order:
    id: int
    items: list[str] = field(default_factory=list)   # свой список у каждого заказа

p = Point(1, 2)
print(p)                          # Point(x=1, y=2)
print(p == Point(1, 2))           # True: __eq__ сгенерирован
o1, o2 = Order(1), Order(2)
o1.items.append("книга")
print(o1, o2)
m = Money(1500)
try:
    m.amount = 0
except Exception as e:
    print(type(e).__name__, e)

Вывод:

Point(x=1, y=2)
True
Order(id=1, items=['книга']) Order(id=2, items=[])
FrozenInstanceError cannot assign to field 'amount'

Три наблюдения. Аннотации типов (x: float) нужны dataclass, чтобы найти поля, но сами типы при создании объекта не проверяются: Point(1, 2) с целыми числами прошел. frozen=True запрещает присваивание полям через обычный синтаксис. А изменяемое значение по умолчанию dataclass сам не пропустит: items: list[str] = [] падает при объявлении класса с ValueError: mutable default <class 'list'> for field items is not allowed: use default_factory — та же ловушка, что с корзиной выше, только пойманная заранее.

Задача Что взять
Объект с поведением и внутренним состоянием Обычный класс
Набор полей с repr и сравнением @dataclass
Неизменяемая запись, ключ словаря @dataclass(frozen=True)
Проверка типов входных данных при создании dataclass сам не проверяет: валидация в __post_init__ или отдельная библиотека

Выводы

  • Класс описывает данные и поведение, объект — конкретный экземпляр; в Python «объект» и «экземпляр» — синонимы.
  • self — соглашение об имени первого параметра, в него Python передает объект; obj.method() равно Class.method(obj).
  • Атрибут класса общий для всех объектов: изменяемые данные (списки, словари) создавайте в __init__.
  • Инкапсуляция в Python держится на соглашениях _name/__name и @property, а не на запретах интерпретатора.
  • Наследник с собственным __init__ должен вызвать super().__init__(); для классов-хранилищ данных удобнее @dataclass.

Где применяется / связь с практикой

Освойте тему на практике

На классах построены почти все популярные Python-библиотеки: модели Django и SQLAlchemy, исключения, представления в веб-фреймворках, тесты в unittest. Чтобы читать такой код и писать свой, мало знать синтаксис class — нужна практика проектирования: какие данные положить в объект, где наследование уместно, а где хватит функции. Системно это разбирают на курсе «Python-разработчик. Базовый уровень» — от основ языка до собственных классов в учебных проектах. Попробовать формат можно на бесплатных открытых уроках Otus.

FAQ

Есть ли в Python интерфейсы и абстрактные классы?
Ключевого слова interface нет. Абстрактный класс делают через модуль abc: наследование от ABC и декоратор @abstractmethod запрещают создать объект, пока наследник не реализует метод. Для проверки «по форме», без наследования, есть typing.Protocol.

Поддерживает ли Python множественное наследование?
Да: class C(A, B):. Порядок поиска методов задает MRO (алгоритм C3), посмотреть его можно через C.__mro__. На практике множественное наследование чаще используют для небольших классов-примесей (mixin).

Чем __str__ отличается от __repr__?
__str__ — строка для пользователя, ее берут print() и str(). __repr__ — строка для разработчика, в идеале похожая на код создания объекта; если __str__ не определен, Python использует __repr__.

OTUS Журнал
Скидка 5% 14-20 сентября на курсы (popup)