Чому 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 (список))?

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

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

Інші "магічні методи" (насправді в фольклорі Python називають спеціальними методами ) мають багато сенсу, а подібні функціональні можливості існують і в інших мовах. В основному вони використовуються для коду, який неявно викликається, коли використовується спеціальний синтаксис.

Наприклад:

  • перевантажені оператори (існують у C ++ та інших)
  • конструктор / деструктор
  • гачки для доступу до атрибутів
  • інструменти для метапрограмування

і так далі...


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

20

Із Дзен Пітона:

В умовах неоднозначності відмовтеся від спокуси здогадатися.
Повинно бути один - і бажано лише один - очевидний спосіб зробити це.

Це одна з причин - з допомогою призначених для користувача методів, розробники могли б вільно вибрати інше ім'я методи, як 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'}

Таким чином, додавши цей метод, раптом сталася магія, і помилка, яку ви робили раніше, відпала.

Сподіваюся, це вам зрозуміло. Для отримання додаткової інформації зверніться до:

Посібник з магічних методів Пітона (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)згадували btw 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__він недоступний, і підтримує додаткові аргументи, які не вписуються у виклик методу. З тієї ж причини 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є дещо винятком із цього правила.


0

До двох вищезгаданих дописів додати не багато, але всі "магічні" функції зовсім не є магічними. Вони є частиною модуля __ buildins__, який імпліцитно / автоматично імпортується при запуску перекладача. Тобто:

from __builtins__ import *

відбувається щоразу перед запуском програми.

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

dir (__builtins__)
...
del __builtins__
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.