Что такое магические методы

Магические методы (или специальные методы) — это встроенные функции в Python для определения специального поведения внутри класса. Они помогают более разнообразно определять объекты: перегрузка операторов, изменение способа вывода объекта, поведение как контейнера и т.д. Основные характеристики:

  • Позволяют определять поведение для базовых операций (арифметика и т.п.)
  • Определяются с помощью двойного подчёркивания (__)
  • В отличие от обычных функций, не вызываются явно
class Example():
    def __init__(self, value):
        self.value = value

Классический пример магического метода — __init__(), который вызывается для инициализации при создании объекта. Тем, кто сначала знакомился с объектно-ориентированным программированием на других языках, может показаться неудобным определять внутренние переменные в __init__(). Однако, поняв контекст использования магических методов в Python, можно более чисто воспринимать это как «Python пытается решить эту проблему с помощью встроенных методов».

Магические методы используются гораздо шире, чем кажется. Например, сложение, которое внешне выглядит как оператор +, на самом деле вызывает функцию __add__() внутри объекта.

class Example():
    def __init__(self, value):
        self.value = value
  
    def __add__(self, x):
        return self.value + x.value

ex1 = Example(10)
ex2 = Example(20)

print(ex1 + ex2)  # возвращает 30

При необходимости можно реализовать и такое: оператор + используется, но на самом деле не выполняет сложения.

def __add__(self, x):
    return "А вы хотели выполнить сложение?"

Включая арифметику, можно определить множество различных магических методов. Помимо приведённых примеров, можно по-новому определять и другие функции.

Магический методОператор/Функция
__add__+
__sub__-
__mul__*
__truediv__/
__gt__>
__lt__<
__le__<=
__ge__>=
__eq__==
__new__Создание объекта
__del__Удаление объекта

Два особых момента

Создание внутренней документации

Хотя это не магический метод, полезно знать о __doc__. __doc__ создаёт внутреннюю документацию (docstring) для класса или метода с помощью многострочного комментария сразу под определением.

class Example():
    '''This is a doc'''
    # ...

Можно записать краткую информацию о классе или методе: для чего он разработан, как его использовать и т.п. Доступ к документации осуществляется через print(Example.__doc__) или метод help(). Более подробные сведения о правилах оформления можно найти в PEP 257.

Просмотр магических методов

При необходимости все магические методы, определённые для конкретного объекта, можно просмотреть с помощью dir(). Например, если проверить магические методы целого числа через dir(10), можно увидеть следующий результат:

['__abs__', '__add__', '__and__', '__bool__', '__ceil__', '__class__', '__delattr__', '__dir__', '__divmod__', '__doc__', 
'__eq__', '__float__', '__floor__', '__floordiv__', '__format__', '__ge__', '__getattribute__', '__getnewargs__', '__getstate__', '__gt__', 
'__hash__', '__index__', '__init__', '__init_subclass__', '__int__', '__invert__', '__le__', '__lshift__', '__lt__', '__mod__', 
'__mul__', '__ne__', '__neg__', '__new__', '__or__', '__pos__', '__pow__', '__radd__', '__rand__', '__rdivmod__', '__reduce__', 
'__reduce_ex__', '__repr__', '__rfloordiv__', '__rlshift__', '__rmod__', '__rmul__', '__ror__', '__round__', '__rpow__', '__rrshift__', 
'__rshift__', '__rsub__', '__rtruediv__', '__rxor__', '__setattr__', '__sizeof__', '__str__', '__sub__', '__subclasshook__', '__truediv__', 
'__trunc__', '__xor__', 'as_integer_ratio', 'bit_count', 'bit_length', 'conjugate', 'denominator', 'from_bytes', 'imag', 
'is_integer', 'numerator', 'real', 'to_bytes']

Благодаря этому можно использовать dir() для определения набора магических методов, поддерживаемых внешним объектом (например, библиотекой), или различать поведение в зависимости от того, поддерживает ли объект определённую операцию, с помощью if '__add__' in dir(obj).

Пример использования магических методов

Может пригодиться для создания упрощённого делегата. Хотя в Python для создания цепочек делегатов проще и рекомендуется добавлять функции в список, меня лично продолжало беспокоить какое-то неясное неудобство, и я нашёл такой способ.

class RaiseEvent():
    def __init__(self):
        self._handlers = []

    def __iadd__(self, function):
        self._handlers.append(function)
        return self

    def __call__(self, *args, **kwargs):
        for function in self._handlers:
            function(*args, **kwargs)

Определив такой класс, можно добавлять функции с помощью оператора += и использовать привычную форму raise_event += function, похожую на C#. Когда проект разрастается, заранее написанный подобный пользовательский класс, как мне кажется, даёт преимущество в читаемости в долгосрочной перспективе.