📦 Репозиторий занятия 78

Урок 78. Магические методы

Как работать с репозиторием

Каждая карточка говорит, о чём файл, что он выводит и что в нём искать. Код виден прямо здесь: его можно скопировать одной кнопкой, скачать файл или открыть его целиком.

Маршрут изучения

  1. Прочитайте описание: по нему уже понятно, о чём файл и что он выведет.
  2. Предскажите вывод: сравните своё предположение со строкой «Что выводит».
  3. Запустите: скачайте файл или скопируйте код кнопкой и выполните его у себя.
  4. Измените: поменяйте одно условие или значение и объясните новый результат.

Файлы: рекомендуемый порядок

1
MarkdownРазбор концепции146 строк

Магические методы сравнения и декоратор @total_ordering

less_40__magic_methods/theory_01__comparison_methods.md

Таблица шести магических методов сравнения (__lt__, __le__, __gt__, __ge__, __eq__, __ne__) и декоратор functools.total_ordering, который достраивает недостающие методы, если реализованы только __eq__ и один из порядковых методов. Основная часть посвящена NotImplemented — специальному значению (не исключению), которое сигнализирует «я не умею сравнивать себя с этим типом» и заставляет Python попробовать обратный метод у другого операнда; если оба объекта возвращают NotImplemented — TypeError. Финальный пример показывает Person и Employee, изначально не умеющие сравниваться друг с другом, где через return NotImplemented Python сам переключается на other.__lt__(self), позволяя реализовать сравнение только в одном из двух классов.

  • Таблица: <, <=, >, >=, ==, != → __lt__/__le__/__gt__/__ge__/__eq__/__ne__
  • @total_ordering: реализовать __eq__ + один из порядковых — остальные достроятся
  • NotImplemented — не исключение, сигнал попробовать обратный метод
  • return NotImplemented вместо raise TypeError — не ломает возможность other.__gt__(self)
  • Пример Person/Employee: p > e работает через p.__gt__(e) → NotImplemented → e.__lt__(p)
Показать начало файла (146 строк всего)
### Методы сравнения в Python

Чтобы объекты пользовательского класса можно было сравнивать, нужно определить специальные методы:

| Оператор | Спецметод             | Значение         |
| -------- | --------------------- | ---------------- |
| `<`      | `__lt__(self, other)` | меньше           |
| `<=`     | `__le__(self, other)` | меньше или равно |
| `>`      | `__gt__(self, other)` | больше           |
| `>=`     | `__ge__(self, other)` | больше или равно |
| `==`     | `__eq__(self, other)` | равно            |
| `!=`     | `__ne__(self, other)` | не равно         |

---

### Декоратор `@functools.total_ordering`

* Если добавить в класс декоратор `@functools.total_ordering`
* то достаточно реализовать один метод `__eq__`
* и один любой метод сравнения (`__lt__`, `__le__`, `__gt__`, `__ge__`).

Остальные 4 метода "добавит" сам декоратор.

---

#### Пример

```python
from functools import total_ordering

@total_ordering
class Person:
    def __init__(self, age):
        self.age = age

    def __eq__(self, other):
        return self.age == other.age

    def __lt__(self, other):
        return self.age < other.age


p1 = Person(30)
p2 = Person(25)

print(p1 > p2)   # True
print(p1 <= p2)  # False
print(p1 == p2)  # False
```

Мы реализовали только:

* `__eq__`
* `__lt__`

Остальные (`>`, `>=`, `<=`) создал `total_ordering`.

---

### Исключение `NotImplemented`
…
Проверьте себя: Почему в финальном примере `p > e` возвращает True, если у класса Person метод __gt__ явно отказывается сравнивать себя с не-Person через NotImplemented?
2
MarkdownРазбор концепции199 строк

Методы индексации: __getitem__, __setitem__, __delitem__, __len__, __contains__ и срезы

less_40__magic_methods/theory_02__indexing_and_element_access_methods.md

Последовательно разбирает пять dunder-методов, дающих объекту поведение коллекции: __getitem__ для v[key], __setitem__ для v[key]=value, __delitem__ для del v[key], __len__ для len(v), __contains__ для in. Отдельный раздел о срезах показывает, что при обращении v[1:4] Python передаёт в __getitem__ не индекс, а объект slice(1, 4, None), который нужно проверять через isinstance(key, slice). В конце — единый класс Vector со всеми методами сразу и развёрнутая демонстрация каждого метода по очереди, включая отрицательный шаг среза v[::-1].

  • __getitem__(self, key) — v[1] читает элемент, поддерживает и индекс, и slice
  • __setitem__(self, key, value) — v[1] = 99 и срезовое присваивание v[1:3] = [...]
  • __delitem__(self, key) — del v[1] и del v[1:3]
  • __len__(self) — len(v)
  • __contains__(self, item) — 20 in v / 15 not in v
  • isinstance(key, slice) внутри __getitem__ — обработка v[::2], v[::-1] через создание нового Vector

Что выводит: 20; затем 'slice(1, 4, None)' и Vector([20, 30, 40]). Далее Vector([10, 99, 30]) и Vector([10, 40, 50]) после setitem. Vector([10, 30, 40]) и Vector([10]) после delitem. 3 для len(v). True, False, True для contains. Для срезов: slice(1, 5, None) → Vector([10, 20, 30, 40]); slice(None, None, 2) → Vector([0, 20, 40, 60]); slice(None, None, -1) → Vector([60, 50, 40, 30, 20, 10, 0]). Все значения проверены запуском кода и совпадают с комментариями в файле.

Показать начало файла (199 строк всего)
## Методы индексации и доступа к элементам

### Содержание

| № | Метод                       | Назначение                       | 
|---|-----------------------------|----------------------------------|
| 1 | [`__getitem__`](#getitem)   | Чтение элемента `obj[key]`       |           
| 2 | [`__setitem__`](#setitem)   | Присваивание `obj[key] = value`  |         
| 3 | [`__delitem__`](#delitem)   | Удаление `del obj[key]`          |
| 4 | [`__len__`](#len)           | Длина объекта `len(obj)`         | 
| 5 | [`__contains__`](#contains) | Проверка `item in obj`           | 
| 6 | [Срезы (`slice`)](#slicing) | Обработка срезов в `__getitem__` | 


<a id="getitem"></a>
### 1. `__getitem__(self, key)`

Используется для чтения элемента `key` в объекте (коллекции) `obj`:

```python
obj[key]
```

**Пример:**

```python
class Vector:
    def __init__(self, data):
        self.data = data

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

v = Vector([10, 20, 30])
print(v[1])  # 20
```

Метод поддерживает индексы, slicing и любые ключи.

---

<a id="setitem"></a>
### 2. `__setitem__(self, key, value)`

Используется для присваивания:

```python
obj[key] = value
```

**Пример:**

```python
def __setitem__(self, index, value):
    self.data[index] = value
```

---

<a id="delitem"></a>
…
Проверьте себя: Что именно приходит в параметр key метода __getitem__, когда вызывается v[::-1] — и как код внутри метода отличает этот случай от обычного v[1]?
3
MarkdownРазбор концепции185 строк

Арифметические dunder-методы: прямые, обратные и in-place

less_40__magic_methods/theory_03__arithmetic_methods.md

Полный набор таблиц по операторам +, -, *, /, //, %, ** — для каждого прямой метод (__add__ и т.п.), обратный (__radd__, вызывается когда у левого операнда нет подходящего метода) и in-place (__iadd__ для +=). Плюс унарные __pos__/__neg__/__abs__. На классе Vector(x, y) показаны обычные __add__/__sub__/__mul__, создающие НОВЫЙ объект, и отдельно __iadd__/__isub__, которые изменяют существующий объект на месте и возвращают self — с явным акцентом на разницу: обычный + всегда даёт объект с новым id(), а += для mutable-объекта меняет объект по тому же адресу.

  • Таблицы операторов: +/-/*/​//​/%/** — прямой/обратный/in-place методы
  • Унарные операции: __pos__, __neg__, __abs__
  • class Vector: __add__/__sub__/__mul__ возвращают НОВЫЙ Vector
  • class Vector: __iadd__/__isub__ изменяют self.x/self.y и возвращают self
  • Ключевое отличие: __add__ → новый id(), __iadd__ → тот же адрес объекта
Показать начало файла (185 строк всего)
## Арифметические dunder методы

Это специальные методы, которые позволяют объектам участвовать в арифметических операциях.  

Они определяют, что будет происходить при использовании операторов `+`, `-`, `*`, `/`, `//`, `%`, `**` и др.



### ➕ Сложение

| Оператор | Метод                   | Описание                                     |
| -------- | ----------------------- | -------------------------------------------- |
| `a + b`  | `__add__(self, other)`  | Прямое сложение                              |
| `b + a`  | `__radd__(self, other)` | Обратное сложение (если у `b` нет `__add__`) |
| `a += b` | `__iadd__(self, other)` | Увеличение на месте                          |

---

### ➖ Вычитание

| Оператор | Метод      |
| -------- | ---------- |
| `a - b`  | `__sub__`  |
| `b - a`  | `__rsub__` |
| `a -= b` | `__isub__` |

---

### ✖️ Умножение

| Оператор | Метод      |
| -------- | ---------- |
| `a * b`  | `__mul__`  |
| `b * a`  | `__rmul__` |
| `a *= b` | `__imul__` |

---

### ➗ Деление

| Оператор | Метод          |
| -------- | -------------- |
| `a / b`  | `__truediv__`  |
| `b / a`  | `__rtruediv__` |
| `a /= b` | `__itruediv__` |

---

### // Целочисленное деление

| Оператор  | Метод           |
| --------- | --------------- |
| `a // b`  | `__floordiv__`  |
| `b // a`  | `__rfloordiv__` |
| `a //= b` | `__ifloordiv__` |

---

### % Остаток

…
Проверьте себя: Почему после `v1 += v2` (с определённым __iadd__) id(v1) остаётся тем же, а после `v1 = v1 + v2` (через __add__) id(v1) меняется?
4
MarkdownРазбор концепции96 строк

__bool__: булево значение объекта и как bool() работает без него

less_40__magic_methods/theory_04__bool__.md

Объясняет __bool__(self) — метод, определяющий, что вернёт объект в логическом контексте (if, while, not). На примере Box, где __bool__ возвращает bool(self.items), показано, что пустая коробка — False, непустая — True. Вторая, более глубокая часть — почему bool(f) для объекта без __bool__ всё равно возвращает True, хотя f.__bool__() бросает AttributeError: bool() не вызывает атрибут через обычный поиск, а использует внутренний алгоритм PyObject_IsTrue — сначала ищет __bool__ на уровне ТИПА, затем __len__, и только если нет ни того, ни другого — возвращает True по умолчанию.

  • class Box: __bool__(self): return bool(self.items)
  • bool(empty_box) → False, bool(full_box) → True, if full_box: работает
  • class F: pass — bool(f) всё равно True, хотя f.__bool__() падает AttributeError
  • Алгоритм PyObject_IsTrue: __bool__ → иначе __len__ → иначе True
  • Поиск спецметодов идёт через слоты ТИПА (не через обычный атрибутный поиск экземпляра)
Показать начало файла (96 строк всего)
### Метод `__bool__`

Магический метод `__bool__` отвечает за булево значение объекта (`True` или `False`)
при использовании его в логическом контексте: `if`, `while`, `not` и других булевых выражениях.

---

**Синтаксис**

```python
class MyClass:
    def __bool__(self):
        # вернуть True или False
        return True  # или False
```

---

### Пример

```python
class Box:
    def __init__(self, items):
        self.items = items
    
    def __bool__(self):
        # объект пустой → False, есть элементы → True
        return bool(self.items)


empty_box = Box([])
full_box = Box([1, 2, 3])

print(bool(empty_box))  # False
print(bool(full_box))   # True

if full_box:
    print("Коробка не пуста!")


# False
# True
# Коробка не пуста!
```

---

### Почему функция `bool()` работает даже когда нет метода `__bool__()`?

Рассмотрим пример:
```python
class F:
    pass


f = F()

print(bool(f))  # True
try:
    print(f.__bool__())
…
Проверьте себя: Почему `bool(f)` для объекта класса без __bool__ и без __len__ возвращает True, а не бросает ту же ошибку, что и `f.__bool__()`?
5
MarkdownРазбор концепции39 строк

__call__: объект, который ведёт себя как функция

less_40__magic_methods/theory_05__call__.md

Короткий файл про __call__(self, *args, **kwargs) — метод, который делает экземпляр класса вызываемым через obj(...). На примере Adder(n) с сохранённым состоянием n показано, что add_five(10) — это то же самое, что add_five.__call__(10), и каждый вызов использует запомненное при создании объекта значение n.

  • class MyCallable: __call__(self, *args, **kwargs) — общий синтаксис
  • class Adder: __init__(self, n) + __call__(self, x): return self.n + x
  • add_five = Adder(5); add_five(10) эквивалентно add_five.__call__(10)
  • Объект хранит состояние n=5 между разными вызовами

Что выводит: 15 (add_five(10) = 5+10), затем 8 (add_five(3) = 5+3).

Показать начало файла (39 строк всего)
### Метод `__call__`

Магический метод `__call__` в Python позволяет сделать объект вызываемым как функцию.  

Иными словами этот метод придаёт объекту свойства функции

---

**Синтаксис**

```python
class MyCallable:
    def __call__(self, *args, **kwargs):
        # код, который выполняется при вызове объекта
        print("Вызов объекта с аргументами:", args, kwargs)
```

---

### Пример

```python
class Adder:
    def __init__(self, n):
        self.n = n
    
    def __call__(self, x):
        return self.n + x

add_five = Adder(5)
print(add_five(10))  # 15
print(add_five(3))   # 8
```

В этом примере:

* `add_five(10)` эквивалентно `add_five.__call__(10)`
* Объект хранит состояние `n = 5`, которое используется при каждом вызове
Проверьте себя: Чем объект Adder(5) с __call__ отличается от обычной функции-замыкания, которая тоже «запоминает» число 5 и прибавляет его к аргументу?
6
MarkdownРазбор концепции227 строк

NotImplemented подробно: разный алгоритм для арифметики и для сравнения

less_40__magic_methods/theory_06__NotImplemented.md

Развивает тему NotImplemented из первого файла урока, показывая, что механизм отличается для арифметических операторов и операторов сравнения. Для арифметики (a + b) есть настоящие обратные методы (__radd__ и т.п.), и если оба возвращают NotImplemented — TypeError. Для сравнения обратных методов НЕ существует — вместо этого Python при NotImplemented вызывает ПРОТИВОПОЛОЖНЫЙ оператор у другого операнда (a < b → b.__gt__(a), а не b.__lt__(a)); если и это не помогло, для == возвращается False, для != — True, а для остальных — TypeError. Четыре развёрнутых примера на классах Money и Currency показывают оба механизма в действии: сложение Money+int, сложение Money+Currency через __radd__, сравнение Money с числом и сравнение Money с Currency.

  • Алгоритм для арифметики: a.__add__(b) → NotImplemented → b.__radd__(a) → TypeError
  • Алгоритм для сравнения: a.__lt__(b) → NotImplemented → b.__gt__(a) (противоположный, не обратный)
  • Таблица соответствий операторов сравнения их 'вторым попыткам'
  • Пример 1: Money.__add__/__radd__ с int — 5 + m работает через __radd__
  • Пример 2: Money + Currency — оба определяют __add__/__radd__, работают в связке
  • Пример 3 и 4: Money.__lt__/__gt__ с числом и с Currency — переключение на противоположный оператор

Что выводит: Пример 1: M(15), M(15), M(20). Пример 2: m + c → M(15) (сработал Currency.__radd__), c + m → M(15) (сработал Currency.__add__). Пример 3: True, False, True, False. Пример 4: False, True, True, False — все значения совпадают с комментариями в файле.

Показать начало файла (227 строк всего)
Для арифметических операторов и операторов сравнения рекомендуется применять `NotImplemented`.

## 1. Арифметические операторы

### Как происходит поиск альтернативы?

Для выражения `a + b` Python действует по следующему алгоритму:

1. Пытается выполнить `a.__add__(b)`
2. Если вернул `NotImplemented` -> `b.__radd__(a)` 
3. Если снова `NotImplemented` -> `TypeError` 

ААналогичный механизм используется большинством бинарных арифметических операторов.

| Оператор | Основной метод | Обратный метод  |
| -------- | -------------- | --------------- |
| `+`      | `__add__`      | `__radd__`      |
| `-`      | `__sub__`      | `__rsub__`      |
| `*`      | `__mul__`      | `__rmul__`      |
| `/`      | `__truediv__`  | `__rtruediv__`  |
| `//`     | `__floordiv__` | `__rfloordiv__` |
| `%`      | `__mod__`      | `__rmod__`      |
| `**`     | `__pow__`      | `__rpow__`      |

---

## 2. Операторы сравнения

### Как происходит поиск альтернативы?

Здесь всё немного иначе.

Обратных методов (`__req__`, `__rlt__` и т.п.) не существует.

Поэтому, для `a < b` Python работает по алгоритму:
1. `a.__lt__(b)`
2. Если получен `NotImplemented`, то вызывает `b.__gt__(a)`.

То есть при неудаче вызывается **не обратный метод**, а **противоположный оператор**.

---

## 3. Где ещё есть обработка `NotImplemented`?`

Есть ещё у битовых операторов и у матричного умножения, но это уже совсем за пределами курса.

---

### Таблица альтернатив

| Выражение | Сначала       | Затем         |
| --------- | ------------- | ------------- |
| `a < b`   | `a.__lt__(b)` | `b.__gt__(a)` |
| `a <= b`  | `a.__le__(b)` | `b.__ge__(a)` |
| `a > b`   | `a.__gt__(b)` | `b.__lt__(a)` |
| `a >= b`  | `a.__ge__(b)` | `b.__le__(a)` |
| `a == b`  | `a.__eq__(b)` | `b.__eq__(a)` |
| `a != b`  | `a.__ne__(b)` | `b.__ne__(a)` |


…
Проверьте себя: В примере 4 (Money vs Currency) почему `m < c` даёт False, а `c < m` — True, хотя у Money(10) и Currency(5) сравниваются те же самые числа 10 и 5?