Перейти к содержимому

Обзор магических методов (dunder) в Python [2.9]

Обзор магических методов (dunder) в Python [2.9]

Содержание

1. Что такое магические методы (dunder) и как они “включаются”

Определение

Магические методы — это специальные методы, имена которых начинаются и заканчиваются двойным подчёркиванием: __init__, __len__, __iter__, __add__ и т.д. Python вызывает их автоматически, когда вы используете стандартные конструкции языка.

Например, когда вы пишете len(obj), Python пытается вызвать obj.__len__(). Когда пишете for x in obj, Python ищет obj.__iter__(). Когда пишете a + b — вызывается a.__add__(b) (если он определён).

Почему это “мост” между объектом и синтаксисом

Можно думать о dunder-методах как о “переводчиках”: синтаксис Python (операторы и функции) обращается к вашему объекту через заранее известные имена методов. Поэтому один раз реализовав метод __iter__, вы сразу получаете совместимость с for, list(), tuple(), многими алгоритмами и библиотеками.

Мини-пример: len(), for и оператор +

Ниже — упрощённый пример: класс содержит внутренний список и “прокидывает” к нему поведение. Это очень распространённая техника: оборачивать встроенный тип, добавляя инварианты/правила.

class Box:
    def __init__(self, items):
        self._items = list(items)

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

    def __iter__(self):
        return iter(self._items)

    def __add__(self, other):
        return Box(self._items + list(other))
Python
↑ К оглавлению

2. SortedArray: идея, инварианты и интерфейс

Задача и требования

Построим класс SortedArray, который хранит только целые числа и всегда держит их отсортированными. Это отличный учебный пример, потому что:

  • нужно контролировать входные данные (проверка типов);
  • нужно поддерживать инвариант (сортировка всегда сохранена);
  • хочется, чтобы объект вёл себя “как список”: for, len(), in, [];
  • хочется перегрузить операторы: +, ==, <, >.

Инварианты: что класс обязан гарантировать

Инвариант — это правило, которое должно быть истинным всегда после завершения любой публичной операции. Для SortedArray инварианты такие:

  • внутри хранятся только значения типа int;
  • внутренний список всегда отсортирован по возрастанию.

Магические методы здесь помогают сделать интерфейс естественным, но важно помнить: именно __init__ и методы изменения данных должны “держать” инвариант.

Как будет выглядеть “удобный” API

Хотим, чтобы пользователь мог писать так:

a = SortedArray([3, 1, 4, 2])
print(list(a))          # [1, 2, 3, 4]
print(len(a))           # 4
print(3 in a)           # True
print(a[0], a[-1])      # 1 4

b = a + 10              # прибавить 10 к каждому элементу
c = a + SortedArray([5, -1])  # объединить и отсортировать
Python
↑ К оглавлению

3. Жизненный цикл: __init__, приватность и __del__

__init__: проверка типов и сортировка

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

class SortedArray:
    def __init__(self, arr=None):
        if arr is None:
            arr = []

        # 1) Проверяем тип каждого элемента
        if not all(isinstance(x, int) for x in arr):
            raise ValueError("Список содержит не только целые числа")

        # 2) Сохраняем всегда в отсортированном виде
        self.__arr = sorted(arr)
Python

Обратите внимание: мы используем arr=None вместо arr=[]. Это лучший стиль: вы избегаете ловушки “изменяемого аргумента по умолчанию”.

Опасность arr=[ ] по умолчанию

Почему arr=[ ] плохая идея? Потому что значение по умолчанию создаётся один раз — в момент определения функции, а не при каждом вызове. Если бы мы изменяли этот список (например, делали arr.append(...)), то изменения могли “протекать” между объектами.

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

__arr и “приватность” (name mangling)

Атрибут __arr не просто “соглашение”, как _arr. Python преобразует имя атрибута, чтобы его нельзя было случайно переопределить в наследниках и чтобы к нему сложнее было обращаться снаружи. Это не безопасность, а механизм дисциплины.

__del__: почему почти всегда не нужен

В Python управление памятью автоматическое (сборщик мусора), поэтому __del__ редко нужен. Более того, наличие __del__ иногда усложняет сборку мусора. Если вы работаете с файлами/сокетами — чаще применяют контекстный менеджер (with), а не __del__.

↑ К оглавлению

4. Коллекция: итерация, len(), in, индексация и приведение типов

__iter__: for, list(), tuple()

Если ваш объект — “контейнер”, то первое ожидание пользователя: по нему можно пройти циклом. Реализация __iter__ обычно очень простая: вернуть итератор внутреннего контейнера.

class SortedArray:
    # ... __init__

    def __iter__(self):
        return iter(self.__arr)
Python

После этого автоматически работают: for x in obj, list(obj), tuple(obj) и множество функций, которые принимают “итерируемое”.

__len__ и __contains__

Чтобы работало len(obj), реализуют __len__. Чтобы работало x in obj (оператор принадлежности), реализуют __contains__. Если не реализовать __contains__, Python иногда пытается перебором через итерацию, но лучше сделать явно.

class SortedArray:
    # ...
    def __len__(self):
        return len(self.__arr)

    def __contains__(self, item):
        return item in self.__arr
Python

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

Метод __getitem__ отвечает за obj[index]. Важно: в Python индекс может быть не только числом, но и срезом slice, например obj[1:4].

class SortedArray:
    # ...
    def __getitem__(self, index):
        return self.__arr[index]
Python

Если вы просто возвращаете self.__arr[index], то:

  • для целого индекса вернётся число;
  • для среза вернётся список.
    Это нормально, но иногда удобнее при срезе возвращать новый SortedArray. Тогда срез сохраняет инвариант “отсортировано” и остаётся тем же типом.

     

    __int__, __float__, __bool__: смысловые преобразования

    Приведение типов — это соглашение: вы сами выбираете смысл. Главное — чтобы преобразование выглядело логично и не удивляло пользователя.

    • __int__: часто “обобщающая характеристика” — например сумма или количество.
    • __float__: может быть среднее значение, медиана и т.д.
    • __bool__: чаще всего “пустой/непустой”.
    class SortedArray:
        # ...
        def __int__(self):
            return sum(self.__arr)
    
        def __float__(self):
            return sum(self.__arr) / len(self.__arr) if self.__arr else 0.0
    
        def __bool__(self):
            return len(self.__arr) > 0
    Python

    __repr__ и __str__: отладка vs “красиво вывести”

    Два метода часто путают:

    • __str__ — человекочитаемый вывод (то, что печатается в print(obj)).
    • __repr__ — отладочное представление (то, что видите в консоли/логах).

    Хорошее правило: __repr__ должен быть максимально информативным и по возможности похожим на код, который создаёт объект (не всегда возможно, но это полезная цель).

    class SortedArray:
        # ...
        def __str__(self):
            return str(self.__arr)
    
        def __repr__(self):
            return f"SortedArray({self.__arr!r})"
    Python
    ↑ К оглавлению

    5. Перегрузка операторов: сравнения и арифметика

    __eq__ и сравнения: как делать корректно

    Сравнение — важный элемент интерфейса. Но у него есть тонкость: что считать “равенством”? В нашем примере (как в учебной задаче) равенство зависит от среднего значения, то есть два разных массива могут быть “равны”, если их среднее одинаковое.

    В реальных проектах чаще сравнивают либо полный набор элементов (как списки), либо уникальный ключ. Но для демонстрации dunder-методов вариант со средним подходит.

    Почему лучше возвращать NotImplemented

    Когда оператор встречает неизвестный тип справа, хорошей практикой считается вернуть NotImplemented, а не бросать исключение. Тогда Python попробует выполнить “обратную” операцию (например, вызвать метод другого объекта). Если и там не получится — Python сам выдаст корректный TypeError.

    class SortedArray:
        # ...
        def __eq__(self, other):
            if not isinstance(other, SortedArray):
                return NotImplemented
            return float(self) == float(other)
    
        def __lt__(self, other):
            if not isinstance(other, SortedArray):
                return NotImplemented
            return float(self) < float(other)
    
        def __gt__(self, other):
            if not isinstance(other, SortedArray):
                return NotImplemented
            return float(self) > float(other)
    Python

    __add__ и __radd__: поддержка obj + x и x + obj

    __add__ определяет поведение obj + other. Но если вы хотите, чтобы работало ещё и other + obj (например, 3 + SortedArray([...])), вам нужен __radd__ — “reverse add”.

    class SortedArray:
        # ...
        def __add__(self, other):
            if isinstance(other, int):
                return SortedArray([x + other for x in self.__arr])
            if isinstance(other, SortedArray):
                return SortedArray(self.__arr + list(other))
            return NotImplemented
    
        def __radd__(self, other):
            # Позволяет писать: 3 + SortedArray([1,2,3])
            return self.__add__(other)
    Python
    ↑ К оглавлению

    6. Шпаргалка: самые полезные dunder-методы

    Группы методов

    • Жизненный цикл: __init__, __del__
    • Строки и отладка: __str__, __repr__
    • Контейнер: __iter__, __len__, __contains__, __getitem__
    • Сравнения: __eq__, __lt__, __gt__, __le__, __ge__
    • Арифметика: __add__, __sub__, __mul__, ...

    Таблица “синтаксис → метод”

    СинтаксисЧто происходитМетод
    len(obj) Python вызывает метод длины __len__
    for x in obj Python получает итератор __iter__
    x in obj Проверка принадлежности __contains__
    obj[i] Индексация/срез __getitem__
    print(obj) Человекочитаемый вывод __str__
    obj в REPL/логах Отладочное представление __repr__
    a == b Сравнение на равенство __eq__
    a + b Сложение __add__
    ↑ К оглавлению

    7. Практика: частые ошибки и улучшения SortedArray

    Типичные ошибки

    ОшибкаПочему плохоКак правильно
    def __init__(self, arr=[]) Изменяемый аргумент по умолчанию может вести к неожиданным эффектам arr=None, внутри: arr = []
    Не проверять типы элементов Инвариант ломается: внутри могут оказаться строки/float и т.д. all(isinstance(x, int) for x in arr)
    Хранить как есть (без сортировки) Класс перестаёт выполнять обещание “Sorted” self.__arr = sorted(arr)
    Снаружи менять __arr Можно сломать сортировку и типы Делать безопасные методы (например, add())

    Как защитить инвариант “всегда отсортировано”

    Самая частая проблема: пользователь может захотеть “вставить элемент” или “изменить по индексу”. Если вы добавите __setitem__ без контроля, можно легко разрушить сортировку.

    Поэтому часто делают так:

    • либо вообще не дают изменять элементы по индексу (нет __setitem__),
    • либо разрешают только безопасные операции, которые сохраняют сортировку (например, метод add(x)).

    Идеи расширения

    Если захочется сделать класс ещё “взрослее”, можно добавить:

    • метод вставки с сохранением сортировки через модуль bisect;
    • __iadd__ для +=;
    • __mul__ для умножения (например, повторить элементы или масштабировать значения);
    • поддержку срезов, возвращающих новый SortedArray, а не обычный список.
    ↑ К оглавлению

    8. Самопроверка (чек-лист)

    Таблица с чекбоксами

    Проверяю себя
    Я понимаю, что dunder-методы вызываются Python автоматически при использовании синтаксиса.
    Я знаю, зачем нужен __iter__ и как он связан с for и list().
    Я понимаю разницу между __str__ и __repr__.
    Я могу перегрузить + через __add__ и понимаю, зачем нужен __radd__.
    Я умею поддерживать инвариант класса (типы + сортировка) и не даю пользователю его сломать.
    ↑ К оглавлению

    Примеры обращений к “волшебным” методам (что именно вызывает Python)

    Важно понимать: в большинстве случаев вы не вызываете dunder-методы напрямую. Вы пишете привычный синтаксис Python, а интерпретатор “под капотом” обращается к методам вида __...__. Но для обучения полезно увидеть, что синтаксис и прямой вызов — это почти одно и то же.

    Нормальная практика — пользоваться синтаксисом (len(obj), obj + x), а не писать obj.__len__(). Прямые вызовы dunder-методов применяют редко: в отладке, экспериментах или при написании библиотек.

    Мини-таблица “синтаксис ↔ прямой вызов”

    Выражение в кодеЧто пытается сделать Python
    len(obj) obj.__len__()
    iter(obj) obj.__iter__()
    next(it) it.__next__()
    x in obj obj.__contains__(x) (или перебор через итерацию, если метода нет)
    obj[i] obj.__getitem__(i)
    obj + other obj.__add__(other), если вернул NotImplementedother.__radd__(obj)
    obj == other obj.__eq__(other)
    int(obj) obj.__int__()
    print(obj) obj.__str__() (в интерактивной среде часто виден __repr__)

    Живой пример на Box

    b = Box([1, 2, 3])
    
    # len(obj) вызывает __len__
    print(len(b))          # 3
    print(b.__len__())     # 3 (прямой вызов, чисто для понимания)
    
    # for вызывает __iter__ (а затем __next__ у итератора)
    for x in b:
        print(x)
    
    it = iter(b)           # вызывает b.__iter__()
    print(next(it))        # вызывает it.__next__()
    print(next(it))
    
    # оператор + вызывает __add__
    b2 = b + [4, 5]
    b3 = b.__add__([4, 5]) # прямой вызов (обычно так не пишут)
    Python
    ↑ К оглавлению

    Примеры: как “коллекционные” dunder-методы вызываются на SortedArray

    Ниже — один набор примеров, где для каждой операции показаны две формы: обычная (правильная в реальном коде) и прямая (для понимания механики).

    a = SortedArray([3, 1, 4, 2])  # внутри станет [1, 2, 3, 4]
    
    # 1) Итерация
    print(list(a))             # list() использует итератор: __iter__
    print(list(a.__iter__()))  # прямой вызов (вернёт тот же результат)
    
    # 2) len()
    # Если вы реализовали __len__, то len(a) будет работать так:
    # len(a) -> a.__len__()
    # (в вашей текущей версии статьи __len__ описан; если его нет — добавьте)
    print(len(a))
    # print(a.__len__())       # можно вызвать напрямую, если __len__ реализован
    
    # 3) in (принадлежность)
    # 3 in a -> a.__contains__(3) (если метод реализован)
    print(3 in a)
    # print(a.__contains__(3)) # прямой вызов
    
    # 4) Индексация
    # a[0] -> a.__getitem__(0)
    print(a[0])
    print(a.__getitem__(0))
    Python

    Почему полезно знать “прямые вызовы”? Потому что так легче отлаживать. Например, если for x in a падает, можно проверить: работает ли a.__iter__() и что он возвращает.

    ↑ К оглавлению

    Примеры: приведение типов и строковые представления (int/float/bool/str/repr)

    Методы __int__, __float__, __bool__, __str__, __repr__ обычно вызываются не напрямую, а через встроенные функции и стандартные механизмы вывода.

    a = SortedArray([3, 1, 4, 2])
    
    # int(obj) -> obj.__int__()
    print(int(a))
    print(a.__int__())   # прямой вызов
    
    # float(obj) -> obj.__float__()
    print(float(a))
    print(a.__float__()) # прямой вызов
    
    # bool(obj) -> obj.__bool__()
    # применяется в if/while, а также в bool(obj)
    print(bool(a))
    print(a.__bool__())  # прямой вызов
    
    empty = SortedArray([])
    print(bool(empty))   # False
    
    # str(obj) -> obj.__str__()
    print(str(a))
    print(a.__str__())   # прямой вызов
    
    # repr(obj) -> obj.__repr__()
    # В интерактивной консоли обычно показывается repr
    print(repr(a))
    print(a.__repr__())  # прямой вызов
    Python
    ↑ К оглавлению

    Примеры: как вызываются __add__/__radd__ и __eq__/__lt__/__gt__

    Для операторов важно понимать, что Python может “перекинуть” операцию на правый операнд, если левый вернул NotImplemented. Это и есть причина, почему NotImplemented — правильный путь для “не моего типа”.

    a = SortedArray([1, 2, 3, 4])
    b = SortedArray([-2, 7])          # среднее тоже 2.5 (если сравниваете по float)
    
    # Сравнения
    # a == b -> a.__eq__(b)
    print(a == b)
    print(a.__eq__(b))               # прямой вызов
    
    # a < b -> a.__lt__(b)
    print(a < b)
    print(a.__lt__(b))               # прямой вызов
    
    # Сложение с int
    # a + 10 -> a.__add__(10)
    c = a + 10
    d = a.__add__(10)                # прямой вызов
    print(list(c), list(d))
    
    # Сложение с SortedArray
    # a + b -> a.__add__(b)
    ab1 = a + b
    ab2 = a.__add__(b)
    print(list(ab1), list(ab2))
    
    # Обратное сложение (если реализован __radd__)
    # 10 + a -> int.__add__(a) (скорее всего вернёт NotImplemented) -> a.__radd__(10)
    ra1 = 10 + a
    ra2 = a.__radd__(10)             # прямой вызов
    print(list(ra1), list(ra2))
    Python

    Практический смысл NotImplemented: вы не “ломаете” выражение сразу, а позволяете Python попробовать альтернативу — метод правого операнда (__radd__, __req__ и т.п.).

    ↑ К оглавлению

    Мини-практика: как проверить, что метод существует и что он делает

    Иногда полезно быстро “прощупать” объект: какие dunder-методы у него определены и что возвращают. Для этого используют dir() и getattr().

    a = SortedArray([3, 1, 2])
    
    # Посмотреть все атрибуты/методы (в том числе dunder)
    methods = [name for name in dir(a) if name.startswith("__") and name.endswith("__")]
    print(methods)
    
    # Аккуратно достать конкретный метод и вызвать
    m = getattr(a, "__iter__")
    it = m()
    print(next(it))
    Python
    ↑ К оглавлению
    Воскресенье, 15 февраля 2026
    Обзор магических методов (dunder) в Python [2.9]