Объяснение магических методов Python

Все, что нам нужно знать о магических методах, которые не такие уж волшебные.

Магические методы - не такие уж и волшебные !! 🃏

В Python имена методов, которые имеют начальные и конечные двойные подчеркивания, зарезервированы для специального использования, например, метод__init__ для конструкторов объектов или метод __call__, чтобы сделать объект доступным для вызова. Эти методы известны как методы dunder. dunder здесь означает «Double Under (подчеркивание)». Эти опасные методы часто называют магическими, хотя в них нет ничего волшебного. Многим в сообществе Python не нравится это слово (магия), поскольку оно дает ощущение, что использование этих методов не рекомендуется, однако на самом деле все обстоит как раз наоборот.

Зачем оборачивать их двойным подчеркиванием?

Заключение этих функций в двойные подчеркивания с обеих сторон было на самом деле просто способом упростить язык. Создатели Python не хотели красть у нас совершенно хорошие имена методов (например, call или iter), но они также не хотели вводить какой-то новый синтаксис, просто чтобы объявить определенные методы «особенными». dunders достигают желаемой цели - делают определенные методы особенными, а также делают их такими же, как и другие простые методы, во всех аспектах, кроме соглашения об именах.

Что следует помнить для dunders

  1. Назовите их «dunders» - поскольку в них нет ничего тайного или волшебного. Терминология вроде «магии» заставляет их казаться намного более сложными, чем они есть на самом деле.
  2. При необходимости используйте dunders - это основная функция Python, которую следует использовать по мере необходимости.
  3. Крайне не рекомендуется придумывать собственные дандеры. Лучше не использовать в наших программах имена, которые начинаются и заканчиваются двойным подчеркиванием, чтобы избежать конфликтов с нашими собственными методами и атрибутами.

Dunder методы могут использоваться для имитации поведения встроенных типов для определенных пользователем объектов. Рассмотрим следующий пример, в котором мы добавляем поддержку метода len() к нашему собственному объекту.

class NoLenDefined:
    pass
>>> obj = NoLenDefined()
>>> len(obj)
TypeError: "object of type 'NoLenDefined' has no len()"

Добавление метода __len__() dunder исправит ошибку.

class LenDefined:
    def __len__():
        return 1
>>> obj = LenDefined()
>>> len(obj)
1

🖊 ПРИМЕЧАНИЕ. len() внутренне вызывает специальный метод __len__() для возврата длины объекта.
💡 Совет: вы можете использовать метод dir() для объекта, чтобы увидеть методы dunder, унаследованные классом. Пример: dir(int)

Давайте рассмотрим различные «глупые» методы, чтобы лучше понять различные функции, предоставляемые Python.

Инициализация объекта: __init__

Когда объект создается, он инициализируется путем вызова метода __init__ для объекта.

class Person:
    def __init__(self, name, age):
        self.name = name
        self.age = age
>>> person = Person('Sarah', 25)
>>> person
<__main__.Person instance at 0x10d580638>

Когда вызывается метод __init__, объект (в данном случае person) передается как «я». Другие аргументы, используемые в вызове метода, передаются функции как остальные аргументы.

Представление объекта: __str__, __repr__

Когда мы определяем настраиваемый класс и пытаемся вывести его экземпляр на консоль, результат плохо описывает объект, поскольку преобразование «в строку» по умолчанию является базовым и не содержит подробностей. Рассмотрим следующий пример:

class Person:
    def __init__(self, name, age):
        self.name = name
        self.age = age
>>> person = Person('Sarah', 25)
>>> print(person)
<__main__.Person instance at 0x10d580638>
>>> person
<__main__.Person instance at 0x10d580638>

По умолчанию мы получили имя класса вместе с id объекта. Было бы более желательно, чтобы атрибуты объекта были напечатаны, например:

print (person.name, person.age)
Sarah 25

Для этого мы можем добавить наш собственный to_string() метод, но при этом упускается из виду встроенный механизм Python для представления объектов в виде строк. Поэтому давайте добавим в наш класс "чертовски" методы, чтобы описать наш объект так, как мы хотим.

class Person:
    def __init__(self, name, age):
        self.name = name
        self.age = age
    def __str__(self):
        return "Person: {}, Age: {}".format(self.name, self.age)
>>> person = Person('Sarah', 25)
>>> print(person)
Person: Sarah, Age: 25
>>> person
<__main__.Person instance at 0x10d5807e8>

Следовательно, метод __str__ может быть переопределен для возврата печатаемого строкового представления любого определенного пользователем класса.

>>> print(person)
Person: Sarah, Age: 25
>>> str(person)
Person: Sarah, Age: 25
>>> '{}'.format(person)
Person: Sarah, Age: 25

__repr__ похож на __str__, но используется в другой ситуации. Если мы проверим наш person объект в сеансе интерпретатора, мы все равно получим результат <__main__.Person instance at 0x10d5807e8>. Давайте переопределим наш класс, чтобы он содержал оба метода dunder.

class Person:
    def __init__(self, name, age):
        self.name = name
        self.age = age
    def __str__(self):
        print('inside str')
        return "Person: {}, Age: {}".format(self.name, self.age)
    def __repr__(self):
        print('inside repr')
        return " Person: {}, Age: {}".format(self.name, self.age)
>>> person = Person('Sarah', 25)
>>> person
inside repr
Person: Sarah, Age: 25
>>>print(person)
inside str
Person: Sarah, Age: 25

Как видно выше, __repr__ вызывается, когда объект проверяется в сеансе интерпретатора.
На высоком уровне __str__ используется для создания вывода для конечного пользователя, а __repr__ в основном используется для отладки и разработки. Цель repr - быть недвусмысленной, а str - удобочитаемой

Основные моменты для __str__, __repr__

  1. Мы можем управлять преобразованием в строку в наших собственных классах, используя __str__ и __repr__ методы «dunder».
  2. __repr__ вычисляет« официальное строковое представление объекта» (имеющее всю информацию об объекте) и __str__ используется для« неформального строкового представления объекта».
  3. Если мы не добавим __str__ метод, Python вернется к результату __repr__ при поиске __str__. Поэтому рекомендуется добавлять __repr__ в наши классы.

Итерация: __getitem__, __setitem__, __len__

Встроенные в Python типы list, str и bytes могут использовать оператор среза [] для доступа к диапазону элементов. Реализация __getitem__, __setitem__ в классе позволяет его экземплярам использовать оператор [] (индексатор). Таким образом, методы __getitem__ и __setitem__dunder используются для индексации списков, поиска по словарю или доступа к диапазонам значений. Чтобы лучше понять концепцию, давайте рассмотрим пример, в котором мы создаем собственный настраиваемый список.

import random as ran
class CustomList:
  def __init__(self, num):
    self.my_list = [ran.randrange(1,101,1) for _ in range(num)]
>>> obj = CustomList(5)
>>> obj.my_list
[59, 83, 96, 86, 59]
>>> len(obj)
AttributeError:
>>> for no in obj:
...    print (no)
AttributeError:
>>> obj[1]
AttributeError:

С указанным выше определением класса мы не можем перебирать наш объект, поскольку приведенные выше операторы вызывают AttributeError. Давайте реализуем чертовы методы в нашем классе, чтобы сделать его итеративным.

import random as ran
class CustomList:
  def __init__(self, num):
    self.my_list = [ran.randrange(1,101,1) for _ in range(num)]
  
  def __str__(self):
    return str(self.my_list)
  def __setitem__(self, index, value):
    self.my_list[index] = value
  def __getitem__(self, index):
    return self.my_list[index]
  def __len__(self):
    return len(self.my_list)
>>> obj = CustomList(5)
>>> print(obj)
[59, 83, 96, 86, 59]
>>> len(obj)
5
>>> obj[1]
83
>>> for item in obj:
...    print (item)
59
83
96
86
59

Следовательно, использование dunder-методов __setitem__, __getitem__, __len__ позволяет нам использовать оператор нарезки и делает наш объект итерируемым.
ПРИМЕЧАНИЕ. __iter__ и __next__ dunder-методы также используются для написания повторяемых объектов, но они выходят за рамки обсуждение здесь, и мы обсудим их в отдельном посте. 😄

Вызов объекта: __call__

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

class Person:
    def __init__(self, name, age):
        self.name = name
        self.age = age
    def __call__(self):
        print ('Person: {}, Age: {}'.format(self.name, self.age))
>>> person = Person()
>>> person()
Person: Sarah, Age: 25

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

class Entity:
    '''Callable to update the entity's position.'''

    def __init__(self, size, x, y):
        self.x, self.y = x, y
        self.size = size

    def __call__(self, x, y):
        '''Change the position of the entity.'''
        self.x, self.y = x, y
>>> point = Entity(10, 20)
>>> print(point.x, point.y)
10 20
>>> point(30, 40)
>>> print (point.x, point.y)
30 40

Обычно __call__ используется всякий раз, когда мы хотим предоставить простой интерфейс, напоминающий простую функцию. Однако бывает трудно увидеть то же самое.

Выводы

Методы Dunder могут использоваться для имитации поведения встроенных типов по отношению к определяемым пользователем объектам и являются основной функцией Python, которую следует использовать по мере необходимости. 🙌
Мы коснулись часто используемых приёмов dunder. Эти методы помогают в написании многофункциональных, элегантных и простых в использовании классов. На языке Python доступно множество разнообразных методов. Чтобы узнать о них больше, лучше всего копнуть в Справочной документации по Python.
В заключение отметим, что абсурдная степень контроля, которую обеспечивает метод dunder, иногда заставляет задуматься, что они действительно волшебные 😉