Конструкторы в Python: как работать с __init__

Конструкторы в Python: как работать с __init__ Полезное

__init__ в Python — это специальный метод класса, который автоматически вызывается сразу после создания объекта и заполняет его начальными данными (атрибутами). В разговоре его называют конструктором, но строго говоря это инициализатор: сам объект к моменту вызова __init__ уже создан методом __new__, а __init__ только настраивает его и ничего не возвращает.

Ниже — как написать __init__, зачем нужен self, как задавать параметры по умолчанию и не попасть в ловушку с изменяемым значением, как вызывать super().__init__ при наследовании и когда вообще нужен __new__. Все примеры прогнаны на Python 3.14.7 (24.09.2026).

Три понятия, которые путают

Понятие Что делает Когда вызывается Что возвращает
Вызов класса Point(1, 2) запускает создание объекта целиком когда вы его пишете готовый объект
__new__(cls, ...) создает пустой объект (выделяет его) первым новый объект
__init__(self, ...) заполняет уже созданный объект вторым, если __new__ вернул экземпляр этого класса только None

Упрощенная модель: вызов класса = __new__, затем __init__. Точнее этот порядок задает метакласс type, и у модели есть граница: если __new__ вернет объект другого класса, __init__ не вызовется (подробнее ниже). В обычном прикладном коде достаточно написать только __init__.

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

class BankAccount:
    bank_name = "Otus Bank"          # атрибут класса: общий для всех объектов

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

    def deposit(self, amount):
        self.balance += amount


a = BankAccount("Анна", 100)
b = BankAccount("Борис")             # balance возьмет значение по умолчанию
a.deposit(50)
print(a.owner, a.balance)
print(b.owner, b.balance)
print(a.bank_name, b.bank_name)

В консоли появятся три строки:

Анна 150
Борис 0
Otus Bank Otus Bank

Разбор по строкам:

  • def __init__(self, owner, balance=0) — два подчеркивания с каждой стороны обязательны. Написали _init_ или __int__ — Python не узнает метод, и аргументы при создании объекта упадут с TypeError.
  • self.owner = owner создает атрибут экземпляра: у a и b он разный.
  • bank_name объявлен в теле класса, а не в __init__, поэтому это атрибут класса, общий для всех объектов.
  • Проверка if balance < 0 — типичная работа __init__: не пустить в программу объект в неверном состоянии. Ошибку лучше поднять сразу, чем потом искать, откуда взялся отрицательный баланс.

Что такое self

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

class Point:
    def __init__(self, x, y):
        print("id(self) =", id(self))
        self.x = x
        self.y = y

p = Point(1, 2)
print("id(p)    =", id(p))
print(p.x, p.y)

try:
    Point(1)
except TypeError as e:
    print(e)
id(self) = 277188087235984
id(p)    = 277188087235984
1 2
Point.__init__() missing 1 required positional argument: 'y'

Числа id у вас будут другими, но совпадут между собой: self внутри __init__ и p снаружи — один и тот же объект. Последняя строка показывает, что параметры __init__ — это параметры вызова класса: не передали y — получили TypeError с именем недостающего аргумента.

Параметры по умолчанию и ловушка со списком

Значение по умолчанию вычисляется один раз — при определении функции, а не при каждом вызове. Для чисел и строк это незаметно, а изменяемый объект (список, словарь) окажется общим для всех экземпляров.

Неверно:

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

c1 = Cart()
c2 = Cart()
c1.items.append("книга")
print(c2.items)
print(c1.items is c2.items)

Фактический результат — товар «появился» во второй корзине:

['книга']
True

Исправление — None как маркер и новый список внутри __init__:

class Cart:
    def __init__(self, items=None):
        self.items = [] if items is None else list(items)

c1 = Cart()
c2 = Cart()
c1.items.append("книга")
print(c2.items)
print(c1.items is c2.items)
[]
False

list(items) заодно делает копию переданного списка, чтобы корзина не зависела от внешней переменной. Нужна ли копия — решение по задаче; если объект должен разделять данные с вызывающим кодом, копировать не надо.

__init__ ничего не возвращает

Метод обязан вернуть None (просто не писать return со значением). Попытка вернуть что-то другое падает при создании объекта:

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

try:
    Temperature(20)
except TypeError as e:
    print(type(e).__name__, e)
TypeError __init__() should return None, not 'Temperature'

Исправление — удалить return self: объект вернет сам вызов Temperature(20).

Несколько конструкторов: перегрузки нет

В отличие от Java или C#, в Python нельзя объявить два __init__ с разными параметрами. Второе определение просто заменяет первое:

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

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

try:
    User("Анна")
except TypeError as e:
    print(e)
User.__init__() missing 1 required positional argument: 'age'

Вместо перегрузки используют параметры по умолчанию или альтернативные конструкторы через @classmethod. Они разбирают вход и вызывают основной __init__ через cls(...), поэтому проверки остаются в одном месте и корректно работают у наследников:

from datetime import date

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

    @classmethod
    def from_string(cls, text):
        name, year = text.split(",")
        return cls(name.strip(), int(year))

    @classmethod
    def from_age(cls, name, age, today=None):
        today = today or date(2026, 9, 24)
        return cls(name, today.year - age)

p1 = Person("Анна", 1998)
p2 = Person.from_string("Борис, 2001")
p3 = Person.from_age("Вера", 30)
for p in (p1, p2, p3):
    print(p.name, p.birth_year)
Анна 1998
Борис 2001
Вера 1996

Дата в from_age зафиксирована, чтобы вывод воспроизводился; в реальном коде по умолчанию берут date.today(). Так устроены и встроенные примеры вроде dict.fromkeys и datetime.fromisoformat.

Наследование: super().__init__

Если у дочернего класса нет своего __init__, Python возьмет родительский. Если есть — родительский не вызывается автоматически, и атрибуты родителя не появятся:

class Employee:
    def __init__(self, name, salary):
        self.name = name
        self.salary = salary

class Manager(Employee):
    def __init__(self, name, salary, team):
        self.team = team              # super().__init__ забыли

m = Manager("Анна", 300000, ["Борис"])
print(m.team)
try:
    print(m.name)
except AttributeError as e:
    print(e)
['Борис']
'Manager' object has no attribute 'name'

Ошибка всплывает не при создании объекта, а позже, при первом обращении к атрибуту, поэтому ее легко пропустить. Исправление — явно вызвать инициализатор родителя:

class Employee:
    def __init__(self, name, salary):
        print("Employee.__init__")
        self.name = name
        self.salary = salary

class Manager(Employee):
    def __init__(self, name, salary, team):
        super().__init__(name, salary)
        print("Manager.__init__")
        self.team = team

class Intern(Employee):
    pass                              # своего __init__ нет

m = Manager("Анна", 300000, ["Борис"])
print(m.name, m.salary, m.team)
i = Intern("Вера", 80000)
print(i.name)
Employee.__init__
Manager.__init__
Анна 300000 ['Борис']
Employee.__init__
Вера

Порядок решаете вы: super().__init__ обычно ставят первой строкой, чтобы код ниже мог опираться на атрибуты родителя. super() лучше, чем Employee.__init__(self, ...): при множественном наследовании он идет по MRO (порядку разрешения методов) и не вызывает общий базовый класс дважды — при условии, что все классы иерархии тоже вызывают super().__init__, а не родителя по имени.

__new__ и __init__: когда нужен __new__

Порядок вызова легко увидеть:

class Demo:
    def __new__(cls, *args, **kwargs):
        print("1. __new__: создаю объект", cls.__name__)
        instance = super().__new__(cls)
        return instance

    def __init__(self, value):
        print("2. __init__: заполняю объект, value =", value)
        self.value = value

d = Demo(42)
print("3. готово:", d.value)
1. __new__: создаю объект Demo
2. __init__: заполняю объект, value = 42
3. готово: 42

Переопределять __new__ нужно редко. Главный практический случай — наследование от неизменяемых типов (int, float, str, tuple): их значение задается при создании, и в __init__ его уже не поменять.

class Celsius(float):
    def __new__(cls, value):
        if value < -273.15:
            raise ValueError("ниже абсолютного нуля")
        return super().__new__(cls, round(value, 1))

t = Celsius(36.649)
print(t, isinstance(t, float))

class Broken(float):
    def __init__(self, value):
        self = round(value, 1)       # переприсваивание локального имени

print(Broken(36.649))
36.6 True
36.649

В Broken округление не сработало: self = ... меняет только локальную переменную, а число уже создано float.__new__.

Граница, о которой говорилось в таблице: если __new__ вернул не экземпляр своего класса (например, строку), __init__ пропускается.

Singleton через __new__: __init__ вызывается каждый раз

Частый учебный пример — «одиночка», у которого __new__ возвращает уже созданный объект. Подвох: раз объект — экземпляр класса, __init__ отрабатывает при каждом вызове и перезаписывает состояние.

class Config:
    _instance = None

    def __new__(cls, *args, **kwargs):
        if cls._instance is None:
            cls._instance = super().__new__(cls)
        return cls._instance

    def __init__(self, env):
        print("__init__ вызван, env =", env)
        self.env = env

a = Config("prod")
b = Config("test")
print(a is b, a.env)
__init__ вызван, env = prod
__init__ вызван, env = test
True test

Настройка prod тихо превратилась в test. Если одиночка действительно нужен, в Python чаще хватает объекта на уровне модуля; иначе в __init__ нужен флаг «уже инициализирован». Этот вариант не потокобезопасен: при одновременном первом вызове из двух потоков нужна блокировка.

Когда __init__ можно не писать: dataclass

Для классов, которые в основном хранят данные, модуль dataclasses генерирует __init__ сам по аннотациям полей. Дополнительную проверку кладут в __post_init__, изменяемые значения по умолчанию — через field(default_factory=...) (это решение той же ловушки со списком):

from dataclasses import dataclass, field

@dataclass
class Order:
    order_id: int
    items: list[str] = field(default_factory=list)
    total: float = 0.0

    def __post_init__(self):
        if self.total < 0:
            raise ValueError("total < 0")

o = Order(1, ["книга"], 990.0)
print(o)
print(Order(2))
Order(order_id=1, items=['книга'], total=990.0)
Order(order_id=2, items=[], total=0.0)

Аннотации в dataclass не проверяют типы при выполнении: Order("1") создастся без ошибки. Для проверки типов нужен статический анализатор (mypy, pyright) или явная проверка в __post_init__.

Если не получилось: симптом -> причина

Симптом Вероятная причина Что сделать
TypeError: Point() takes no arguments метод назван с ошибкой (_init_, __int__) и не определен проверить имя: два подчеркивания с обеих сторон
missing 1 required positional argument при создании не передан аргумент __init__ передать его или задать значение по умолчанию
P.__init__() takes 1 positional argument but 2 were given в определении забыли self первым параметром поставить self
'X' object has no attribute ... у наследника не вызван super().__init__ добавить вызов в начало __init__
should return None в __init__ есть return значение убрать возврат
данные «перетекают» между объектами изменяемое значение по умолчанию None + новый список внутри

Выводы

  • __init__ — инициализатор: заполняет уже созданный объект атрибутами и возвращает только None; создает объект __new__.
  • self — это сам создаваемый объект, Python передает его первым аргументом автоматически.
  • Изменяемые значения по умолчанию ([], {}) в параметрах __init__ общие для всех объектов — используйте None или default_factory.
  • Перегрузки __init__ нет: вместо нее параметры по умолчанию и альтернативные конструкторы через @classmethod.
  • В наследнике со своим __init__ родительский нужно вызвать явно через super().__init__; __new__ переопределяют в редких случаях, например для подклассов int/str/float.

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

Инициализация объектов — основа любого кода на классах: модели в Django и SQLAlchemy, dataclass-конфиги, клиенты API, обработчики в тестах. Ошибки с super().__init__ и изменяемыми значениями по умолчанию регулярно встречаются на ревью и собеседованиях джунов.

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

Если хочется разобрать классы, наследование и работу с объектами на практических задачах с проверкой кода, посмотрите курс «Python-разработчик. Базовый уровень». Бесплатно познакомиться с преподавателями и форматом можно на открытых уроках Otus.

FAQ

Можно ли вызвать __init__ повторно на готовом объекте?
Технически да (obj.__init__(...)), и атрибуты перезапишутся, но это читается как ошибка. Для сброса состояния лучше завести отдельный метод, например reset().

Есть ли в Python деструктор?
Есть метод __del__, но момент его вызова не гарантирован и зависит от сборщика мусора. Для освобождения файлов и соединений используют with и контекстные менеджеры.

Где объявлять атрибуты: в __init__ или в теле класса?
Данные конкретного объекта — в __init__ через self. В теле класса — то, что общее для всех экземпляров (константы, счетчики); изменяемый список в теле класса станет общим, как и в параметре по умолчанию.

OTUS Журнал
Скидка 5% 12-19 октября на курсы (popup)
Бесплатные открытые уроки (поп-ап)