Перейти к содержанию

232 уроков, 16 библиотек и челлендж «Что выведет код?» — бесплатно, код прямо в браузере

Начать обучение
Урок 13 из 20 Средний 35 мин 130 XP

Перегрузка операторов: __add__ и __getitem__

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

Редакция Питоники

Плюс между строками склеивает их, между списками — объединяет, между числами — складывает: знак один, поведение зависит от типов. За каждым таким знаком стоит магический метод: + — это __add__, [] — __getitem__, len() — __len__. Определив их в своём классе, вы научите вектор складываться с вектором, а заказ — объединяться с заказом. Сравнение объектов вы уже умеете переопределять — теперь очередь арифметики и индексации.

__add__: свой плюс

Выражение a + b интерпретатор читает как a.__add__(b). Если метод вернул NotImplemented, Python пробует зеркальный b.__radd__(a) — reflected-операцию, — а если и там неудача, поднимает TypeError. __radd__ особенно важен для встроенной sum(): она начинает с нуля, то есть первым вызовом будет именно 0 + vector.

вектор умеет складываться и работать в sum()
class Vector:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __repr__(self):
        return f"Vector({self.x}, {self.y})"

    def __add__(self, other):
        if not isinstance(other, Vector):
            return NotImplemented
        return Vector(self.x + other.x, self.y + other.y)

    def __radd__(self, other):
        if other == 0:
            return self
        return self.__add__(other)

start = Vector(2, 3)
finish = Vector(10, -1)

print(start + finish)
print(sum([start, finish]))
Вывод
Vector(12, 2)
Vector(12, 2)

sum() стартует с аккумулятора 0: первый шаг — 0 + start. У int нет представления о векторах, поэтому int.__add__ возвращает NotImplemented, и Python обращается к зеркальному __radd__: тот видит other == 0 и возвращает сам вектор. Дальше уже работает обычный __add__. Несовместимый тип без зеркальной поддержки даёт честную ошибку:

TypeError с точным перечислением типов
class Vector:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __add__(self, other):
        if not isinstance(other, Vector):
            return NotImplemented
        return Vector(self.x + other.x, self.y + other.y)

try:
    Vector(1, 2) + 5
except TypeError as error:
    print("Ошибка:", error)
Вывод
Ошибка: unsupported operand type(s) for +: 'Vector' and 'int'

__getitem__ и __len__: индексация и срезы

Метод __getitem__ обслуживает не только obj[2], но и срезы: в playlist[1:3] в метод приходит объект slice, и обычно его просто передают внутреннему списку. __len__ подключает функцию len(). Бонус: объект с __getitem__, который бросает IndexError на границе, можно перебирать циклом for даже без __iter__ — Python сам подставляет индексы с нуля.

плейлист отвечает на len, индексы и срезы
class Playlist:
    def __init__(self, tracks):
        self.tracks = list(tracks)

    def __len__(self):
        return len(self.tracks)

    def __getitem__(self, index):
        return self.tracks[index]

playlist = Playlist(["рассвет", "ветер", "прибой", "север"])
print(len(playlist))
print(playlist[0])
print(playlist[-1])
print(playlist[1:3])
Вывод
4
рассвет
север
['ветер', 'прибой']
СинтаксисМагический методПример
a + b__add__vector1 + vector2
a - b__sub__vector1 - vector2
b + a, когда слева встроенный тип__radd__0 + vector
obj[i], obj[a:b]__getitem__playlist[1:3]
obj[i] = x__setitem__playlist[0] = "ветер"
len(obj)__len__len(playlist)
Что выведет код?

Сначала предскажи ответ в голове — это главный навык программиста.

class Money:
    def __init__(self, rubles):
        self.rubles = rubles

    def __repr__(self):
        return f"Money({self.rubles})"

    def __add__(self, other):
        return Money(self.rubles + other.rubles)

print(Money(120) + Money(80))
class Shelf:
    def __init__(self, items):
        self.items = items

    def __getitem__(self, index):
        return self.items[index]

shelf = Shelf(["кружка", "тарелка", "стакан"])
print(shelf[-2])
class Temp:
    def __init__(self, celsius):
        self.celsius = celsius

    def __add__(self, other):
        return Temp(self.celsius + other.celsius)

    def __repr__(self):
        return f"{self.celsius}°C"

print(Temp(10) + Temp(15))
Проверь себя
0 / 5

1. Как интерпретируется выражение a + b?

2. Что означает возвращённый из __add__ NotImplemented?

3. В какой метод приходит срез playlist[1:3]?

4. Что нужно определить, чтобы заработала функция len(obj)?

5. Зачем в __radd__ проверка other == 0?

Карточки терминов
Запомнено: 0 / 6
Практика

Создайте класс Order со списком позиций items. Определите __add__, который возвращает новый Order из позиций обоих заказов (исходные не меняются), и __len__, возвращающий число позиций. Сложите заказ «хлеб, молоко» и заказ «сыр, кофе», выведите len результата и его items.

practice.py
Вопросы и ответы по уроку

Что такое перегрузка операторов в Python?

Это определение поведения встроенных операций для своих классов: + работает через __add__, [] — через __getitem__, len() — через __len__. Язык не меняется — вы лишь сообщаете своим объектам, как реагировать на знакомый синтаксис.

Урок 12: __eq__ и __hash__

Почему в __add__ нельзя возвращать False при несовместимых типах?

False — легитимный результат операции, Python прекратит поиск и вернёт его как ответ: sum() получит мусор, а TypeError не поднимется. Правильный ответ — NotImplemented: он запускает зеркальный метод и лишь затем честную ошибку.

Как работают срезы через __getitem__?

Срез playlist[1:3] приходит в __getitem__ объектом slice с атрибутами start, stop и step. Обычно его просто передают внутреннему списку: return self.tracks[index] — список сам умеет обрабатывать slice.

Что такое reflected-операции (__radd__ и друзья)?

Это методы с префиксом r: они вызываются, когда левый операнд не справился (вернул NotImplemented). Например, 0 + vector вызывает vector.__radd__(0). Проверка other == 0 в __radd__ — стандартный способ сделать свои объекты пригодными для sum().

Понравился урок? Сошлитесь на него

Скопируйте готовую ссылку в формате HTML, Markdown или чистый адрес и вставьте в статью на Habr, VC, Telegram-канал или свой блог — так о проекте узнают новые читатели.

TelegramVK

Похожие уроки по темам

Подобраны автоматически по пересечению тем и ключевых слов.