Почему Python использует «магические методы»?

Я недавно играл с Python, и одна вещь, которую я нахожу немного странной, - это широкое использование «магических методов», например. чтобы сделать свою длину доступной, объект реализует метод def __len__(self), а затем вызывается при написании len(obj).

Мне просто интересно, почему объекты просто не определяют метод len(self) и вызывают его непосредственно как член объекта, например. obj.len()? Я уверен, что должны быть веские причины для того, чтобы Python делал это так, как он делает, но как новичок я еще не понял, что они из себя представляют.


person Greg Beech    schedule 17.04.2010    source источник
comment
Я думаю, что общая причина а) историческая и б) что-то вроде len() или reversed() применяется ко многим типам объектов, но такой метод, как append(), применяется только к последовательностям и т. д.   -  person Grant Paul    schedule 17.04.2010


Ответы (7)


Насколько я знаю, len в этом отношении особенный и имеет исторические корни.

Вот цитата из часто задаваемых вопросов:

Почему Python использует методы для одних функций (например, list.index()), а функции — для других (например, len(list))?

Основная причина — история. Функции использовались для тех операций, которые были общими для группы типов и предназначались для работы даже с объектами, у которых вообще не было методов (например, кортежи). Также удобно иметь функцию, которую можно легко применить к аморфному набору объектов при использовании функциональных возможностей Python (map(), apply() и др.).

На самом деле, реализация len(), max(), min() как встроенной функции на самом деле требует меньше кода, чем реализация их как методов для каждого типа. Можно придираться к отдельным случаям, но это часть Python, и сейчас слишком поздно вносить такие фундаментальные изменения. Функции должны оставаться, чтобы избежать массовой поломки кода.

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

Например:

  • перегруженные операторы (существуют в C++ и др.)
  • конструктор/деструктор
  • хуки для доступа к атрибутам
  • инструменты для метапрограммирования

и так далее...

person Eli Bendersky    schedule 17.04.2010
comment
Python и принцип наименьшего удивления — хорошее чтение потому что некоторые преимущества Python таковы (хотя я признаю, что английский язык нуждается в доработке). Основной момент: это позволяет стандартной библиотеке реализовать массу кода, который становится очень, очень многоразовым, но все же переопределяемым. - person jpmc26; 16.01.2014

Из дзен Python:

Перед лицом двусмысленности откажитесь от искушения угадать.
Должен быть один — и желательно только один — очевидный способ сделать это.

Это одна из причин — с помощью пользовательских методов разработчики могут свободно выбирать другое имя метода, например getLength(), length(), getlength() или любое другое. Python применяет строгие имена, чтобы можно было использовать общую функцию len().

Все операции, которые являются общими для многих типов объектов, помещаются в магические методы, такие как __nonzero__, __len__ или __repr__. Однако в основном они необязательны.

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

person AndiDog    schedule 17.04.2010
comment
Это убедительный аргумент. Более удовлетворительно то, что Гвидо на самом деле не верил в ОО.... (как я видел в другом месте). - person Andy Hayden; 01.08.2016

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

Рассмотрим следующий пример:

dict1 = {1 : "ABC"}
dict2 = {2 : "EFG"}

dict1 + dict2
Traceback (most recent call last):
  File "python", line 1, in <module>
TypeError: unsupported operand type(s) for +: 'dict' and 'dict'

Это дает ошибку, потому что тип словаря не поддерживает сложение. Теперь давайте расширим класс словаря и добавим магический метод "__add__":

class AddableDict(dict):

    def __add__(self, otherObj):
        self.update(otherObj)
        return AddableDict(self)


dict1 = AddableDict({1 : "ABC"})
dict2 = AddableDict({2 : "EFG"})

print (dict1 + dict2)

Теперь он дает следующий вывод.

{1: 'ABC', 2: 'EFG'}

Таким образом, добавив этот метод, внезапно произошло волшебство, и ошибка, которую вы получали ранее, исчезла.

Я надеюсь, это проясняет вам ситуацию. Для получения дополнительной информации см.:

Руководство по магическим методам Python (Рэйф Кеттлер, 2012 г.)

person Mangu Singh Rajpurohit    schedule 06.09.2016

Некоторые из этих функций делают больше, чем может реализовать один метод (без абстрактных методов в суперклассе). Например, bool() действует примерно так:

def bool(obj):
    if hasattr(obj, '__nonzero__'):
        return bool(obj.__nonzero__())
    elif hasattr(obj, '__len__'):
        if obj.__len__():
            return True
        else:
            return False
    return True

Вы также можете быть на 100 % уверены, что bool() всегда будет возвращать значение True или False; если бы вы полагались на метод, вы не могли бы быть полностью уверены, что получите обратно.

Некоторые другие функции, которые имеют относительно сложную реализацию (более сложную, чем базовые магические методы), это iter() и cmp(), а также все методы атрибутов (getattr, setattr и delattr). Такие вещи, как int, также получают доступ к магическим методам при выполнении приведения (вы можете реализовать __int__), но выполняют двойную функцию как типы. len(obj) на самом деле единственный случай, когда я не верю, что он когда-либо отличается от obj.__len__().

person Ian Bicking    schedule 17.04.2010
comment
Вместо hasattr() я бы использовал try: / except AttributeError:, а вместо if obj.__len__(): return True else: return False я бы просто сказал return obj.__len__() > 0, но это чисто стилистические вещи. - person Chris Lutz; 17.04.2010
comment
В python 2.6 (кстати, bool(x) ссылается на x.__nonzero__()) ваш метод не будет работать. Экземпляры bool имеют метод __nonzero__(), и ваш код будет продолжать вызывать себя, как только obj станет логическим. Возможно, к bool(obj.__bool__()) следует относиться так же, как вы относились к __len__? (Или этот код действительно работает для Python 3?) - person Ponkadoodle; 20.04.2010
comment
Циклическая природа bool() была намеренно абсурдной, чтобы отразить своеобразную циклическую природу определения. Есть аргумент, что его следует просто считать примитивным. - person Ian Bicking; 21.04.2010
comment
Единственная разница (в настоящее время) между len(x) и x.__len__() заключается в том, что первый вызовет OverflowError для длин, превышающих sys.maxsize, а последний обычно не будет вызывать для типов, реализованных в Python. Однако это скорее ошибка, чем функция (например, объект диапазона Python 3.2 может в основном обрабатывать произвольно большие диапазоны, но использование с ними len может привести к сбою. Их __len__ также не работает, поскольку они реализованы на C, а не на Python) - person ncoghlan; 05.03.2011

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

person Stefano Borini    schedule 17.04.2010

Хотя причина в основном историческая, в Python len есть некоторые особенности, которые делают целесообразным использование функции вместо метода.

Некоторые операции в Python реализованы как методы, например list.index и dict.append, тогда как другие реализованы как вызываемые объекты и магические методы, например str, iter и reversed. Эти две группы достаточно различаются, поэтому оправдан различный подход:

  1. Они распространены.
  2. str, int и друзья - типы. Имеет смысл вызвать конструктор.
  3. Реализация отличается от вызова функции. Например, iter может вызвать __getitem__, если __iter__ недоступен, и поддерживает дополнительные аргументы, которые не подходят для вызова метода. По той же причине it.next() было изменено на next(it) в последних версиях Python — это имеет больше смысла.
  4. Некоторые из них являются близкими родственниками операторов. Есть синтаксис для вызова __iter__ и __next__ — он называется циклом for. Для согласованности функция лучше. И это делает его лучше для определенных оптимизаций.
  5. Некоторые функции просто слишком похожи на остальные — repr действует так же, как str. Наличие str(x) против x.repr() сбивает с толку.
  6. Некоторые из них редко используют фактический метод реализации, например isinstance.
  7. Некоторые из них являются настоящими операторами, getattr(x, 'a') — еще один способ выполнения x.a, а getattr обладает многими из вышеупомянутых качеств.

Лично я называю первую группу методоподобной, а вторую группу оператороподобной. Это не очень хорошее различие, но я надеюсь, что оно как-то поможет.

Сказав это, len точно не вписывается во вторую группу. Он более близок к операциям из первого, с той лишь разницей, что встречается гораздо чаще, чем почти любой из них. Но единственное, что он делает, это вызывает __len__, и это очень близко к L.index. Однако есть некоторые отличия. Например, __len__ может быть вызван для реализации других функций, таких как bool, если бы метод назывался len, вы могли бы сломать bool(x) с помощью пользовательского метода len, который делает совершенно другое.

Короче говоря, у вас есть набор очень общих функций, которые могут быть реализованы классами, к которым можно получить доступ через оператор, через специальную функцию (которая обычно делает больше, чем реализация, как это сделал бы оператор), во время создания объекта, и все они разделяют некоторые общие черты. Все остальное - метод. И len является своего рода исключением из этого правила.

person Rosh Oxymoron    schedule 05.03.2011

К двум вышеприведенным постам добавить особо нечего, но все «магические» функции на самом деле вовсе не волшебные. Они являются частью модуля __builtins__, который неявно/автоматически импортируется при запуске интерпретатора. То есть:

from __builtins__ import *

происходит каждый раз перед запуском вашей программы.

Я всегда думал, что было бы правильнее, если бы Python делал это только для интерактивной оболочки и требовал скриптов для импорта различных частей из необходимых им встроенных модулей. Также, вероятно, было бы неплохо по-разному обрабатывать __ main__ в оболочках по сравнению с интерактивными. В любом случае, проверьте все функции и посмотрите, каково это без них:

dir (__builtins__)
...
del __builtins__
person user318904    schedule 19.04.2010