Объяснение магических методов Python
Все, что нам нужно знать о магических методах, которые не такие уж волшебные.
Магические методы - не такие уж и волшебные !! 🃏
В Python имена методов, которые имеют начальные и конечные двойные подчеркивания, зарезервированы для специального использования, например, метод__init__
для конструкторов объектов или метод __call__
, чтобы сделать объект доступным для вызова. Эти методы известны как методы dunder. dunder здесь означает «Double Under (подчеркивание)». Эти опасные методы часто называют магическими, хотя в них нет ничего волшебного. Многим в сообществе Python не нравится это слово (магия), поскольку оно дает ощущение, что использование этих методов не рекомендуется, однако на самом деле все обстоит как раз наоборот.
Зачем оборачивать их двойным подчеркиванием?
Заключение этих функций в двойные подчеркивания с обеих сторон было на самом деле просто способом упростить язык. Создатели Python не хотели красть у нас совершенно хорошие имена методов (например, call
или iter
), но они также не хотели вводить какой-то новый синтаксис, просто чтобы объявить определенные методы «особенными». dunders достигают желаемой цели - делают определенные методы особенными, а также делают их такими же, как и другие простые методы, во всех аспектах, кроме соглашения об именах.
Что следует помнить для dunders
- Назовите их «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)
Давайте рассмотрим различные «глупые» методы, чтобы лучше понять различные функции, предоставляемые 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__
- Мы можем управлять преобразованием в строку в наших собственных классах, используя
__str__
и__repr__
методы «dunder». __repr__
вычисляет« официальное строковое представление объекта» (имеющее всю информацию об объекте) и__str__
используется для« неформального строкового представления объекта».- Если мы не добавим
__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, иногда заставляет задуматься, что они действительно волшебные 😉