ООП в 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
Что здесь происходит по шагам:
class BankAccount:создает класс. В Python 3 любой класс неявно наследуется отobject, писатьclass BankAccount(object)не нужно.BankAccount("Анна")создает объект и вызывает__init__, который заполняет атрибуты экземпляра. Это инициализатор (его часто называют конструктором); здесь он нужен только чтобы задать стартовые данные.anna.deposit(300)меняет состояние именно объектаanna: уborisбаланс остался 500.__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__.



