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

Магические методы - не такие уж и магические!

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

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

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

Что нужно помнить о дублерах

  • Назовите их “dunders” – Поскольку в них нет ничего тайного или магического. Такие термины, как “магия” делает их более сложными, чем они есть на самом деле.
  • Реализация дандеров по желанию — Это основная функция Python, и ее следует использовать по мере необходимости.
  • Изобретать собственные дандеры крайне не рекомендуется — Лучше не использовать имена, начинающиеся и заканчивающиеся двойным подчеркиванием, в наших программах, чтобы избежать конфликтов с нашими собственными методами и атрибутами.

Методы 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)

Давайте рассмотрим различные методы "dunder", чтобы лучше понять различные возможности, которые предоставляет 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__ объект (в данном случае человек) передается как "self". Другие аргументы, используемые в вызове метода, передаются как остальные аргументы функции.

Представление объектов: __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 механизм представления объектов в виде строк. Поэтому давайте добавим в наш класс методы "dunder", чтобы описать наш объект так, как мы хотим.

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__

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

import random as ranclass 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. Давайте реализуем методы dunder в нашем классе, чтобы сделать его итерируемым.

import random as ranclass 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

Поэтому использование методов __setitem__, __getitem__, __len__ dunder позволяет нам использовать оператор нарезки и делает наш объект итерабельным.
NOTE: __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 существует множество методов dunder. Чтобы прочитать о них больше, лучше всего обратиться к справочной документации Python .
В заключение, абсурдная степень контроля, которую обеспечивает метод dunder, заставляет нас иногда задумываться о том, что они действительно волшебные.

Вернуться на верх