Классы в Python: self, атрибуты, методы и экземпляры

Python Автор: Среда и версия: CPython 3.14.7; исполняемые блоки — Pyodide 3.14.0 прямо на странице
содержание

Класс — это описание того, из чего состоит объект и что он умеет. Экземпляр — конкретный объект, сделанный по этому описанию. Один класс Cart, сколько угодно корзин, у каждой свой владелец и свой список товаров.

class Cart:
    def __init__(self, owner):
        self.owner = owner
        self.items = []

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


cart = Cart("Аня")
cart.add("мышь")
print(cart.owner, cart.items)
Аня ['мышь']

self — не магия, а первый аргумент

self смущает потому, что при вызове его не пишут. Но он никуда не девается: Python подставляет его сам, и в этом легко убедиться — вызвать метод можно обоими способами, результат одинаковый.

class Cart:
    def __init__(self, owner):
        self.owner = owner
        self.items = []

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


cart = Cart("Аня")
cart.add("мышь")           # обычный вызов
Cart.add(cart, "клавиатура")   # то же самое, но self передан руками
print(cart.items)
['мышь', 'клавиатура']

Разница видна, если напечатать сам метод:

class Cart:
    def add(self, item):
        pass


cart = Cart()
print(type(cart.add).__name__)
print(type(Cart.add).__name__)
method
function

Из класса add — обычная функция. Из экземпляра — «связанный метод»: та же функция, к которой уже привязан первый аргумент. Поэтому self не ключевое слово: его можно назвать как угодно, просто все договорились называть self, и отступать от этого не стоит.

Где живут атрибуты

Атрибут, присвоенный через self внутри метода, принадлежит экземпляру. Атрибут, записанный прямо в теле класса, принадлежит классу и общий для всех.

Python / 01

Два экземпляра — два списка покупок

Два экземпляра — два списка покупок01 Общее у класса Basket.vat = 0.2 Атрибут класса доступен обоим экземплярам. 02 Корзина Ани a.items = [] Список создаётся в __init__: принадлежит a. 03 Корзина Бориса b.items = [] Отдельный список принадлежит b.01Общее у классаBasket.vat =0.2Атрибут класса доступенобоим экземплярам.02Корзина Аниa.items = []Список создаётся в __init__:принадлежит a.03Корзина Борисаb.items = []Отдельный списокпринадлежит b.
Поиск атрибута идёт снизу вверх: сначала в экземпляре, потом в классе. Поэтому a.vat_rate находит общее значение, а a.owner — своё.

Общий атрибут на месте — это удобно для констант:

class Order:
    vat_rate = 0.20

    def __init__(self, amount):
        self.amount = amount

    def with_vat(self):
        return round(self.amount * (1 + self.vat_rate), 2)


print(Order(1000).with_vat(), Order(2500).with_vat())
1200.0 3000.0

Ловушка: изменяемый атрибут класса

А вот если общим по недосмотру окажется список, начнётся странное:

class Basket:
    items = []            # ← в теле класса, а не в __init__

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

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


a, b = Basket("Аня"), Basket("Борис")
a.add("мышь")
print("у Ани:  ", a.items)
print("у Бориса:", b.items)
print("это один и тот же список:", a.items is b.items)
у Ани:   ['мышь']
у Бориса: ['мышь']
это один и тот же список: True

Борис ничего не клал, но товар у него есть: список создан один раз при описании класса, и self.items.append(...) меняет именно его. Тот же класс ошибок, что и изменяемое значение по умолчанию у функции — и та же причина: изменяемый объект создан один раз, а используется многими.

Увидеть, где что лежит, можно напрямую:

class Basket:
    items = []

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


a = Basket("Аня")
print("в экземпляре:", a.__dict__)
print("в классе:    ", "items" in Basket.__dict__)
в экземпляре: {'owner': 'Аня'}
в классе:     True

Чинится переносом в __init__ — тогда список создаётся заново на каждый экземпляр:

class Basket:
    def __init__(self, owner):
        self.owner = owner
        self.items = []       # ← своё для каждого

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


a, b = Basket("Аня"), Basket("Борис")
a.add("мышь")
print("у Ани:  ", a.items)
print("у Бориса:", b.items)
у Ани:   ['мышь']
у Бориса: []

Правило простое: в теле класса — только неизменяемое (числа, строки, кортежи). Всё, у чего есть методы изменения на месте, создавай в __init__.

init — не конструктор, а настройщик

__init__ вызывается уже после того, как объект создан: его задача — разложить по местам начальные значения, а не вернуть объект. Поэтому return в нём не пишут.

Атрибуты можно добавлять и потом, в любой момент, и Python не возразит:

class Point:
    def __init__(self, x):
        self.x = x


p, q = Point(1), Point(2)
p.color = "красный"
print(p.__dict__)
print("у q такого атрибута нет:", hasattr(q, "color"))
{'x': 1, 'color': 'красный'}
у q такого атрибута нет: False

Гибко, но опасно: опечатка в имени атрибута создаст новый вместо того, чтобы упасть. Если такое поведение мешает, помогает __slots__ или @dataclass(slots=True). Обычный @dataclass без slots=True не запрещает новые атрибуты.

Печать объекта

По умолчанию объект печатается неинформативно:

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


print(Cart("Аня"))
<__main__.Cart object at 0x7fad5f290d70>

Шестнадцатеричное число — адрес в памяти, у тебя будет другое. Чтобы объект печатался по-человечески, добавь __repr__:

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

    def __repr__(self):
        return f"Cart(owner={self.owner!r})"


print(Cart("Аня"))
print([Cart("Аня"), Cart("Борис")])
Cart(owner='Аня')
[Cart(owner='Аня'), Cart(owner='Борис')]

__repr__ работает и внутри списков, и в отладчике, и в логах — одна строка кода экономит часы на диагностике. Модификатор !r в f-строке добавляет кавычки, чтобы было видно, что это строка.

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

class Temperature:
    def __init__(self, celsius):
        self.celsius = celsius

    def to_fahrenheit(self):              # обычный: работает с объектом
        return self.celsius * 9 / 5 + 32

    @classmethod
    def from_fahrenheit(cls, f):          # классовый: создаёт объект
        return cls((f - 32) * 5 / 9)

    @staticmethod
    def is_freezing(celsius):             # статический: просто функция рядом
        return celsius < 0


print(Temperature(20).to_fahrenheit())
print(Temperature.from_fahrenheit(68).celsius)
print(Temperature.is_freezing(-5))
68.0
20.0
True

Обычный метод получает self и работает с конкретным объектом. @classmethod получает сам класс и чаще всего служит альтернативным способом создать объект. @staticmethod не получает ничего — это функция, положенная внутрь класса из соображений порядка. Про то, как устроены сами @-обёртки, есть разбор декораторов.

Когда класс не нужен

  1. Данные без поведения — не классЕсли у объекта только поля и ни одного осмысленного метода, это словарь, кортеж или dataclass. Класс с одним __init__ ничего не добавляет.
  2. Одна операция — это функцияКласс с единственным методом и без состояния между вызовами — переодетая функция. Признак: экземпляр создают и тут же выбрасывают.
  3. Разобранный JSON — уже словарьОборачивать в класс данные, которые пришли извне и уходят наружу без обработки, незачем. Подробнее — в разборе работы с JSON.
  4. Класс оправдан состояниемКогда между вызовами что-то нужно помнить — накопленные товары, открытое соединение, позиция в файле — вот тогда класс на месте.

Частые вопросы

Что такое self и можно ли его не писать

self — это ссылка на сам объект, первый параметр каждого обычного метода. При вызове через точку Python подставляет его автоматически, но в определении писать обязательно, иначе аргументы сдвинутся и вызов упадёт с TypeError.

Чем класс отличается от экземпляра

Класс — описание, экземпляр — сделанный по нему объект. Cart один, Cart("Аня") и Cart("Борис") — два разных объекта со своими данными.

Почему у всех объектов один и тот же список

Потому что он создан в теле класса, а не в __init__. Проверить: "items" in Basket.__dict__ вернёт True, если атрибут на классе. Перенеси создание списка в __init__.

Зачем нужен init, если атрибуты можно присвоить потом

Чтобы объект сразу был готов к работе и нельзя было забыть обязательное поле. Присваивание задним числом молча создаёт новый атрибут даже при опечатке, а __init__ задаёт минимальный набор один раз и в одном месте.

Что печатается вместо объекта

<__main__.Cart object at 0x…> — имя класса и адрес в памяти. Так выглядит объект без __repr__. Добавь метод __repr__, возвращающий строку, и объект станет читаемым везде, включая списки и логи.

Где потренироваться

Блоки на этой странице запускаются прямо здесь — поменяй items = [] местами с __init__ и посмотри, как меняется поведение. На живых задачах классы разбираются в пути «ООП на Python», а следующий шаг после ручных классов — dataclass, который пишет __init__ и __repr__ за тебя.

Источники