Написать пост

Гайд по магическим методам в Python

Руководство по магическим методам, или dunder-методам Питона, которые предназначены для перегрузки Python-операторов или встроенных методов.

Магические методы в языке программирования Python нужны, чтобы реализовывать свойства объектов при их взаимодействии.
В статье раскроем их «магию» и остановимся на каждом из методов подробно.

Создание и удаление объектов

Любое описание объекта в объектно-ориентированном программировании начинается с создания объекта и его удаления. Давайте подробнее остановимся на каждом из них:

__new__(cls[, ...]) — метод создания типа класса. Он принимает первым аргументом тип класса, в котором он вызывается, и, как правило, возвращает этот же тип. В основном используется, чтобы настраивать создание экземпляра класса тех объектов, которые наследуются от неизменяемых типов (например, int, str, или tuple).

__init__(self[, ...]) — конструктор класса. Используется при определении объектов.

__init_subclass__(cls) — позволяет переопределить создание подклассов объекта. Например, добавлять дополнительные атрибуты:

			>>> class Test:
>>>     def __init_subclass__(cls, /, test_param, **kwargs):
>>>         super().__init_subclass__(**kwargs)
>>>         cls.test_param = test_param
>>>
>>> class AnotherTest(Test, test_param="Hello World"):
>>>     pass
		

__del__(self) — деструктор класса. Вызывается автоматически сборщиком мусора, практически никогда не используется, за исключением, когда пользователя необходимо предупредить о незакрытых дескрипторах.

Общие свойства объектов

Любой объект может содержать дополнительную информацию, полезную при отладке или приведении типов. Например:

__repr__(self) — информационная строка об объекте. Выводится при вызове функции repr(...) или в момент отладки. Для последнего этот метод и предназначен. Например:

			>>> class Test:
>>>     def __repr__(self):
>>>         return ""
>>>
>>> Test()
... ''
		

__str__(self) — вызывается при вызове функции str(...), возвращает строковый объект. Например:

			>>> class Test:
>>>     def __str__(self):
>>>         return "Hello World"
>>>
>>> test = Test()
>>> str(test)
... 'Hello World'
		

__bytes__(self) — аналогично __str__(self), только возвращается набор байт.

__format__(self, format_spec) — вызывается при вызове функции format(...) и используется для форматировании строки с использованием строковых литералов.

Методы сравнения объектов между собой

__lt__(self, other) — определяет поведение оператора сравнения «меньше», <.

__le__(self, other) — определяет поведение оператора сравнения «меньше или равно», <=.

__eq__(self, other) — определяет поведение оператора «равенства», ==.

__ne__(self, other) — определяет поведение оператора «неравенства», !=.

__gt__(self, other) — определяет поведение оператора сравнения «больше», >.

__ge__(self, other) — определяет поведение оператора сравнения «больше или равно», >=.

__hash__(self) — вызывается функцией hash(...) и используется для определения контрольной суммы объекта, чтобы доказать его уникальность. Например, чтобы добавить объект в set, frozenset, или использовать в качестве ключа в словаре dict.

__bool__(self) — вызывается функцией bool(...) и возвращает True или False в соответствии с реализацией. Если данный метод не реализован в объекте, и объект является какой-либо последовательностью (списком, кортежем и т.д.), вместо него вызывается метод __len__. Используется, в основном, в условиях if, например:

			>>> class Test:
>>>     def __bool__(self):
>>>         return True
>>>
>>> test = Test()
>>>
>>> if test:
>>>    print("Hello World")
>>>
... 'Hello World'
		

Доступ к атрибутам объекта

Доступ ко всем свойствам объекта также контролируются отдельными методами:

__getattr__(self, name) — вызывается методом getattr(...) или при обращении к атрибуту объекта через x.y, где x — объект, а y — атрибут.

__setattr__(self, name, value) — вызывается методом setattr(...)или при обращении к атрибуту объекта с последующим определением значения переданного атрибута. Например: x.y = 1, где x — объект, y — атрибут, а 1 — значение атрибута.

__delattr__(self, name) — вызывается методом delattr(...)или при ручном удалении атрибута у объекта с помощью del x.y, где  x — объект, а y — атрибут.

__dir__(self) — вызывается методом dir(...) и выводит список доступных атрибутов объекта.

Создание последовательностей

Любой объект может реализовать методы встроенных последовательностей (словари, кортежи, списки, строки и так далее). Доступ к значениям последовательности переопределяется следующими методами:

__len__(self) — вызывается методом len(...) и возвращает количество элементов в последовательности.

__getitem__(self, key) — вызывается при обращении к элементу в последовательности по его ключу (индексу). Метод должен выбрасывать исключение TypeError, если используется некорректный тип ключа, KeyError, если данному ключу не соответствует ни один элемент в последовательности. Например:

			>>> list_object = [1, 2, 3, 4, 5]
>>> print(list_object[0])
... 1
>>>
>>> string_object = "hello world"
>>> print(string_object[0:5])
... 'hello'
>>>
>>> dict_object = {"key0": True, "key1": False}
>>> print(dict_object["key0"])
... True
		

__setitem__(self, key, value) — вызывается при присваивании какого-либо значения элементу в последовательности. Также может выбрасывать исключения TypeError и KeyError. Например:

			>>> list_object = [1, 2, 3, 4, 5]
>>> list_object[0] = 78
>>> print(list_object)
... [78, 2, 3, 4, 5]
>>>
>>> dict_object = {"key0": True, "key1": False}
>>> dict_object["key0"] = False
>>> print(dict_object)
... {"key0": False, "key1": False}
		

__delitem__(self, key) — вызывается при удалении значения в последовательности по его индексу (ключу) с помощью синтаксиса ключевого слова del.

__missing__(self, key) — вызывается в случаях, когда значения в последовательности не существует.

__iter__(self) — вызывается методом iter(...) и возвращает итератор последовательности, например, для использования объекта в цикле:

			>>> class Test:
>>>     def __iter__(self):
>>>         return (1, 2, 3)
>>>
>>> for value in Test():
>>>     print(value)
... 1
... 2
... 3
		

__reversed__(self) — вызывается методом reversed(...) и аналогично методу __iter__ возвращает тот же итератор, только в обратном порядке.

__contains__(self, item) — вызывается при проверке принадлежности элемента к последовательности с помощью in или not in.

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

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

Унарные операторы

__neg__(self) — определяет поведение для отрицания (-a)

__pos__(self) — определяет поведение для унарного плюса (+a)

__abs__(self) — определяет поведение для встроенной функции abs(...)

__invert__(self) — определяет поведение для инвертирования оператором ~

Обычные арифметические операторы

__add__(self, other) — сложение, оператор +

__sub__(self, other) — вычитание, оператор -

__mul__(self, other) — умножение, оператор *

__matmul__(self, other) — умножение матриц, оператор @

__truediv__(self, other) — деление, оператор /

__floordiv__(self, other) — целочисленное деление, оператор //

__mod__(self, other) — остаток от деления, оператор %

__divmod__(self, other) — деление с остатком, определяет поведение для встроенной функции divmod(...)

__pow__(self, other[, modulo]) — возведение в степень, оператор **

__lshift__(self, other) — двоичный сдвиг влево, оператор <<

__rshift__(self, other) — двоичный сдвиг вправо, оператор >>

__and__(self, other) — двоичное И, оператор &

__xor__(self, other) — исключающее ИЛИ, оператор ^

__or__(self, other) — двоичное ИЛИ, оператор |

Отражённые арифметические операторы

Если в обычной арифметике между объектами a и b, объектом, который мы изменяем, является a, и объектом, с которым мы работаем, являетсяb, то в отражённой арифметике наоборот — b является изменяемым, a — объектом, с которым мы работаем, и который передается в качестве аргумента. Например:

			# Сложение, используется обычная арифметика и метод __add__ в объекте left:
>>> left + right
# Сложение, используется отражённая арифметика и метод __radd__ в объекте left:
>>> right + left
		

Список методов похож на тот, что используется в обычной арифметике, за исключением того, что добавляется префикс «r» ко всем методам:

__radd__(self, other) — сложение, оператор +

__rsub__(self, other) — вычитание, оператор -

__rmul__(self, other) — умножение, оператор *

__rmatmul__(self, other) — умножение матриц, оператор @

__rtruediv__(self, other) — деление, оператор /

__rfloordiv__(self, other) — целочисленное деление, оператор //

__rmod__(self, other) — остаток от деления, оператор %

__rdivmod__(self, other) — деление с остатком

__rpow__(self, other[, modulo]) — возведение в степень, оператор **

__rlshift__(self, other) — двоичный сдвиг влево, оператор <<

__rrshift__(self, other) — двоичный сдвиг вправо, оператор >>

__rand__(self, other) — двоичное И, оператор &

__rxor__(self, other) — исключающее ИЛИ, оператор ^

__ror__(self, other) — двоичное ИЛИ, оператор |

Составное присваивание

Эти методы — комбинация «обычного» оператора и присваивания. Возвращают тот же тип объекта, который будет присвоен переменной слева. Например:

			x = 3
x += 2 # другими словами x = x + 2
		

__iadd__(self, other) — сложение с присваиванием, оператор +=

__isub__(self, other) — вычитание с присваиванием, оператор -=

__imul__(self, other) — умножение с присваиванием, оператор *=

__imatmul__(self, other) — умножение матриц с присваиванием, оператор @=

__itruediv__(self, other) — деление с присваиванием, оператор /=

__ifloordiv__(self, other) — целочисленное деление с присваиванием, оператор //=

__imod__(self, other) — остаток от деления с присваиванием, оператор %=

__ipow__(self, other[, modulo]) — возведение в степень с присваиванием, оператор **=

__ilshift__(self, other) — двоичный сдвиг влево с присваиванием, оператор <<=

__irshift__(self, other) — двоичный сдвиг вправо с присваиванием, оператор >>=

__iand__(self, other) — двоичное И с присваиванием, оператор &=

__ixor__(self, other) — исключающее ИЛИ с присваиванием, оператор ^=

__ior__(self, other) — двоичное ИЛИ с присваиванием, оператор |=

Преобразования типов

Помимо всего прочего, в Python множество методов, которые позволяют переопределять поведение встроенных функций преобразования типов, таких как int(...), float(...) и т.д. Например:

__complex__(self) — преобразование типа в комплексное число

__int__(self) — преобразование типа к int

__float__(self) — преобразование типа к float

__index__(self) — преобразование типа к int, когда объект используется в срезах (выражения вида [start:stop:step])

__round__(self[, ndigits]) — округление числа с помощью функции round(...)

__trunc__(self) — вызывается методом math.trunc(...)

__floor__(self) — вызывается методом math.floor(...)

__ceil__(self) — вызывается методом math.ceil(...)

Вызываемые объекты

__call__(self[, args...]) — позволяет любому экземпляру класса вести себя как обычная функция. Например:

			>>> class Test:
>>>     def __call__(self, message):
>>>         print(message)
>>>         return True
>>>
>>> test = Test()
>>> test("Hello World")
... 'Hello World'
... True
		

__await__(self) — возвращает итератор, превращая класс в корутину, результат выполнения которой можно получить с помощью await. Подробнее об этом можно узнать в PEP 492.

Контекстные менеджеры

Любой объект может быть представлен как контекстный менеджер, который вызывается с помощью with или async with. Данная конструкция позволяет выполнить какие-либо действия по настройке объекта и при выходе из контекстного менеджера, произвести какие-либо действия по очистке, не смотря на то, было ли вызвано исключение в блоке контекстного менеджера.

__enter__(self) — определяет начало блока контекстного менеджера, вызванного с помощью with

__exit__(self, exc_type, exc_value, traceback) — определяет конец блока контекстного менеджера. Может использоваться для контролирования исключений, очистки, или любых действий, которые должны быть выполнены после блока внутри with. Если блок выполнился успешно, то все три аргумента (exc_type, exc_value и traceback) будут установлены в значение None.

Например:

			>>> class ContextManager:
>>>     def __enter__(self):
>>>         log("entering context")
>>>
>>>     def __exit__(self, exc_type, exc_value, traceback):
>>>         log("exiting context")
>>>
>>> with ContextManager():
...     print("in context manager")
...
... 'entering context'
... 'in context manager'
... 'exiting context'
		

__aenter__(self) — аналогично __enter__, только функция возвращает корутину (результат которой можно получить с помощью await)

__aexit__(self, exc_type, exc_value, traceback) — аналогично __exit__, только функция возвращает корутину (результат которой можно получить с помощью await)

Например:

			>>> class AsyncContextManager:
>>>     async def __aenter__(self):
>>>         await log("entering context")
>>>
>>>     async def __aexit__(self, exc_type, exc_value, traceback):
>>>         await log("exiting context")
>>>
>>> async with AsyncContextManager():
...     print("in context manager")
...
... 'entering context'
... 'in context manager'
... 'exiting context'
		

Неиспользуемые методы

Некоторые методы, после полного перехода с Python 2 на Python 3 стали устаревшими и больше не используются.

__unicode__ — полностью исчез в версии Python 3, вместо него используются отдельные методы __str__и __bytes__

__div__ — так как в Python 3 теперь по умолчанию «правильное деление», данного метода не существует

__cmp__ — более не существует, вместо него используются __lt__, __le__, __eq__, __ne__, __gt__и __ge__

__nonzero__ — переименован в __bool__

Упрощение работы с магическими методами

Большая часть из вышеописанных методов реализуется библиотеками, они позволяют использовать так называемые «обрёртки» над классами, в которых будут реализованы необходимые магические методы.

Пример таких библиотек:

Принципы и идеология этих (и других библиотек) схожи в одном – они позволяют реализовать всю необходимую логику работы с объектом, не дублируя код для каждого отдельного объекта.

Например:

			>>> from dataclasses import dataclass
>>>
>>> @dataclass(init=True, repr=True, eq=True, order=True, unsafe_hash=True)
>>> class Person:
>>>     first_name: str
>>>     last_name: str
>>>     age: int
		

Данный объект будет иметь в себе:

  • Три атрибута first_name, last_name и age, которые также будут передаваться в конструктор класса;
  • Будет реализован метод __repr__ для вывода информации для отладки;
  • Будут реализованы все магические методы сравнения (такие как __lt__, __eq__ и так далее), а также метод __hash__.

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

Следите за новыми постами
Следите за новыми постами по любимым темам
40К открытий45К показов