Perché Python usa i "metodi magici"?


99

Recentemente ho giocato con Python e una cosa che trovo un po 'strana è l'ampio uso di' metodi magici ', ad esempio per rendere disponibile la sua lunghezza, un oggetto implementa un metodo def __len__(self), e quindi viene chiamato quando scrivi tu len(obj).

Mi stavo solo chiedendo perché gli oggetti non definiscono semplicemente un len(self)metodo e lo fanno chiamare direttamente come membro dell'oggetto, ad esempio obj.len()? Sono sicuro che ci devono essere buone ragioni per Python che lo fa in questo modo, ma come novellino non ho ancora capito cosa sono.


4
Penso che il motivo generale sia a) storico eb) qualcosa di simile len()o reversed()applicabile a molti tipi di oggetti, ma un metodo come append()si applica solo alle sequenze, ecc.
Grant Paul

Risposte:


64

PER QUANTO NE SO, len è speciale in questo senso e ha radici storiche.

Ecco una citazione dalle FAQ :

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.

Gli altri "metodi magici" (in realtà chiamati metodo speciale nel folklore di Python) hanno molto senso, e funzionalità simili esistono in altri linguaggi. Vengono utilizzati principalmente per il codice che viene chiamato implicitamente quando viene utilizzata una sintassi speciale.

Per esempio:

  • operatori sovraccarichi (esistono in C ++ e altri)
  • costruttore / distruttore
  • hook per accedere agli attributi
  • strumenti per la metaprogrammazione

e così via...


2
Python and the Principle of Least Astonishment è una buona lettura per alcuni dei vantaggi di Python in questo modo (anche se ammetto che l'inglese ha bisogno di lavoro). Il punto fondamentale: consente alla libreria standard di implementare una tonnellata di codice che diventa molto, molto riutilizzabile ma comunque sovrascrivibile.
jpmc26

20

Dallo Zen di Python:

Di fronte all'ambiguità, rifiuta la tentazione di indovinare.
Dovrebbe esserci un modo ovvio, e preferibilmente solo uno, per farlo.

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

Tutte le operazioni comuni a molti tipi di oggetti vengono inserite in metodi magici, come __nonzero__, __len__o __repr__. Tuttavia, sono per lo più opzionali.

Il sovraccarico degli operatori viene eseguito anche con metodi magici (ad esempio __le__), quindi ha senso utilizzarli anche per altre operazioni comuni.


Questo è un argomento convincente. Più soddisfacente che "Guido non credeva veramente in OO" .... (come ho visto affermare altrove).
Andy Hayden

15

Python usa la parola "metodi magici" , perché quei metodi eseguono davvero la magia per il tuo programma. Uno dei maggiori vantaggi dell'utilizzo dei metodi magici di Python è che forniscono un modo semplice per far sì che gli oggetti si comportino come tipi incorporati. Ciò significa che puoi evitare modi brutti, controintuitivi e non standard di eseguire gli operatori di base.

Considera un esempio seguente:

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'

Questo dà un errore, perché il tipo di dizionario non supporta l'aggiunta. Ora estendiamo la classe del dizionario e aggiungiamo il metodo magico "__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)

Ora fornisce il seguente output.

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

Quindi, aggiungendo questo metodo, improvvisamente si è verificata la magia e l'errore che stavi ottenendo prima è scomparso.

Spero che ti renda le cose chiare. Per ulteriori informazioni, fare riferimento a:

Una guida ai metodi magici di Python (Rafe Kettler, 2012)


9

Alcune di queste funzioni fanno più di quanto un singolo metodo sarebbe in grado di implementare (senza metodi astratti su una superclasse). Ad esempio, si bool()comporta in questo modo:

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

Puoi anche essere sicuro al 100% che bool()restituirà sempre Vero o Falso; se ti affidassi a un metodo non potresti essere del tutto sicuro di cosa riceveresti.

Alcune altre funzioni che hanno implementazioni relativamente complicate (più complicate dei metodi magici sottostanti potrebbero essere) sono iter()e cmp(), e tutti i metodi degli attributi ( getattr, setattre delattr). Cose come intaccedere anche a metodi magici quando si fa la coercizione (puoi implementare __int__), ma fanno il doppio dovere come tipi. len(obj)in realtà è l'unico caso da cui non credo sia mai diverso obj.__len__().


2
Invece di hasattr()usare try:/ except AttributeError:e invece di if obj.__len__(): return True else: return Falsedirei solo return obj.__len__() > 0ma queste sono solo cose stilistiche.
Chris Lutz

In python 2.6 (a cui btw si fa bool(x)riferimento x.__nonzero__()), il tuo metodo non funzionerebbe. Le istanze bool hanno un metodo __nonzero__()e il codice continua a chiamarsi se stesso una volta che obj era un bool. Forse bool(obj.__bool__())dovrebbe essere trattato nello stesso modo in cui lo hai trattato __len__? (O questo codice funziona davvero per Python 3?)
Ponkadoodle

La natura circolare di bool () era in qualche modo intenzionalmente assurda, per riflettere la natura particolarmente circolare della definizione. C'è un argomento che dovrebbe essere considerato semplicemente un primitivo.
Ian Bicking

L'unica differenza (attualmente) tra len(x)e x.__len__()è che il primo solleverà OverflowError per lunghezze che eccedono sys.maxsize, mentre il secondo generalmente non lo farà per i tipi implementati in Python. Questo è più un bug che una funzionalità, però (ad esempio, l'oggetto range di Python 3.2 può gestire per lo più intervalli arbitrariamente grandi, ma l'uso lencon essi potrebbe fallire. Anche loro __len__falliscono, dato che sono implementati in C piuttosto che in Python)
ncoghlan

4

Non sono realmente "nomi magici". È solo l'interfaccia che un oggetto deve implementare per fornire un determinato servizio. In questo senso, non sono più magiche di qualsiasi definizione di interfaccia predefinita che devi reimplementare.


1

Anche se il motivo è per lo più storico, ci sono alcune peculiarità in Python lenche rendono appropriato l'uso di una funzione invece di un metodo.

Alcune operazioni in Python sono implementate come metodi, ad esempio list.indexe dict.append, mentre altre sono implementate come invocabili e metodi magici, ad esempio stre itere reversed. I due gruppi differiscono abbastanza da giustificare il diverso approccio:

  1. Sono comuni.
  2. str, intE gli amici sono i tipi. Ha più senso chiamare il costruttore.
  3. L'implementazione è diversa dalla chiamata alla funzione. Ad esempio, iterpotrebbe chiamare __getitem__se __iter__non è disponibile e supporta argomenti aggiuntivi che non rientrano in una chiamata al metodo. Per lo stesso motivo it.next()è stato modificato next(it)nelle versioni recenti di Python: ha più senso.
  4. Alcuni di questi sono parenti stretti degli operatori. C'è la sintassi per chiamare __iter__e __next__- si chiama forloop. Per coerenza, una funzione è migliore. E lo rende migliore per alcune ottimizzazioni.
  5. Alcune delle funzioni sono semplicemente troppo simili alle altre in qualche modo - si reprcomporta come strfa. Avere str(x)contro x.repr()sarebbe fonte di confusione.
  6. Alcuni di loro utilizzano raramente il metodo di implementazione effettivo, ad esempio isinstance.
  7. Alcuni di loro sono veri e propri operatori, getattr(x, 'a')è un altro modo di fare x.ae getattrcondivide molte delle suddette qualità.

Io personalmente chiamo il primo gruppo come metodo e il secondo gruppo come operatore. Non è una distinzione molto buona, ma spero che in qualche modo aiuti.

Detto questo, lennon rientra esattamente nel secondo gruppo. È più vicino alle operazioni del primo, con l'unica differenza che è molto più comune di quasi tutte le operazioni. Ma l'unica cosa che fa è chiamare __len__, ed è molto vicino L.index. Tuttavia, ci sono alcune differenze. Ad esempio, __len__potrebbe essere chiamato per l'implementazione di altre funzionalità, ad esempio bool, se il metodo fosse chiamato lenpotresti rompere bool(x)con il lenmetodo personalizzato che fa cose completamente diverse.

In breve, hai una serie di funzionalità molto comuni che le classi potrebbero implementare a cui si potrebbe accedere tramite un operatore, tramite una funzione speciale (che di solito fa più dell'implementazione, come farebbe un operatore), durante la costruzione di oggetti e tutte condividono alcuni tratti comuni. Tutto il resto è un metodo. Ed lenè in qualche modo un'eccezione a questa regola.


0

Non c'è molto da aggiungere ai due post precedenti, ma tutte le funzioni "magiche" non sono affatto magiche. Fanno parte del modulo __ builtins__ che viene importato in modo implicito / automatico all'avvio dell'interprete. Cioè:

from __builtins__ import *

accade ogni volta prima dell'avvio del programma.

Ho sempre pensato che sarebbe stato più corretto se Python lo facesse solo per la shell interattiva e richiedesse agli script di importare le varie parti dai builtin di cui avevano bisogno. Probabilmente anche una diversa gestione di __ main__ sarebbe utile nelle shell rispetto all'interazione. Ad ogni modo, controlla tutte le funzioni e guarda com'è senza di loro:

dir (__builtins__)
...
del __builtins__
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.