In Python, quando dovrei usare una funzione invece di un metodo?


118

Lo Zen di Python afferma che dovrebbe esserci un solo modo per fare le cose, ma spesso mi imbatto nel problema di decidere quando usare una funzione rispetto a quando usare un metodo.

Facciamo un esempio banale: un oggetto ChessBoard. Diciamo che abbiamo bisogno di un modo per ottenere tutte le mosse legali di King disponibili sul tabellone. Scriviamo ChessBoard.get_king_moves () o get_king_moves (chess_board)?

Ecco alcune domande correlate che ho esaminato:

Le risposte che ho ottenuto sono state in gran parte inconcludenti:

Perché Python usa metodi per alcune funzionalità (ad esempio list.index ()) ma funzioni per altre (ad esempio len (list))?

La ragione principale è la storia. Le funzioni venivano usate per quelle operazioni che erano generiche per un gruppo di tipi e che dovevano funzionare anche per oggetti che non avevano affatto metodi (es. Tuple). È anche conveniente avere una funzione che può essere prontamente applicata a una raccolta amorfa di oggetti quando si utilizzano le caratteristiche funzionali di Python (map (), apply () e altri).

In effetti, implementare len (), max (), min () come funzione incorporata è in realtà meno codice rispetto all'implementazione come metodi per ciascun tipo. Si può cavillare su casi individuali, ma fa parte di Python ed è troppo tardi per apportare modifiche così fondamentali ora. Le funzioni devono rimanere per evitare una massiccia rottura del codice.

Sebbene interessante, quanto sopra non dice molto sulla strategia da adottare.

Questo è uno dei motivi: con i metodi personalizzati, gli sviluppatori sarebbero liberi di scegliere un nome di metodo diverso, come getLength (), length (), getlength () o qualsiasi altra cosa. Python applica una denominazione rigorosa in modo che la funzione comune len () possa essere utilizzata.

Leggermente più interessante. La mia opinione è che le funzioni siano, in un certo senso, la versione pitonica delle interfacce.

Infine, dallo stesso Guido :

Parlare delle Abilità / Interfacce mi ha fatto pensare ad alcuni dei nostri nomi di metodi speciali "canaglia". In Language Reference, si dice: "Una classe può implementare determinate operazioni che vengono richiamate da una sintassi speciale (come operazioni aritmetiche o indici e affettamenti) definendo metodi con nomi speciali". Ma ci sono tutti questi metodi con nomi speciali come __len__o __unicode__che sembrano essere forniti a beneficio delle funzioni incorporate, piuttosto che per il supporto della sintassi. Presumibilmente in un Python basato su interfaccia, questi metodi si trasformerebbero in metodi con nomi regolari su un ABC, in modo che __len__diventino

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

Tuttavia, riflettendoci ancora un po ', non vedo perché tutte le operazioni sintattiche non si limiterebbero a richiamare il metodo appropriato con nome normale su uno specifico ABC. " <", per esempio, dovrebbe presumibilmente invocare " object.lessthan" (o forse " comparable.lessthan"). Quindi un altro vantaggio sarebbe la capacità di svezzare Python da questa stranezza dal nome alterato, che mi sembra un miglioramento dell'HCI .

Hm. Non sono sicuro di essere d'accordo (figurati questo :-).

Ci sono due parti della "logica di Python" che vorrei spiegare prima.

Prima di tutto, ho scelto len (x) su x.len () per motivi HCI (è def __len__()venuto molto dopo). Ci sono due ragioni intrecciate in realtà, entrambe HCI:

(a) Per alcune operazioni, la notazione del prefisso si legge meglio di quella del suffisso: le operazioni del prefisso (e dell'infisso!) hanno una lunga tradizione in matematica a cui piacciono le notazioni in cui le immagini aiutano il matematico a pensare a un problema. Confrontare la facile con cui riscriviamo una formula come x*(a+b)in x*a + x*bper la goffaggine di fare la stessa cosa usando una notazione OO crudo.

(b) Quando leggo un codice che dice len(x)che so che sta chiedendo la lunghezza di qualcosa. Questo mi dice due cose: il risultato è un numero intero e l'argomento è una sorta di contenitore. Al contrario, quando leggo x.len(), devo già sapere che xè una sorta di contenitore che implementa un'interfaccia o eredita da una classe che ha uno standard len(). Testimone la confusione che di tanto in tanto avere quando una classe che non sta realizzando una mappatura ha una get()o keys() metodo, o qualcosa che non è un file dispone di un write()metodo.

Dire la stessa cosa in un altro modo, vedo 'len' come un built-in funzionamento . Non vorrei perderlo. Non posso dire con certezza se lo intendevi o no, ma "def len (self): ..." sembra certamente che tu voglia degradarlo a un metodo ordinario. Sono fortemente -1 su questo.

La seconda parte della logica di Python che ho promesso di spiegare è il motivo per cui ho scelto metodi speciali per guardare __special__e non semplicemente special. Prevedevo molte operazioni che le classi potrebbero voler sovrascrivere, alcune standard (ad esempio __add__o __getitem__), altre non così standard (ad esempio, pickle __reduce__per molto tempo non ha avuto alcun supporto nel codice C). Non volevo che queste operazioni speciali usassero nomi di metodi ordinari, perché allora classi preesistenti, o classi scritte da utenti senza una memoria enciclopedica per tutti i metodi speciali, sarebbero passibili di definire accidentalmente operazioni che non intendevano implementare , con possibili conseguenze disastrose. Ivan Krstić lo ha spiegato in modo più conciso nel suo messaggio, che è arrivato dopo che avevo scritto tutto questo.

- --Guido van Rossum (home page: http://www.python.org/~guido/ )

La mia comprensione di questo è che in alcuni casi, la notazione del prefisso ha più senso (cioè, Duck.quack ha più senso di ciarlatano (Duck) da un punto di vista linguistico) e ancora una volta, le funzioni consentono "interfacce".

In tal caso, la mia ipotesi sarebbe di implementare get_king_moves basandosi esclusivamente sul primo punto di Guido. Ma questo lascia ancora molte domande aperte riguardo, ad esempio, l'implementazione di una classe stack e queue con metodi push e pop simili: dovrebbero essere funzioni o metodi? (qui immagino funzioni, perché voglio davvero segnalare un'interfaccia push-pop)

TLDR: Qualcuno può spiegare quale dovrebbe essere la strategia per decidere quando utilizzare funzioni e metodi?


2
Meh, ho sempre pensato che fosse del tutto arbitrario. La digitazione a papera consente "interfacce" implicite, non fa molta differenza se si dispone di X.frobo X.__frob__e indipendenti frob.
Cat Plus Plus

2
Sebbene io sia per lo più d'accordo con te, in linea di principio la tua risposta non è pitonica. Ricorda: "Di fronte all'ambiguità, rifiuta la tentazione di indovinare". (Ovviamente, le scadenze cambierebbero questo, ma lo sto facendo per divertimento / miglioramento personale.)
Caesar Bautista

Questa è una cosa che non mi piace di Python. Sento che se stai per forzare la digitazione del cast come un int su una stringa, rendilo un metodo. È fastidioso doverlo racchiudere tra parentesi e richiede tempo.
Matt

1
Questa è la ragione più importante per cui non mi piace Python: non sai mai se devi cercare una funzione o un metodo quando vuoi ottenere qualcosa. E diventa ancora più complicato quando si utilizzano librerie aggiuntive con nuovi tipi di dati come vettori o frame di dati.
vonjd

1
"Lo Zen di Python afferma che dovrebbe esserci un solo modo per fare le cose" tranne che non lo fa.
gented

Risposte:


84

La mia regola generale è questa: l'operazione viene eseguita sull'oggetto o dall'oggetto?

se viene eseguita dall'oggetto, dovrebbe essere un'operazione membro. Se potrebbe applicarsi anche ad altre cose, o è fatto da qualcos'altro all'oggetto, allora dovrebbe essere una funzione (o forse un membro di qualcos'altro).

Quando si introduce la programmazione, è tradizionale (sebbene l'implementazione non sia corretta) descrivere gli oggetti in termini di oggetti del mondo reale come le automobili. Hai menzionato un'anatra, quindi andiamo con quello.

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

Nel contesto dell'analogia "gli oggetti sono cose reali", è "corretto" aggiungere un metodo di classe per tutto ciò che l'oggetto può fare. Quindi dì che voglio uccidere un'anatra, aggiungo un .kill () all'anatra? No ... per quanto ne so gli animali non si suicidano. Quindi se voglio uccidere un'anatra dovrei fare questo:

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."

Allontanandoci da questa analogia, perché usiamo metodi e classi? Perché vogliamo contenere dati e, si spera, strutturare il nostro codice in modo tale che sia riutilizzabile ed estendibile in futuro. Questo ci porta alla nozione di incapsulamento che è così cara al design OO.

Il principio di incapsulamento è in realtà ciò a cui si riduce: come progettista dovresti nascondere tutto ciò che riguarda l'implementazione e gli interni della classe a cui non è assolutamente necessario che alcun utente o altro sviluppatore acceda. Poiché ci occupiamo di istanze di classi, questo si riduce a "quali operazioni sono cruciali su questa istanza ". Se un'operazione non è specifica dell'istanza, non dovrebbe essere una funzione membro.

TL; DR : cosa ha detto @Bryan. Se opera su un'istanza e deve accedere ai dati interni all'istanza della classe, dovrebbe essere una funzione membro.


Quindi, in breve, le funzioni non membri operano su oggetti immutabili, gli oggetti mutabili usano funzioni membro? (O questa è una generalizzazione troppo rigida? Questo per certi funziona solo perché i tipi immutabili non hanno uno stato.)
Cesare Bautista

1
Da un punto di vista rigoroso OOP, credo che sia giusto. Poiché Python ha variabili sia pubbliche che private (variabili con nomi che iniziano con __) e fornisce una protezione dall'accesso garantito zero a differenza di Java, non ci sono assoluti semplicemente perché stiamo discutendo di un linguaggio permissivo. In un linguaggio meno permissivo come Java, tuttavia, ricorda che le funzioni getFoo () e setFoo () sono la norma, quindi l'immutabilità non è assoluta. Il codice client non è autorizzato a fare assegnazioni ai membri.
arrdem

1
@Ceasar Non è vero. Gli oggetti immutabili hanno uno stato; altrimenti non ci sarebbe nulla a distinguere un intero da un altro. Gli oggetti immutabili non cambiano il loro stato. In generale, questo rende molto meno problematico che tutto il loro stato sia pubblico. E in quell'impostazione, è molto più facile manipolare in modo significativo un oggetto tutto pubblico immutabile con funzioni; non c'è "privilegio" di essere un metodo.
Ben

1
@CeasarBautista yeah, Ben ha ragione. Ci sono tre scuole "principali" di progettazione del codice 1) non progettato, 2) OOP e 3) funzionale. in uno stile funzionale, non ci sono affatto stati. Questo è il modo in cui vedo la maggior parte del codice Python progettato, prende input e genera output con pochi effetti collaterali. Il punto di OOP è che tutto ha uno stato. Le classi sono un contenitore per gli stati e le funzioni "membro" sono quindi codice dipendente dallo stato e basato sugli effetti collaterali che carica lo stato definito nella classe ogni volta che viene richiamato. Python tende ad essere funzionale, da qui la preferenza del codice non membro.
arrdem

1
mangiare (sé), fare schifo (sé), morire (sé). hahahaha
Wapiti

27

Usa una lezione quando vuoi:

1) Isolare il codice chiamante dai dettagli di implementazione, sfruttando l' astrazione e l' incapsulamento .

2) Quando vuoi essere sostituibile con altri oggetti - sfruttando il polimorfismo .

3) Quando si desidera riutilizzare il codice per oggetti simili, sfruttando l' ereditarietà .

Usa una funzione per le chiamate che abbia senso in molti tipi di oggetti diversi - per esempio, le funzioni incorporate len e repr si applicano a molti tipi di oggetti.

Detto questo, la scelta a volte si riduce a una questione di gusti. Pensa in termini di ciò che è più comodo e leggibile per le chiamate tipiche. Ad esempio, quale sarebbe meglio (x.sin()**2 + y.cos()**2).sqrt()o sqrt(sin(x)**2 + cos(y)**2)?


8

Ecco una semplice regola pratica: se il codice agisce su una singola istanza di un oggetto, usa un metodo. Ancora meglio: usa un metodo a meno che non ci sia un motivo valido per scriverlo come una funzione.

Nel tuo esempio specifico, vuoi che assomigli a questo:

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

Non pensarci troppo. Usa sempre i metodi finché non arriva il punto in cui ti dici "non ha senso renderlo un metodo", nel qual caso puoi creare una funzione.


2
Puoi spiegare perché utilizzi metodi predefiniti rispetto alle funzioni? (E potresti spiegare se questa regola ha ancora senso nel caso dei metodi stack e queue / pop e push?)
Caesar Bautista

11
Quella regola pratica non ha senso. La stessa libreria standard può essere una guida su quando usare le classi rispetto alle funzioni. heapq e math sono funzioni perché operano su normali oggetti Python (float ed elenchi) e perché non hanno bisogno di mantenere uno stato complesso come fa il modulo random .
Raymond Hettinger

1
Stai dicendo che la regola non ha senso perché la libreria standard la viola? Non credo che questa conclusione abbia senso. Per prima cosa, le regole pratiche sono proprio questo: non sono regole assolute. In più, una gran parte della libreria standard fa seguire quella regola. L'OP stava cercando alcune semplici linee guida e penso che il mio consiglio sia perfettamente valido per qualcuno che è appena agli inizi. Tuttavia apprezzo il tuo punto di vista.
Bryan Oakley

Ebbene, l'STL ha delle buone ragioni per farlo. Per math and co è ovviamente inutile avere una classe poiché non abbiamo uno stato per essa (in altri linguaggi quelle sono classi con solo funzioni statiche). Per cose che funzionano su diversi contenitori diversi, come dire, len()si può anche sostenere che il design ha senso, anche se personalmente ritengo che le funzioni non sarebbero poi così male anche lì - avremmo solo un'altra convenzione che len()deve sempre restituire un intero (ma con tutti i problemi aggiuntivi di backcomp non lo consiglierei nemmeno per python)
Voo

-1 poiché questa risposta è completamente arbitraria: stai essenzialmente cercando di dimostrare che X è migliore di Y assumendo che X sia migliore di Y.
Gented

7

Di solito penso a un oggetto come una persona.

Gli attributi sono il nome della persona, l'altezza, il numero di scarpe, ecc.

I metodi e le funzioni sono operazioni che la persona può eseguire.

Se l'operazione potesse essere eseguita da una qualsiasi persona, senza richiedere nulla di unico per questa persona specifica (e senza cambiare nulla su questa persona specifica), allora è una funzione e dovrebbe essere scritta come tale.

Se un'operazione sta agendo sulla persona (ad esempio mangiare, camminare, ...) o richiede qualcosa di unico per questa persona per essere coinvolta (come ballare, scrivere un libro, ...), allora dovrebbe essere un metodo .

Ovviamente, non è sempre banale tradurlo nell'oggetto specifico con cui stai lavorando, ma trovo che sia un buon modo per pensarci.


ma puoi misurare l'altezza di qualsiasi persona anziana, quindi con quella logica dovrebbe essere height(person), no person.height?
endolith

@endolith Certo, ma direi che l'altezza è meglio come attributo perché non è necessario eseguire alcun lavoro di fantasia per recuperarla. Scrivere una funzione per recuperare un numero sembra un cerchio inutile da superare.
Thriveth

@endolith D'altra parte, se la persona è un bambino che cresce e cambia altezza, sarebbe ovvio lasciare che un metodo si occupi di questo, non una funzione.
Thriveth

Questo non è vero. E se lo avessi are_married(person1, person2)? Questa query è molto generale e dovrebbe quindi essere una funzione e non un metodo.
Pithikos

@Pithikos Certo, ma questo implica anche più di un semplice attributo o azione eseguita su un oggetto (Persona), ma piuttosto una relazione tra due oggetti.
Thriveth

4

Generalmente utilizzo le classi per implementare un insieme logico di capacità per qualcosa , in modo che nel resto del mio programma posso ragionare sulla cosa , non dovendo preoccuparmi di tutte le piccole preoccupazioni che costituiscono la sua implementazione.

Tutto ciò che fa parte di quell'astrazione fondamentale di "cosa puoi fare con una cosa " dovrebbe di solito essere un metodo. Questo generalmente include tutto ciò che può alterare una cosa , poiché lo stato dei dati interni è solitamente considerato privato e non fa parte dell'idea logica di "cosa puoi fare con una cosa ".

Quando si arriva a operazioni di livello superiore, specialmente se coinvolgono più cose , trovo che di solito siano espresse in modo più naturale come funzioni, se possono essere costruite dall'astrazione pubblica di una cosa senza bisogno di un accesso speciale agli interni (a meno che non ' re metodi di qualche altro oggetto). Questo ha il grande vantaggio che quando decido di riscrivere completamente gli interni di come funziona la mia cosa (senza cambiare l'interfaccia), ho solo un piccolo set di metodi di base da riscrivere, e quindi tutte le funzioni esterne scritte in termini di quei metodi funzionerà e basta. Trovo che insistere sul fatto che tutte le operazioni da fare con la classe X sono metodi sulla classe X porta a classi troppo complicate.

Tuttavia, dipende dal codice che sto scrivendo. Per alcuni programmi li modello come una raccolta di oggetti le cui interazioni danno origine al comportamento del programma; qui la funzionalità più importante è strettamente accoppiata a un singolo oggetto, e quindi è implementata nei metodi, con una serie di funzioni di utilità. Per altri programmi la cosa più importante è un insieme di funzioni che manipolano i dati, e le classi sono usate solo per implementare i naturali "tipi di anatra" che sono manipolati dalle funzioni.


0

Si può dire che "di fronte all'ambiguità, rifiuta la tentazione di indovinare".

Tuttavia, non è nemmeno un'ipotesi. Sei assolutamente sicuro che i risultati di entrambi gli approcci siano gli stessi in quanto risolvono il tuo problema.

Credo che sia solo una buona cosa avere più modi per raggiungere gli obiettivi. Ti direi umilmente, come hanno già fatto altri utenti, di utilizzare qualsiasi cosa "abbia un sapore migliore" / ritenga più intuitivo , in termini di linguaggio.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.