Почему Python использует «волшебные методы»?


99

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

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


4
Я думаю, что основная причина - а) историческая и б) что-то подобное len()или reversed()применимое ко многим типам объектов, но такой метод append()применяется только к последовательностям и т. Д.
Грант Пол

Ответы:


64

AFAIK lenявляется особенным в этом отношении и имеет исторические корни.

Вот цитата из FAQ :

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

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

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

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

Например:

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

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


2
Python и принцип наименьшего удивления - хорошее прочтение некоторых преимуществ такого подхода к Python (хотя я признаю, что английский требует доработки). Основной момент: он позволяет стандартной библиотеке реализовывать тонну кода, который становится очень и очень многоразовым, но все же переопределяемым.
jpmc26

20

Из дзен Python:

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

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

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

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


Это веский аргумент. Больше удовлетворения, что «Гвидо на самом деле не верил в объектно-ориентированный подход» .... (как я видел, утверждалось в другом месте).
Энди Хайден

15

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 (Rafe Kettler, 2012)


9

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


2
Вместо этого hasattr()я бы использовал try:/ except AttributeError:и вместо этого if obj.__len__(): return True else: return Falseя бы просто сказал, return obj.__len__() > 0но это просто стилистические вещи.
Крис Лутц

В python 2.6 (который, кстати, bool(x)упоминается x.__nonzero__()) ваш метод не будет работать. У экземпляров bool есть метод __nonzero__(), и ваш код будет продолжать называть себя, если obj будет bool. Может bool(obj.__bool__())быть, к вам следует относиться так же, как и к вам __len__? (Или этот код действительно работает для Python 3?)
Ponkadoodle

Круговой характер bool () был несколько намеренно абсурдным, чтобы отразить своеобразный круговой характер определения. Есть аргумент, что это просто нужно считать примитивом.
Ян Бикинг

Единственное различие (в настоящее время) между len(x)и x.__len__()заключается в том, что первый вызовет OverflowError для длин, которые превышают sys.maxsize, тогда как последний обычно не будет для типов, реализованных в Python. Однако это скорее ошибка, чем функция (например, объект диапазона Python 3.2 в основном может обрабатывать произвольно большие диапазоны, но использование lenс ними может привести к сбою. Их также __len__не удается, поскольку они реализованы на C, а не на Python)
ncoghlan

4

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


1

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

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

  1. Они обычные.
  2. str, intа друзья - типы. Имеет смысл вызвать конструктор.
  3. Реализация отличается от вызова функции. Например, iterможет вызывать, __getitem__если __iter__он недоступен, и поддерживает дополнительные аргументы, которые не подходят для вызова метода. По той же причине в последних версиях Python it.next()был изменен на next(it)- это имеет больше смысла.
  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это своего рода исключение из этого правила.


0

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

from __builtins__ import *

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

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

dir (__builtins__)
...
del __builtins__
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.