Dlaczego Python używa „magicznych metod”?


99

Bawiłem się ostatnio Pythonem i jedną rzeczą, która wydaje mi się nieco dziwna, jest szerokie użycie `` magicznych metod '', np. Aby udostępnić swoją długość, obiekt implementuje metodę def __len__(self), a następnie jest wywoływany, gdy piszesz len(obj).

Zastanawiałem się tylko, dlaczego obiekty po prostu nie definiują len(self)metody i nie są wywoływane bezpośrednio jako element członkowski obiektu, np. obj.len()? Jestem pewien, że muszą istnieć dobre powody, aby Python robił to w taki sposób, ale jako początkujący jeszcze nie odkryłem, czym one są.


4
Myślę, że ogólny powód jest a) historyczny ib) coś podobnego len()lub reversed()stosuje się do wielu typów obiektów, ale metoda taka append()ma zastosowanie tylko do sekwencji itp.
Grant Paul

Odpowiedzi:


64

AFAIK lenjest pod tym względem wyjątkowy i ma historyczne korzenie.

Oto cytat z FAQ :

Dlaczego Python używa metod dla niektórych funkcji (np. List.index ()), a funkcje dla innych (np. Len (lista))?

Głównym powodem jest historia. Funkcje były używane dla tych operacji, które były ogólne dla grupy typów i które miały działać nawet dla obiektów, które w ogóle nie miały metod (np. Krotki). Wygodne jest również posiadanie funkcji, którą można łatwo zastosować do amorficznej kolekcji obiektów, gdy używasz funkcji funkcjonalnych Pythona (map (), apply () et al).

W rzeczywistości implementacja len (), max (), min () jako funkcji wbudowanej to w rzeczywistości mniej kodu niż implementacja ich jako metod dla każdego typu. Można spierać się o pojedyncze przypadki, ale jest to część Pythona i jest już za późno, aby wprowadzić tak fundamentalne zmiany. Funkcje muszą pozostać, aby uniknąć masowego uszkodzenia kodu.

Inne „magiczne metody” ( w folklorze Pythona właściwie nazywane metodami specjalnymi ) mają wiele sensu, a podobne funkcje istnieją w innych językach. Są one głównie używane w przypadku kodu, który jest wywoływany niejawnie, gdy używana jest specjalna składnia.

Na przykład:

  • przeciążone operatory (istnieją w C ++ i innych)
  • konstruktor / destruktor
  • punkty zaczepienia dostępu do atrybutów
  • narzędzia do metaprogramowania

i tak dalej...


2
Python i zasada najmniejszego zdziwienia to dobra lektura, jeśli chodzi o niektóre zalety takiego rozwiązania (chociaż przyznaję, że angielski wymaga pracy). Podstawowa kwestia: pozwala standardowej bibliotece na zaimplementowanie mnóstwa kodu, który staje się bardzo, bardzo przydatny do ponownego wykorzystania, ale nadal można go zastąpić.
jpmc26

20

Z Zen Pythona:

W obliczu niejasności odrzuć pokusę zgadywania.
Powinien być jeden - a najlepiej tylko jeden - oczywisty sposób na zrobienie tego.

Jest to jeden z powodów - z metod niestandardowych, programiści będą wolne, aby wybrać inną nazwę metody, jak getLength(), length(), getlength()albo w ogóle. Python wymusza ścisłe nazewnictwo, aby len()można było używać wspólnej funkcji .

Wszystkie operacje, które są wspólne dla wielu typów obiektów, są umieszczane w metodach magicznych, takich jak __nonzero__, __len__lub __repr__. Są one jednak w większości opcjonalne.

Przeciążanie operatorów również odbywa się za pomocą metod magicznych (np. __le__), Więc sensowne jest używanie ich również do innych typowych operacji.


To przekonujący argument. Bardziej satysfakcjonujące jest to, że „Guido tak naprawdę nie wierzył w OO”… (jak widziałem w innym miejscu).
Andy Hayden

15

Python używa słowa „magiczne metody” , ponieważ te metody naprawdę wykonują magię za program. Jedną z największych zalet korzystania z magicznych metod Pythona jest to, że zapewniają one prosty sposób na sprawienie, by obiekty zachowywały się jak typy wbudowane. Oznacza to, że możesz uniknąć brzydkich, sprzecznych z intuicją i niestandardowych sposobów wykonywania podstawowych operatorów.

Rozważmy następujący przykład:

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'

Daje to błąd, ponieważ typ słownika nie obsługuje dodawania. Teraz rozszerzmy klasę słownika i dodajmy magiczną metodę „__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)

Teraz daje następujący wynik.

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

Tak więc, dodając tę ​​metodę, nagle pojawiła się magia i błąd, który otrzymywałeś wcześniej, zniknął.

Mam nadzieję, że to wszystko wyjaśnia. Więcej informacji można znaleźć w:

Przewodnik po magicznych metodach Pythona (Rafe Kettler, 2012)


9

Niektóre z tych funkcji robią więcej niż jedna metoda byłaby w stanie zaimplementować (bez metod abstrakcyjnych w nadklasie). Na przykład bool()działa mniej więcej tak:

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

Możesz także mieć 100% pewności, że bool()zawsze zwróci True lub False; jeśli polegałeś na metodzie, nie możesz być całkowicie pewien, co otrzymasz.

Niektóre inne funkcje, które stosunkowo skomplikowane wdrożenia (bardziej skomplikowane niż podstawowych metod magicznych są prawdopodobnie) są iter()i cmp(), a wszystkie metody (atrybut getattr, setattra delattr). Rzeczy takie jak intrównież dostęp do magicznych metod podczas stosowania przymusu (możesz zaimplementować __int__), ale wykonuj podwójne obowiązki jako typy. len(obj)jest właściwie jedynym przypadkiem, w którym nie wierzę, że kiedykolwiek się różni obj.__len__().


2
Zamiast tego hasattr()użyłbym try:/ except AttributeError:a zamiast tego if obj.__len__(): return True else: return Falsepowiedziałbym tylko, że return obj.__len__() > 0to tylko stylistyczne rzeczy.
Chris Lutz

W Pythonie 2.6 (do którego bool(x)przywołano x.__nonzero__()) Twoja metoda nie zadziała. instancje bool mają metodę __nonzero__(), a twój kod będzie się wywoływał, gdy obj był bool. Może bool(obj.__bool__())powinieneś być traktowany tak samo jak ty __len__? (A może ten kod faktycznie działa dla Pythona 3?)
Ponkadoodle

Okrągły charakter bool () był celowo absurdalny, aby odzwierciedlić szczególnie kołowy charakter definicji. Istnieje argument, że należy go po prostu uznać za prymitywny.
Ian Bicking

Jedyną różnicą (obecnie) między len(x)i x.__len__()jest to, że pierwszy z nich zgłosi OverflowError dla długości, które przekraczają sys.maxsize, podczas gdy drugi generalnie nie dla typów zaimplementowanych w Pythonie. To raczej błąd niż funkcja (np. Obiekt range w Pythonie 3.2 może w większości obsłużyć dowolnie duże zakresy, ale używanie lenz nimi może się nie powieść. Ich również __len__zawodzi, ponieważ są zaimplementowane w C, a nie w Pythonie)
ncoghlan

4

Nie są to tak naprawdę „magiczne imiona”. To tylko interfejs, który obiekt musi zaimplementować, aby świadczyć daną usługę. W tym sensie nie są one bardziej magiczne niż jakakolwiek predefiniowana definicja interfejsu, którą musisz ponownie zaimplementować.


1

Chociaż przyczyna jest głównie historyczna, w Pythonie istnieją pewne osobliwości, lenktóre sprawiają, że użycie funkcji zamiast metody jest właściwe.

Niektóre operacje w Pythonie są zaimplementowane na przykład jako metody, list.indexa dict.appendpodczas gdy inne są zaimplementowane jako wywołania i metody magiczne, na przykład stri iteri reversed. Te dwie grupy różnią się wystarczająco, więc inne podejście jest uzasadnione:

  1. Są pospolite.
  2. str, intI przyjaciele są typy. Bardziej sensowne jest wywołanie konstruktora.
  3. Implementacja różni się od wywołania funkcji. Na przykład itermoże wywołać, __getitem__jeśli __iter__nie jest dostępny, i obsługuje dodatkowe argumenty, które nie pasują do wywołania metody. Z tego samego powodu it.next()został zmieniony next(it)w ostatnich wersjach Pythona - ma to większy sens.
  4. Niektórzy z nich są bliskimi krewnymi operatorów. Istnieje składnia do wywoływania __iter__i __next__- to się nazywa forpętla. Dla spójności funkcja jest lepsza. I to sprawia, że ​​jest to lepsze dla niektórych optymalizacji.
  5. Niektóre funkcje są po prostu w pewien sposób zbyt podobne do innych - reprdziała jak strdziała. Mając str(x)kontra x.repr()byłoby mylące.
  6. Na przykład niektóre z nich rzadko korzystają z rzeczywistej metody implementacji isinstance.
  7. Niektórzy z nich są faktycznymi operatorami, getattr(x, 'a')to inny sposób działania x.ai getattrmają wiele z wyżej wymienionych cech.

Osobiście nazywam pierwszą grupę metodą metodyczną, a drugą grupę operacyjną. To niezbyt dobre rozróżnienie, ale mam nadzieję, że jakoś pomoże.

Powiedziawszy to, lennie pasuje dokładnie do drugiej grupy. Jest bardziej zbliżony do operacji z pierwszej, z tą różnicą, że jest o wiele bardziej powszechny niż prawie każda z nich. Ale jedyne, co robi, to dzwonienie __len__i jest bardzo blisko L.index. Istnieją jednak pewne różnice. Na przykład __len__może zostać wywołany w celu implementacji innych funkcji, na przykład bool, jeśli metoda została wywołana, lenmożesz zerwać bool(x)z lenmetodą niestandardową , która robi zupełnie inne rzeczy.

Krótko mówiąc, masz zestaw bardzo typowych funkcji, które mogą implementować klasy, a do których można uzyskać dostęp za pośrednictwem operatora, poprzez specjalną funkcję (która zwykle robi więcej niż implementacja, jak zrobiłby to operator), podczas konstruowania obiektu i wszystkie z nich mają wspólne cechy. Cała reszta to metoda. I lenjest pewnym wyjątkiem od tej reguły.


0

Nie ma wiele do dodania do powyższych dwóch postów, ale wszystkie „magiczne” funkcje wcale nie są magiczne. Są częścią modułu __ builtins__, który jest niejawnie / automatycznie importowany podczas uruchamiania interpretera. To znaczy:

from __builtins__ import *

dzieje się za każdym razem przed rozpoczęciem programu.

Zawsze uważałem, że byłoby bardziej poprawne, gdyby Python robił to tylko dla powłoki interaktywnej i wymagał skryptów do importowania różnych części z wbudowanych, których potrzebowali. Prawdopodobnie inna obsługa __ main__ byłaby fajna w powłokach niż interaktywna. W każdym razie sprawdź wszystkie funkcje i zobacz, jak to jest bez nich:

dir (__builtins__)
...
del __builtins__
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.