Под словом «add» в Python скрываются три разные вещи. set.add() кладёт элемент в множество. list.append() дописывает элемент в конец списка. А __add__() — магический метод, который задаёт, что произойдёт при объект + объект. Дальше в статье — про третий, про перегрузку оператора + в питоне и его напарника __radd__().

Вам нужен какой add?

Три разных add в Python: куда идти с вашей задачей
Три разных add в Python: куда идти с вашей задачей

Развилка занимает полминуты. Определитесь по левой колонке — и переходите туда, куда указывает правая.

Задача Что вызывать Куда идти
Добавить элемент в множество set.add(elem) Короткий разбор ниже, дальше читать не обязательно
Добавить элемент в список list.append(value) То же
Научить свой класс работать с + __add__(), __radd__() Основная часть статьи
Сложить два числа функцией, а не оператором operator.add(a, b) Это обёртка над тем же +

Если вы искали set.add() и list.append()

Документация описывает их одной строкой каждый: set.add(elem, /) — «Add element elem to the set», sequence.append(value, /) — «Append value to the end of the sequence». Оба меняют объект на месте и возвращают None, поэтому писать x = mylist.append(5) бессмысленно — в x ляжет None.

tags = {"python", "оор"} tags.add("классы") tags.add("python") # дубликат молча игнорируется print(len(tags)) # 3 nums = [1, 2] nums.append(3) print(nums) # [1, 2, 3]

Разница между ними принципиальная: множество хранит только уникальные значения и только хэшируемые (неизменяемые) объекты, список — любые и с повторами. Если ваш вопрос был про это — задача закрыта. Про операции со списками у нас есть отдельный разбор: реверс и сортировка списка в Python.

Всё остальное в статье — про магические методы и перегрузку операторов.

ОНЛАЙН-ПРАКТИКУМ
ЗАПУСК нейросети DEEPSEEK R1 ЛОКАЛЬНО НА СВОЕМ КОМПЬЮТЕРЕ
ЧТО БУДЕТ НА ОБУЧЕНИИ?
  • ПОКАЖЕМ, КАК РАЗВЕРНУТЬ МОДЕЛЬ нейросети DEEPSEEK R1 ПРЯМО НА СВОЁМ КОМПЬЮТЕРЕ
  • Где и как применять? Потестируем модель после установки на разных задачах
  • Как дообучить модель под себя?

Что такое магические методы и перегрузка операторов в питоне

Магические методы (их же называют дандер-методами, от double underscore) — это методы с двойным подчёркиванием с обеих сторон имени: __init__, __repr__, __len__, __add__. Вы их почти никогда не вызываете руками — их вызывает сам интерпретатор, когда встречает соответствующую синтаксическую конструкцию.

Перегрузка операторов — это использование таких методов, чтобы +, -, * и остальные знаки работали с объектами ваших классов. В разделе «Emulating numeric types» документации Python перечислен весь набор: __add__, __sub__, __mul__, __matmul__, __truediv__, __floordiv__, __mod__, __divmod__, __pow__, __lshift__, __rshift__, __and__, __xor__, __or__. Формулировка там прямая: «These methods are called to implement the binary arithmetic operations (+, -, *, @, /, //, %, divmod(), pow(), **, <<, >>, &, ^, |)».

Метод ищется на типе, а не на объекте

Тонкость, о которой обычно молчат. Документация формулирует вызов так: чтобы вычислить выражение x + y, где x — экземпляр класса с методом __add__(), вызывается type(x).__add__(x, y). То есть Python идёт за методом в тип, а не в словарь экземпляра. Отсюда следствие, на котором спотыкаются: присвоить магический метод конкретному объекту нельзя.

class Money: pass m = Money() m.__add__ = lambda other: "никогда не вызовется" print(m + m)

Результат:

TypeError: unsupported operand type(s) for +: 'Money' and 'Money'

Отдельный раздел документации, «Special method lookup», объясняет причину: неявный вызов магических методов гарантированно работает, только если метод определён на типе объекта, а не в его словаре экземпляра.

Обратная сторона того же правила: type(v1).__add__(v1, v2) — это ровно то же самое, что v1 + v2. Можно вызвать метод напрямую и убедиться в этом на своём коде.

ОНЛАЙН-ПРАКТИКУМ
ЗАПУСК нейросети DEEPSEEK R1 ЛОКАЛЬНО НА СВОЕМ КОМПЬЮТЕРЕ
ЧТО БУДЕТ НА ОБУЧЕНИИ?
  • ПОКАЖЕМ, КАК РАЗВЕРНУТЬ МОДЕЛЬ нейросети DEEPSEEK R1 ПРЯМО НА СВОЁМ КОМПЬЮТЕРЕ
  • Где и как применять? Потестируем модель после установки на разных задачах
  • Как дообучить модель под себя?

Метод __add__(): синтаксис и первый рабочий пример

__add__() задаёт, что вернёт выражение a + b, когда слева стоит объект вашего класса. Сигнатура из документации — object.__add__(self, other): self — текущий объект, other — то, что стоит справа от плюса.

class YourClass: def __add__(self, other): # логика сложения self и other return результат

Пример: класс Vector

class Vector: def __init__(self, x, y): self.x = x self.y = y def __add__(self, other): if isinstance(other, Vector): return Vector(self.x + other.x, self.y + other.y) return NotImplemented def __repr__(self): return f"Vector({self.x}, {self.y})" v1 = Vector(2, 3) v2 = Vector(4, 5) print(v1 + v2)

Вывод:

Vector(6, 8)

Метод проверяет, что справа тоже Vector, и возвращает новый объект с суммой координат. Не меняет self, не меняет other — это важно, иначе a + b начнёт незаметно портить a. Если вам нужна арифметика векторов целиком, а не только сложение, посмотрите разбор работы с векторами в Python.

Ключевая строка — return NotImplemented

Самая частая ошибка в русскоязычных руководствах — писать в ветке «неподходящий тип» raise TypeError(...). Документация требует другого: «If one of those methods does not support the operation with the supplied arguments, it should return NotImplemented».

Разница не косметическая. Возврат NotImplemented — это сигнал интерпретатору «я не умею, попробуй второй операнд». Он подхватывает сигнал и продолжает разбор выражения. Свой raise TypeError обрывает разбор навсегда, и второй операнд шанса не получает.

Приятный побочный эффект: сообщение об ошибке Python сформирует сам, и оно будет привычного вида.

print(v1 + "строка")

TypeError: unsupported operand type(s) for +: 'Vector' and 'str'

Никакого raise в классе нет, а TypeError всё равно поднялся — потому что после NotImplemented интерпретатор исчерпал варианты и выбросил исключение сам, с правильным текстом и правильными именами типов.

Метод __radd__(): когда объект стоит справа

Порядок разбора выражения a + b
Порядок разбора выражения a + b

__radd__() — отражённая (reflected) версия сложения. Она срабатывает, когда ваш объект оказался справа от плюса, а левый операнд не умеет с ним работать. Классика — 5 + obj: у int нет ни малейшего понятия, как складываться с вашим классом.

Точное условие вызова

Документация формулирует так: отражённые методы «are only called if the operands are of different types, when the left operand does not support the corresponding operation, or the right operand’s class is derived from the left operand’s class». Сноска расшифровывает, что значит «does not support»: «the class has no such method, or the method returns NotImplemented».

Отсюда порядок разбора выражения a + b:

  • Python берёт type(a).__add__ и вызывает type(a).__add__(a, b).
  • Если метода нет или он вернул NotImplemented — вызывается type(b).__radd__(b, a).
  • Если и он вернул NotImplemented — интерпретатор поднимает TypeError.

Пример: класс Weight

class Weight: def __init__(self, kg): self.kg = kg def __add__(self, other): if isinstance(other, Weight): return Weight(self.kg + other.kg) if isinstance(other, (int, float)): return Weight(self.kg + other) return NotImplemented def __radd__(self, other): return self.__add__(other) def __repr__(self): return f"Weight({self.kg})" w = Weight(70) print(w + 5) print(5 + w)

Weight(75) Weight(75)

Сложение здесь коммутативно, поэтому __radd__() просто переадресует вызов в __add__(). Для некоммутативных операций — вычитания, деления, конкатенации строк — так делать нельзя: в __rsub__() порядок операндов обратный, и переадресация даст неверный знак.

Главная причина, по которой __radd__ вообще нужен: sum()

Встроенная функция описана в документации как sum(iterable, /, start=0) — «Sums start and the items of an iterable from left to right». То есть первое, что она делает — складывает ноль с вашим первым элементом. Слева int, справа ваш объект.

print(sum([Weight(70), Weight(30)]))

Weight(100)

Без __radd__() эта строка упала бы с TypeError на самом первом шаге. Именно поэтому у любого класса-числа отражённый метод обязателен.

Цена ошибки: что ломает raise TypeError

Утверждение «raise вместо NotImplemented — это плохой стиль» звучит абстрактно, пока не увидишь поломку. Соберём две версии одного класса и партнёра, который умеет складываться справа.

class BadMoney: def __init__(self, v): self.v = v def __add__(self, other): if isinstance(other, BadMoney): return BadMoney(self.v + other.v) raise TypeError(f"Unsupported type for addition: {type(other)}") class Bonus: def __radd__(self, other): return "бонус применён" try: print(BadMoney(100) + Bonus()) except TypeError as e: print(type(e).__name__ + ":", e)

TypeError: Unsupported type for addition: <class '__main__.Bonus'>

Класс Bonus полностью корректен и готов обработать сложение — но его __radd__() никто не вызвал. raise убил цепочку до того, как интерпретатор до неё добрался. Меняем одну строку:

class GoodMoney(BadMoney): def __add__(self, other): if isinstance(other, GoodMoney): return GoodMoney(self.v + other.v) return NotImplemented print(GoodMoney(100) + Bonus())

бонус применён

Один return вместо одного raise — и чужой класс начинает работать с вашим. Это и есть разница между «мой класс закрыт» и «мой класс участвует в системе типов».

Правило приоритета при наследовании

Есть случай, когда отражённый метод вызывается первым, до __add__() левого операнда. Документация помечает его отдельной заметкой: «If the right operand’s type is a subclass of the left operand’s type and that subclass provides a different implementation of the reflected method for the operation, this method will be called before the left operand’s non-reflected method. This behavior allows subclasses to override their ancestors’ operations».

class Base: def __add__(self, other): return "Base.__add__" def __radd__(self, other): return "Base.__radd__" class Child(Base): def __radd__(self, other): return "Child.__radd__" print(Base() + Child()) print(Base() + Base())

Child.__radd__ Base.__add__

Первая строка: справа подкласс со своей реализацией __radd__ — он выигрывает. Вторая: типы одинаковые, работает обычный порядок. Правило существует, чтобы подкласс мог переопределить арифметику предка, не переписывая базовый класс.

Одинаковые типы: отражённый метод не зовут

Ещё одна деталь, которая экономит часы отладки. Если оба операнда одного типа, __radd__() не вызывается вообще — даже если __add__() вернул NotImplemented. Сноска документации объясняет логику: «For operands of the same type, it is assumed that if the non-reflected method (such as __add__()) fails then the operation is not supported, which is why the reflected method is not called».

class Same: def __add__(self, other): print(" __add__ вызван") return NotImplemented def __radd__(self, other): print(" __radd__ вызван") return NotImplemented Same() + Same()

__add__ вызван TypeError: unsupported operand type(s) for +: 'Same' and 'Same'

Строки «__radd__ вызван» в выводе нет. Не пытайтесь чинить сложение однотипных объектов через отражённый метод — до него не дойдёт.

Метод __iadd__() и оператор +=

Что происходит при a += b
Что происходит при a += b

__iadd__() отвечает за составное присваивание +=. По документации такие методы «should attempt to do the operation in-place (modifying self) and return the result (which could be, but does not have to be, self)». Если метод не определён или вернул NotImplemented, присваивание откатывается к обычной паре: «x.__add__(y) and y.__radd__(x) are considered, as with the evaluation of x + y».

Практическая разница между + и += — в том, создаётся новый объект или меняется старый.

class Basket: def __init__(self, items): self.items = list(items) def __add__(self, other): return Basket(self.items + other.items) def __iadd__(self, other): self.items.extend(other.items) return self def __repr__(self): return f"Basket({self.items})" a = Basket(["хлеб"]) b = a a += Basket(["молоко"]) print(a, b, a is b) c = Basket(["хлеб"]) d = c c = c + Basket(["молоко"]) print(c, d, c is d)

Basket(['хлеб', 'молоко']) Basket(['хлеб', 'молоко']) True Basket(['хлеб', 'молоко']) Basket(['хлеб']) False

После += изменился объект, на который смотрят обе переменные — b «внезапно» тоже получил молоко. После + создался новый объект, старый остался нетронутым. Если ваш класс задуман неизменяемым, __iadd__() определять не нужно вовсе: откат к __add__() даст корректное поведение бесплатно.

Ловушка кортежа: ошибка есть, изменение тоже

Документация прямо предупреждает: «In certain situations, augmented assignment can result in unexpected errors … but this behavior is in fact part of the data model». Вот самая известная из этих ситуаций.

t = ([1], [2]) try: t[0] += [99] except TypeError as e: print(type(e).__name__ + ":", e) print(t)

TypeError: 'tuple' object does not support item assignment ([1, 99], [2])

Читайте вывод внимательно: исключение поднялось — и список всё равно изменился. Потому что t[0] += [99] раскладывается на два шага: сначала list.__iadd__ дописывает элемент на месте (успех), потом интерпретатор пытается записать результат обратно в t[0] (провал, кортеж неизменяем). Первый шаг откатить некому.

Мини-проект: класс CustomNumber

Соберём всё в один класс, который ведёт себя как число: складывается с себе подобными, с int и float, работает в sum() и поддерживает +=.

class CustomNumber: def __init__(self, value): self.value = value def __add__(self, other): if isinstance(other, CustomNumber): return CustomNumber(self.value + other.value) if isinstance(other, (int, float)): return CustomNumber(self.value + other) return NotImplemented def __radd__(self, other): return self.__add__(other) def __iadd__(self, other): result = self.__add__(other) if result is NotImplemented: return NotImplemented self.value = result.value return self def __repr__(self): return f"CustomNumber({self.value})"

Проверка:

a = CustomNumber(5) b = CustomNumber(10) print(a + b) print(a + 2) print(3 + a) print(sum([CustomNumber(5), CustomNumber(10), CustomNumber(1)])) x = CustomNumber(1) x += 4 print(x) a + "10"

CustomNumber(15) CustomNumber(7) CustomNumber(8) CustomNumber(16) CustomNumber(5) TypeError: unsupported operand type(s) for +: 'CustomNumber' and 'str'

Что тут происходит по шагам. a + b — оба CustomNumber, отрабатывает первая ветка. a + 2 — вторая ветка, число прибавляется к value. 3 + aint.__add__(3, a) возвращает NotImplemented, управление уходит в a.__radd__(3). sum() начинает с 0 + CustomNumber(5) — снова отражённый метод. x += 4 идёт в __iadd__() и меняет объект на месте. Последняя строка возвращает NotImplemented из обеих сторон, и Python сам поднимает TypeError с внятным текстом.

Обратите внимание на сравнение result is NotImplemented — именно is, а не if not result.

Что изменилось в Python 3.14

Проверять NotImplemented «на истинность» нельзя, и с версии 3.14 язык за это наказывает. Документация фиксирует: «Changed in version 3.14: Evaluating NotImplemented in a boolean context now raises a TypeError. It previously evaluated to True and emitted a DeprecationWarning since Python 3.9».

if NotImplemented: print("истина")

TypeError: NotImplemented should not be used in a boolean context

Код, который годами тихо работал неправильно (проверка if result: давала True и пропускала «неудачу» дальше), теперь падает явно. Сравнивайте только через is.

Вторая правка того же релиза касается степени: «Changed in version 3.14: Three-argument pow() now try calling __rpow__() if necessary. Previously it was only called in two-argument pow() and the binary power operator».

Сравнительная таблица: три метода сложения

Метод Выражение Когда вызывается Что возвращать
__add__ a + b Всегда первым, у левого операнда Новый объект или NotImplemented
__radd__ a + b У правого операнда, если левый вернул NotImplemented или правый — подкласс левого Новый объект или NotImplemented
__iadd__ a += b Первым при +=; при отсутствии — откат к паре выше self после изменения на месте

Практические рекомендации: чек-лист перед коммитом

  • В ветке «чужой тип» стоит return NotImplemented, а не raise TypeError.
  • __add__() возвращает новый объект и не мутирует self — мутирует только __iadd__().
  • Есть __radd__(), если объект должен переживать sum() и выражения вида число + объект.
  • Для некоммутативных операций отражённый метод написан отдельно, а не переадресован в прямой.
  • NotImplemented проверяется через is, а не в булевом контексте.
  • Метод определён на классе, а не навешен на экземпляр.
  • Есть __repr__() — иначе print() покажет адрес в памяти вместо значения.
  • Реализованы парные операции (__sub__, __mul__, __truediv__), если класс претендует на роль числа.

Разбираться с этим быстрее, когда редактор подсвечивает сигнатуры магических методов: плагины для Visual Studio Code под Python.

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

Что такое add в питоне?

Это три разные сущности. set.add() добавляет элемент в множество, list.append() — в список, а __add__() — магический метод, задающий поведение оператора + для объектов вашего класса.

Чем __add__ отличается от __radd__?

__add__() вызывается у левого операнда выражения a + b. __radd__() вызывается у правого — если левый не поддерживает операцию, то есть не имеет метода или вернул NotImplemented, либо если правый операнд принадлежит подклассу левого.

Почему нужно возвращать NotImplemented, а не вызывать TypeError?

NotImplemented сообщает интерпретатору, что операцию стоит попробовать через второй операнд, — тогда сработает его __radd__(). raise TypeError обрывает разбор выражения сразу, и корректный партнёрский класс не получает шанса. Если оба варианта вернули NotImplemented, Python поднимет TypeError сам, с правильным сообщением.

Зачем нужен __radd__, если сложение и так работает?

Оно работает, только пока ваш объект стоит слева. Выражение 5 + obj и встроенная функция sum(), которая начинает суммирование со start=0, обращаются к отражённому методу. Без него оба варианта падают с TypeError.

Что делает __iadd__ и чем += отличается от +?

__iadd__() обслуживает += и меняет объект на месте, возвращая self. Оператор + создаёт новый объект и старый не трогает. Если __iadd__() не определён, += откатывается к __add__() и __radd__().

Можно ли добавить магический метод к готовому объекту?

Нет. Интерпретатор ищет магические методы на типе: для x + y вызывается type(x).__add__(x, y). Присвоение obj.__add__ = ... в словарь экземпляра игнорируется.

Заключение

Перегрузка операторов делает пользовательские классы полноправными участниками арифметики: объект складывается с объектом, с числом, попадает в sum() и не требует от вызывающего кода помнить про метод add_to(). Механизм держится на трёх методах — __add__(), __radd__(), __iadd__() — и на одном соглашении: возвращать NotImplemented, когда тип не подходит.

Дальше набор расширяется по тому же шаблону: __sub__() и __rsub__() для вычитания, __mul__() и __rmul__() для умножения, __truediv__() и __rtruediv__() для деления. Полный список — в разделе Emulating numeric types официальной документации Python.

Если писать классы с нуля пока тяжело, начните с генерации заготовки и разбора её построчно: создание функций Python с помощью ChatGPT.

РОССИЙСКИЕ НЕЙРОСЕТИ ДЛЯ ЖИЗНИ И КАРЬЕРЫ В 2025
Присоединяйся к онлайн-вебинару.
В прямом эфире разберем и потестируем лучшие на сегодняшний день отечественные ИИ!
Вы узнаете о том:
  • Выполним базовые задачи на российских нейросетях и посмотрим на результаты!
  • Файл-инструкцию «Как сделать нейро-фотосессию из своего фото бесплатно, без иностранных карт и прочих сложностей»
  • Покажем 10+ способов улучшить свою жизнь с ИИ каждому — от ребенка и пенсионера до управленца и предпринимателя
Участвовать бесплатно
ОБЗОРНЫЙ ПРАКТИКУМ ПО НАШУМЕВШИМ НЕЙРОСЕТЯМ
Нейросети DEEPSEEK И QWEN
За 2 часа сделаем полный обзор новых мощных ИИ-моделей, которые бросают вызов нейросети ChatGPT
Вы узнаете:
  • Возможность получить Доступ в Нейроклуб на целый месяц
  • Как ИИ ускоряет работу и приносит деньги
  • За 2 часа вы получите четкий план, как начать работать с ИИ прямо сейчас!
Участвовать бесплатно

Большой практикум
ЗАМЕНИ ВСЕ НЕЙРОСЕТИ НА ОДНУ — PERPLEXITY
ПОКАЖЕМ НА КОНКРЕТНЫХ КЕЙСАХ
  • Освой нейросеть Perplexity и узнай, как пользоваться функционалом остальных ИИ в одном
  • УЧАСТВОВАТЬ ЗА 0 РУБ.
  • Расскажем, как получить подписку
Участвовать бесплатно
ОНЛАЙН-ПРАКТИКУМ
ЗАПУСК нейросети DEEPSEEK R1 ЛОКАЛЬНО НА СВОЕМ КОМПЬЮТЕРЕ
ЧТО БУДЕТ НА ОБУЧЕНИИ?
  • ПОКАЖЕМ, КАК РАЗВЕРНУТЬ МОДЕЛЬ нейросеть DEEPSEEK R1 ПРЯМО НА СВОЁМ КОМПЬЮТЕРЕ
Участвовать бесплатно