Kiedy w Pythonie powinienem używać funkcji zamiast metody?

118

Zen of Python mówi, że powinien istnieć tylko jeden sposób robienia rzeczy - jednak często napotykam problem z podjęciem decyzji, kiedy użyć funkcji, a kiedy metody.

Weźmy trywialny przykład - obiekt ChessBoard. Powiedzmy, że potrzebujemy sposobu, aby uzyskać wszystkie legalne ruchy króla dostępne na planszy. Czy piszemy ChessBoard.get_king_moves () czy get_king_moves (chess_board)?

Oto kilka powiązanych pytań, na które spojrzałem:

Odpowiedzi, które otrzymałem, były w dużej mierze niejednoznaczne:

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.

Chociaż jest to interesujące, powyższe nie mówi zbyt wiele o tym, jaką strategię przyjąć.

Jest to jeden z powodów - w przypadku metod niestandardowych programiści mogliby wybrać inną nazwę metody, na przykład getLength (), length (), getlength () lub jakąkolwiek inną. Python wymusza ścisłe nazewnictwo, aby można było użyć wspólnej funkcji len ().

Nieco bardziej interesujące. Uważam, że funkcje są w pewnym sensie Pythonową wersją interfejsów.

Na koniec od samego Guido :

Rozmowa o zdolnościach / interfejsach sprawiła, że ​​pomyślałem o niektórych z naszych „nieuczciwych” nazw metod specjalnych. W dokumencie Language Reference jest napisane: „Klasa może implementować pewne operacje, które są wywoływane za pomocą specjalnej składni (takie jak operacje arytmetyczne lub indeksowanie i wycinanie), definiując metody o specjalnych nazwach”. Ale są wszystkie te metody ze specjalnymi nazwami, takimi jak __len__lub, __unicode__które wydają się być dostarczane z korzyścią dla funkcji wbudowanych, a nie dla obsługi składni. Przypuszczalnie w Pythonie interfejsu opartego na te metody zamieni się regularnie wymienionych metod działania w ABC, tak że __len__stałby się

class container:
  ...
  def len(self):
    raise NotImplemented

Chociaż myśląc o tym więcej, nie rozumiem, dlaczego wszystkie operacje składniowe nie wywoływałyby po prostu odpowiedniej metody o normalnej nazwie na określonym ABC. „ <”, Na przykład, przypuszczalnie wywołać „ object.lessthan” (albo " comparable.lessthan„). Więc kolejną korzyścią byłaby możliwość odzwyczajenia Pythona od tej dziwacznej nazwy, co wydaje mi się poprawą HCI .

Hm. Nie jestem pewien, czy się zgadzam (wyobraź sobie :-).

Istnieją dwa fragmenty „uzasadnienia Pythona”, które chciałbym najpierw wyjaśnić.

Przede wszystkim wybrałem len (x) zamiast x.len () z powodów związanych z HCI ( def __len__()pojawiło się znacznie później). W rzeczywistości istnieją dwa powiązane ze sobą powody, oba HCI:

(a) W przypadku niektórych operacji notacja przedrostków po prostu czyta lepiej niż operacje na postfiksach - prefiksach (i wrostkach!) mają długą tradycję w matematyce, która lubi notacje, gdzie wizualizacje pomagają matematykowi w myśleniu o problemie. Porównaj łatwość, z jaką przepisujemy formułę x*(a+b)na, x*a + x*bz niezdarnością robienia tego samego przy użyciu surowej notacji OO.

(b) Kiedy czytam kod, który mówi, len(x)że wiem , że prosi o długość czegoś. To mówi mi o dwóch rzeczach: wynik jest liczbą całkowitą, a argumentem jest jakiś kontener. Wręcz przeciwnie, kiedy czytam x.len(), muszę już wiedzieć, że xjest to jakiś kontener implementujący interfejs lub dziedziczący po klasie, która ma standard len(). Świadek zamieszanie niekiedy mają kiedy klasa, która nie realizuje mapowanie ma get()lub keys() metody, lub coś, co nie jest plikiem ma write()metody.

Mówiąc to samo w inny sposób, widzę „len” jako operację wbudowaną . Nie chciałbym tego stracić. Nie jestem pewien, czy miałeś to na myśli, czy nie, ale „def len (self):…” z pewnością brzmi tak, jakbyś chciał zdegradować to do zwykłej metody. Jestem zdecydowanie -1 w tej sprawie.

Drugie uzasadnienie Pythona, które obiecałem wyjaśnić, to powód, dla którego wybrałem specjalne metody wyszukiwania, __special__a nie tylko special. Spodziewałem się wielu operacji, które klasy mogłyby chcieć przesłonić, niektóre standardowe (np. __add__Lub __getitem__), niektóre nie tak standardowe (np. Pikle __reduce__przez długi czas nie były w ogóle obsługiwane w kodzie C). Nie chciałem, aby te specjalne operacje używały zwykłych nazw metod, ponieważ wcześniej istniejące klasy lub klasy napisane przez użytkowników bez encyklopedycznej pamięci dla wszystkich metod specjalnych byłyby podatne na przypadkowe zdefiniowanie operacji, których nie zamierzali zaimplementować , co może mieć katastrofalne skutki. Ivan Krstić wyjaśnił to bardziej zwięźle w swoim przesłaniu, które dotarło po tym, jak to wszystko napisałem.

- --Guido van Rossum (strona domowa: http://www.python.org/~guido/ )

Rozumiem to, że w niektórych przypadkach notacja przedrostków ma po prostu więcej sensu (tj. Duck.quack ma więcej sensu niż szarlatan (Duck) z językowego punktu widzenia) i znowu funkcje pozwalają na „interfejsy”.

W takim przypadku przypuszczam, że zaimplementowałbym get_king_moves wyłącznie na podstawie pierwszego punktu Guido. Ale to wciąż pozostawia wiele otwartych pytań dotyczących, powiedzmy, implementacji klasy stosu i kolejki z podobnymi metodami push i pop - czy powinny to być funkcje czy metody? (tutaj zgadywałbym funkcje, bo naprawdę chcę zasygnalizować interfejs push-pop)

TLDR: Czy ktoś może wyjaśnić, jaka powinna być strategia podejmowania decyzji, kiedy używać funkcji, a kiedy metod?

Ceasar Bautista
źródło
2
Ech, zawsze uważałem to za całkowicie arbitralne. Duck typing pozwala na niejawne „interfejsy”, nie ma większego znaczenia, czy masz, X.frobczy X.__frob__wolnostojące frob.
Cat Plus Plus
2
Chociaż w większości się z tobą zgadzam, w zasadzie twoja odpowiedź nie jest Pythonic. Przypomnijmy: „W obliczu dwuznaczności odrzuć pokusę zgadywania”. (Oczywiście terminy by to zmieniły, ale robię to dla zabawy / samodoskonalenia.)
Ceasar Bautista,
To jedna rzecz, której nie lubię w Pythonie. Wydaje mi się, że jeśli zamierzasz wymusić typowanie rzutowania, jak int na string, po prostu zrób z tego metodę. Irytujące jest zamykanie go w parens i czasochłonne.
Matt,
1
To jest najważniejszy powód, dla którego nie lubię Pythona: nigdy nie wiesz, czy musisz szukać funkcji, czy metody, gdy chcesz coś osiągnąć. A nawet staje się bardziej skomplikowany, gdy używasz dodatkowych bibliotek z nowymi typami danych, takimi jak wektory lub ramki danych.
vonjd
1
„Zen Pythona mówi, że powinien istnieć tylko jeden sposób robienia rzeczy”, z wyjątkiem tego, że tak nie jest.
Gented

Odpowiedzi:

84

Moja ogólna zasada jest taka - czy operacja jest wykonywana na obiekcie, czy przez obiekt?

jeśli jest to wykonywane przez obiekt, powinna to być operacja składowa. Jeśli mogłoby to odnosić się również do innych rzeczy, lub jest wykonywane przez coś innego do obiektu, to powinno być funkcją (lub być może członkiem czegoś innego).

Wprowadzając programowanie, tradycyjnym (aczkolwiek niepoprawnym wdrożeniem) jest opisywanie obiektów w kategoriach obiektów świata rzeczywistego, takich jak samochody. Wspomniałeś o kaczce, więc chodźmy z tym.

class duck: 
    def __init__(self):pass
    def eat(self, o): pass 
    def crap(self) : pass
    def die(self)
    ....

W kontekście analogii „obiekty są rzeczywistymi rzeczami” „poprawne jest” dodanie metody klasy do wszystkiego, co obiekt może zrobić. Więc powiedz, że chcę zabić kaczkę, czy dodać .kill () do kaczki? Nie ... o ile wiem, zwierzęta nie popełniają samobójstw. Dlatego jeśli chcę zabić kaczkę, powinienem zrobić to:

def kill(o):
    if isinstance(o, duck):
        o.die()
    elif isinstance(o, dog):
        print "WHY????"
        o.die()
    elif isinstance(o, nyancat):
        raise Exception("NYAN "*9001)
    else:
       print "can't kill it."

Odchodząc od tej analogii, dlaczego używamy metod i klas? Ponieważ chcemy zawierać dane i, miejmy nadzieję, ustrukturyzować nasz kod w taki sposób, aby w przyszłości można go było wielokrotnie używać i rozszerzać. To prowadzi nas do pojęcia hermetyzacji, które jest tak bliskie projektowi OO.

Zasada hermetyzacji jest tak naprawdę tym, do czego to się sprowadza: jako projektant powinieneś ukryć wszystko, co dotyczy implementacji i elementów wewnętrznych klas, do których dostęp nie jest absolutnie konieczny dla jakiegokolwiek użytkownika lub innego programisty. Ponieważ mamy do czynienia z instancjami klas, ogranicza się to do „jakie operacje są kluczowe w tej instancji ”. Jeśli operacja nie jest specyficzna dla instancji, nie powinna być funkcją składową.

TL; DR : co powiedział @Bryan. Jeśli działa na instancji i potrzebuje dostępu do danych, które są wewnętrzne dla instancji klasy, powinna być funkcją składową.

arrdem
źródło
Krótko mówiąc, funkcje nie-składowe działają na niezmiennych obiektach, zmienne obiekty używają funkcji składowych? (A może jest to zbyt surowe uogólnienie? Dotyczy to tylko niektórych dzieł, ponieważ niezmienne typy nie mają stanu).
Ceasar Bautista,
1
Ze ścisłego punktu widzenia OOP myślę, że jest to sprawiedliwe. Ponieważ Python ma zarówno zmienne publiczne, jak i prywatne (zmienne o nazwach rozpoczynających się od __) i zapewnia zerową gwarantowaną ochronę dostępu, w przeciwieństwie do Javy, nie ma żadnych absolutów tylko dlatego, że debatujemy nad liberalnym językiem. Jednak w mniej liberalnym języku, takim jak Java, należy pamiętać, że funkcje getFoo () ad setFoo () są normą, więc niezmienność nie jest absolutna. Kod klienta nie może po prostu przypisywać członkom.
arrdem
1
@Ceasar To nieprawda. Niezmienne obiekty mają stan; w przeciwnym razie nie byłoby nic, co mogłoby odróżnić jakąkolwiek liczbę całkowitą od innych. Niezmienne obiekty nie zmieniają swojego stanu. Ogólnie rzecz biorąc, sprawia to, że upublicznianie całego państwa jest znacznie mniej problematyczne. W takim ustawieniu znacznie łatwiej jest w znaczący sposób manipulować niezmiennym, ogólnodostępnym obiektem z funkcjami; nie ma „przywileju” bycia metodą.
Ben
1
@CeasarBautista tak, Ben ma rację. Istnieją trzy „główne” szkoły projektowania kodu: 1) nie zaprojektowany, 2) OOP i 3) funkcjonalny. w funkcjonalnym stylu nie ma w ogóle stanów. W ten sposób widzę większość zaprojektowanego kodu Pythona, pobiera dane wejściowe i generuje dane wyjściowe z kilkoma efektami ubocznymi. Punkt OOP jest to, że wszystko ma swój stan. Klasy są kontenerem dla stanów, dlatego funkcje „składowe” są kodem zależnym od stanu i opartym na efektach ubocznych, który ładuje stan zdefiniowany w klasie po każdym wywołaniu. Python ma tendencję do odchudzania funkcjonalnego, stąd preferowany jest kod niebędący składnikiem.
arrdem
1
jeść (siebie), gówno (siebie), umrzeć (siebie). hahahaha
Wapiti
27

Skorzystaj z zajęć, jeśli chcesz:

1) Oddziel wywołanie kodu od szczegółów implementacji - korzystając z abstrakcji i hermetyzacji .

2) Gdy chcesz być substytutem innych obiektów - wykorzystując polimorfizm .

3) Gdy chcesz ponownie użyć kodu dla podobnych obiektów - korzystając z dziedziczenia .

Użyj funkcji dla wywołań, które mają sens w wielu różnych typach obiektów - na przykład wbudowane funkcje len i repr mają zastosowanie do wielu rodzajów obiektów.

Biorąc to pod uwagę, wybór czasami sprowadza się do gustu. Pomyśl o tym, co jest najwygodniejsze i najbardziej czytelne w przypadku typowych rozmów. Na przykład, który byłby lepszy (x.sin()**2 + y.cos()**2).sqrt()czy sqrt(sin(x)**2 + cos(y)**2)?

Raymond Hettinger
źródło
7

Oto prosta zasada: jeśli kod działa na pojedynczą instancję obiektu, użyj metody. Jeszcze lepiej: użyj metody, chyba że istnieje ważny powód, aby zapisać ją jako funkcję.

W swoim konkretnym przykładzie chcesz, aby wyglądało to tak:

chessboard = Chessboard()
...
chessboard.get_king_moves()

Nie myśl nad tym. Zawsze używaj metod, aż dojdzie do momentu, w którym powiesz sobie „nie ma sensu robić tego metodą”, w takim przypadku możesz utworzyć funkcję.

Bryan Oakley
źródło
2
Czy możesz wyjaśnić, dlaczego domyślnie wybierasz metody zamiast funkcji? (Czy możesz wyjaśnić, czy ta reguła nadal ma sens w przypadku metod stack i queue / pop i push?)
Ceasar Bautista
11
Ta praktyczna zasada nie ma sensu. Sama biblioteka standardowa może być wskazówką, kiedy używać klas, a kiedy funkcji. heapq i math to funkcje, ponieważ działają na normalnych obiektach Pythona (elementy zmiennoprzecinkowe i listy) oraz ponieważ nie muszą utrzymywać złożonego stanu, jak robi to moduł losowy .
Raymond Hettinger
1
Czy twierdzisz, że reguła nie ma sensu, ponieważ biblioteka standardowa ją narusza? Myślę, że ten wniosek nie ma sensu. Po pierwsze, praktyczne zasady są po prostu takie - nie są regułami absolutnymi. Ponadto duża część biblioteki standardowej przestrzega tej praktycznej zasady. OP szukał kilku prostych wskazówek i myślę, że moja rada jest bardzo dobra dla kogoś, kto dopiero zaczyna. Doceniam jednak twój punkt widzenia.
Bryan Oakley
Cóż, STL ma ku temu dobre powody. Dla matematyki i współpracy oczywiście bezcelowe jest posiadanie klasy, ponieważ nie mamy dla niej żadnego stanu (w innych językach są to klasy z tylko funkcjami statycznymi). W przypadku rzeczy, które działają na kilku różnych kontenerach, takich jak powiedzmy len(), można również argumentować, że projekt ma sens, chociaż osobiście uważam, że funkcje też nie byłyby tam takie złe - mielibyśmy po prostu inną konwencję, która len()zawsze musi zwracać integer (ale ze wszystkimi dodatkowymi problemami backcomp nie polecałbym tego również dla Pythona)
Voo
-1, ponieważ ta odpowiedź jest całkowicie arbitralna: zasadniczo próbujesz wykazać, że X jest lepszy niż Y, zakładając, że X jest lepszy niż Y.
gented
7

Zwykle myślę o przedmiocie jak o osobie.

Atrybuty to imię i nazwisko osoby, wzrost, rozmiar buta itp.

Metody i funkcje to operacje, które osoba może wykonać.

Jeśli operacja mogłaby być wykonana przez jakąkolwiek starą osobę, bez wymagania czegokolwiek unikalnego dla tej jednej konkretnej osoby (i bez zmiany czegokolwiek na tej jednej konkretnej osobie), to jest to funkcja i powinna być zapisana jako taka.

Jeśli operacja działa na osobę (np. Jedzenie, chodzenie, ...) lub wymaga czegoś wyjątkowego dla tej osoby (np. Taniec, pisanie książki, ...), to powinna to być metoda .

Oczywiście nie zawsze jest to trywialne, aby przetłumaczyć to na konkretny obiekt, z którym pracujesz, ale uważam, że jest to dobry sposób, aby o tym pomyśleć.

Thriveth
źródło
ale możesz zmierzyć wzrost każdej starszej osoby, więc zgodnie z tą logiką powinno być height(person), prawda person.height?
endolit
@endolith Jasne, ale powiedziałbym, że wysokość jest lepsza jako atrybut, ponieważ nie musisz wykonywać żadnej wymyślnej pracy, aby go odzyskać. Pisanie funkcji pobierającej liczbę wydaje się niepotrzebnym przeskakiwaniem.
Thriveth
@endolith Z drugiej strony, jeśli osoba jest dzieckiem, które rośnie i zmienia swój wzrost, byłoby oczywiste, że zadba o to metoda, a nie funkcja.
Thriveth
To nie jest prawdą. A co jeśli masz are_married(person1, person2)? To zapytanie jest bardzo ogólne i dlatego powinno być funkcją, a nie metodą.
Pithikos
@Pithikos Jasne, ale dotyczy to również czegoś więcej niż tylko atrybutu lub czynności wykonanej na jednym obiekcie (osobie), ale raczej relacji między dwoma obiektami.
Thriveth
5

Generalnie używam klas do implementacji logicznego zestawu możliwości dla jakiejś rzeczy , tak że w pozostałej części mojego programu mogę o tym myśleć , nie martwiąc się o wszystkie małe problemy, które składają się na jej implementację.

Wszystko, co jest częścią tej podstawowej abstrakcji mówiącej o tym, „co można z czymś zrobić ”, powinno być zwykle metodą. Zwykle obejmuje to wszystko, co może coś zmienić , ponieważ stan danych wewnętrznych jest zwykle uważany za prywatny i nie jest częścią logicznej idei „co można z rzeczą zrobić ”.

Kiedy dochodzisz do operacji wyższego poziomu, zwłaszcza jeśli dotyczą wielu rzeczy , stwierdzam, że są one zwykle wyrażane w najbardziej naturalny sposób jako funkcje, jeśli można je zbudować z publicznej abstrakcji rzeczy bez konieczności specjalnego dostępu do elementów wewnętrznych (chyba że są one) metody innego obiektu). Ma to tę dużą zaletę, że kiedy zdecyduję się całkowicie przepisać wewnętrzne zasady działania mojej rzeczy (bez zmiany interfejsu), mam po prostu mały podstawowy zestaw metod do przepisania, a następnie wszystkie funkcje zewnętrzne napisane pod kątem tych metod będzie po prostu działać. Uważam, że upieranie się, że wszystkie operacje związane z klasą X są metodami w klasie X, prowadzi do zbyt skomplikowanych klas.

Zależy to jednak od kodu, który piszę. W przypadku niektórych programów modeluję je jako zbiór obiektów, których interakcje prowadzą do zachowania programu; tutaj najważniejsza funkcjonalność jest ściśle powiązana z pojedynczym obiektem, a więc jest implementowana w metodach z rozproszeniem funkcji użytkowych. Dla innych programów najważniejszy jest zestaw funkcji, które manipulują danymi, a klasy są używane tylko do implementacji naturalnych „typów kaczych”, którymi te funkcje zajmują się.

Ben
źródło
0

Można powiedzieć, że „w obliczu dwuznaczności odrzućcie pokusę zgadywania”.

Jednak to nawet nie domysł. Masz absolutną pewność, że wyniki obu podejść są takie same, ponieważ rozwiązują Twój problem.

Uważam, że dobrze jest mieć wiele sposobów osiągania celów. Pokornie powiedziałbym, tak jak inni użytkownicy, aby zastosować ten, który „smakuje lepiej” / jest bardziej intuicyjny pod względem języka.

João Silva
źródło