Python __call__ esempio pratico di metodo speciale


159

So che il __call__metodo in una classe viene attivato quando viene chiamata l'istanza di una classe. Tuttavia, non ho idea di quando posso usare questo metodo speciale, perché si può semplicemente creare un nuovo metodo ed eseguire la stessa operazione eseguita nel __call__metodo e invece di chiamare l'istanza, è possibile chiamare il metodo.

Lo apprezzerei molto se qualcuno mi desse un uso pratico di questo metodo speciale.



8
la funzionalità di _call_ è proprio come l'operatore sovraccarico di () in C ++ . Se si crea semplicemente un nuovo metodo al di fuori della classe, non è possibile accedere ai dati interni in una classe.
Andy,

2
L'uso più comune di __call__è nascosto in bella vista; è come creare un'istanza di una classe: x = Foo()è davvero x = type(Foo).__call__(Foo), dove __call__è definita dalla metaclasse di Foo.
Chepner,

Risposte:


88

Il modulo moduli Django utilizza il __call__metodo per implementare un'API coerente per la convalida dei moduli. Puoi scrivere il tuo validatore per un modulo in Django come funzione.

def custom_validator(value):
    #your validation logic

Django ha alcuni validatori integrati predefiniti come validatori e-mail, validatori url ecc., Che ricadono ampiamente sotto l'egida dei validatori RegEx. Per implementarli in modo pulito, Django ricorre a classi richiamabili (anziché a funzioni). Implementa la logica di convalida Regex predefinita in un RegexValidator e quindi estende queste classi per altre convalide.

class RegexValidator(object):
    def __call__(self, value):
        # validation logic

class URLValidator(RegexValidator):
    def __call__(self, value):
        super(URLValidator, self).__call__(value)
        #additional logic

class EmailValidator(RegexValidator):
    # some logic

Ora sia la funzione personalizzata che EmailValidator integrato possono essere richiamati con la stessa sintassi.

for v in [custom_validator, EmailValidator()]:
    v(value) # <-----

Come puoi vedere, questa implementazione in Django è simile a ciò che altri hanno spiegato nelle loro risposte di seguito. Questo può essere implementato in qualsiasi altro modo? Potresti, ma IMHO non sarà così leggibile o facilmente estendibile per un grande framework come Django.


5
Quindi, se usato correttamente, può rendere il codice più leggibile. Suppongo che se usato nel posto sbagliato, renderebbe anche il codice molto illeggibile.
mohi666,

15
Questo è un esempio di come può essere usato, ma secondo me non è buono. Non c'è alcun vantaggio in questo caso per avere un'istanza richiamabile. Sarebbe meglio avere un'interfaccia / classe astratta con un metodo, come .validate (); è la stessa cosa solo più esplicita. Il vero valore di __call__ è la possibilità di utilizzare un'istanza in un luogo in cui è previsto un callable. Uso __call__ più spesso durante la creazione di decoratori, ad esempio.
Daniel,

120

In questo esempio viene utilizzata la memoization , che in sostanza memorizza i valori in una tabella (dizionario in questo caso) in modo da poterli cercare in seguito invece di ricalcolarli.

Qui usiamo una classe semplice con un __call__metodo per calcolare fattoriali (attraverso un oggetto richiamabile ) invece di una funzione fattoriale che contiene una variabile statica (poiché ciò non è possibile in Python).

class Factorial:
    def __init__(self):
        self.cache = {}
    def __call__(self, n):
        if n not in self.cache:
            if n == 0:
                self.cache[n] = 1
            else:
                self.cache[n] = n * self.__call__(n-1)
        return self.cache[n]

fact = Factorial()

Ora hai un factoggetto richiamabile, proprio come ogni altra funzione. Per esempio

for i in xrange(10):                                                             
    print("{}! = {}".format(i, fact(i)))

# output
0! = 1
1! = 1
2! = 2
3! = 6
4! = 24
5! = 120
6! = 720
7! = 5040
8! = 40320
9! = 362880

Ed è anche stato.


2
Preferirei avere un factoggetto indicizzabile poiché la tua __call__funzione è essenzialmente un indice. Userei anche un elenco anziché un dict, ma sono solo io.
Chris Lutz,

4
@delnan - Quasi tutto può essere fatto in diversi modi. Quale è più leggibile dipende dal lettore.
Chris Lutz,

1
@ Chris Lutz: sei libero di contemplare questo tipo di cambiamenti. Per la memoization in generale , un dizionario funziona bene perché non puoi garantire l'ordine in cui le cose riempiono il tuo elenco. In questo caso, un elenco potrebbe funzionare, ma non sarà più veloce o più semplice.
S.Lott

8
@delnan: questo non è destinato ad essere il più breve. Nessuno vince a code golf. Ha lo scopo di mostrare __call__, essere semplice e niente di più.
S.Lott

3
Ma rovina l'esempio quando la tecnica dimostrata non è l'ideale per i compiti, non è vero? (E non mi riferivo allo Short "salviamo le righe per il diavolo di esso", stavo parlando dello Short "scrivilo in questo modo altrettanto chiaro e risparmia un po 'di codice"). uno di quei pazzi che cercano di scrivere il codice più breve possibile, voglio semplicemente evitare il codice del bollettino che non aggiunge nulla per il lettore.)

40

Lo trovo utile perché mi consente di creare API facili da usare (hai qualche oggetto richiamabile che richiede alcuni argomenti specifici) e sono facili da implementare perché puoi usare pratiche orientate agli oggetti.

Di seguito è riportato il codice che ho scritto ieri che crea una versione dei hashlib.foometodi che esegue l' hashing di interi file anziché delle stringhe:

# filehash.py
import hashlib


class Hasher(object):
    """
    A wrapper around the hashlib hash algorithms that allows an entire file to
    be hashed in a chunked manner.
    """
    def __init__(self, algorithm):
        self.algorithm = algorithm

    def __call__(self, file):
        hash = self.algorithm()
        with open(file, 'rb') as f:
            for chunk in iter(lambda: f.read(4096), ''):
                hash.update(chunk)
        return hash.hexdigest()


md5    = Hasher(hashlib.md5)
sha1   = Hasher(hashlib.sha1)
sha224 = Hasher(hashlib.sha224)
sha256 = Hasher(hashlib.sha256)
sha384 = Hasher(hashlib.sha384)
sha512 = Hasher(hashlib.sha512)

Questa implementazione mi consente di utilizzare le funzioni in modo simile alle hashlib.foofunzioni:

from filehash import sha1
print sha1('somefile.txt')

Ovviamente avrei potuto implementarlo in un modo diverso, ma in questo caso sembrava un approccio semplice.


7
Ancora una volta, le chiusure rovinano questo esempio. pastebin.com/961vU0ay è l'80% delle linee e altrettanto chiaro.

8
Non sono convinto che sarebbe sempre altrettanto chiaro per qualcuno (es. Forse qualcuno che ha usato solo Java). Le funzioni nidificate e la ricerca / ambito variabile potrebbero creare confusione. Suppongo che il mio punto sia che __call__ti offre uno strumento che ti consente di utilizzare le tecniche OO per risolvere i problemi.
bradley.ayers,

4
Penso che la domanda "perché usare X su Y" quando entrambi forniscono funzionalità equivalenti è terribilmente soggettiva. Per alcune persone l'approccio OO è più facile da capire, per altri l'approccio di chiusura. Non c'è alcun argomento convincente per utilizzare uno sopra l'altro, a meno che non hai avuto una situazione in cui si doveva usare isinstanceo qualcosa di simile.
bradley.ayers,

2
@delnan Il tuo esempio di chiusure è meno righe di codice, ma è altrettanto chiaro che è più difficile discutere.
Dennis,

8
Un esempio di dove preferiresti usare un __call__metodo invece di una chiusura è quando hai a che fare con il modulo multiprocessing, che usa il decapaggio per passare informazioni tra i processi. Non puoi decapare una chiusura, ma puoi decapare un'istanza di una classe.
John Peter Thompson Garcés,

21

__call__è anche usato per implementare classi di decoratore in Python. In questo caso l'istanza della classe viene chiamata quando viene chiamato il metodo con il decoratore.

class EnterExitParam(object):

    def __init__(self, p1):
        self.p1 = p1

    def __call__(self, f):
        def new_f():
            print("Entering", f.__name__)
            print("p1=", self.p1)
            f()
            print("Leaving", f.__name__)
        return new_f


@EnterExitParam("foo bar")
def hello():
    print("Hello")


if __name__ == "__main__":
    hello()

9

Sì, quando sai che hai a che fare con oggetti, è perfettamente possibile (e in molti casi consigliabile) usare una chiamata esplicita al metodo. Tuttavia, a volte hai a che fare con codice che prevede oggetti richiamabili - in genere funzioni, ma grazie a __call__te puoi costruire oggetti più complessi, con dati di istanza e più metodi per delegare attività ripetitive, ecc. Che sono ancora richiamabili.

Inoltre, a volte stai usando sia oggetti per compiti complessi (dove ha senso scrivere una classe dedicata) sia oggetti per compiti semplici (che esistono già nelle funzioni o che sono più facilmente scritti come funzioni). Per avere un'interfaccia comune, è necessario scrivere minuscole classi avvolgendo quelle funzioni con l'interfaccia prevista, oppure mantenere le funzioni delle funzioni e rendere richiamabili gli oggetti più complessi. Prendiamo le discussioni come esempio. Gli Threadoggetti dal modulo libary standardthreading vogliono un callable come targetargomento (cioè come azione da fare nel nuovo thread). Con un oggetto richiamabile, non si è limitati alle funzioni, è possibile passare anche altri oggetti, come un lavoratore relativamente complesso che ottiene compiti da svolgere da altri thread e li esegue in sequenza:

class Worker(object):
    def __init__(self, *args, **kwargs):
        self.queue = queue.Queue()
        self.args = args
        self.kwargs = kwargs

    def add_task(self, task):
        self.queue.put(task)

    def __call__(self):
        while True:
            next_action = self.queue.get()
            success = next_action(*self.args, **self.kwargs)
            if not success:
               self.add_task(next_action)

Questo è solo un esempio della mia testa, ma penso che sia già abbastanza complesso da giustificare la classe. Farlo solo con le funzioni è difficile, almeno richiede di restituire due funzioni e questo sta lentamente diventando complesso. Si potrebbe rinominare __call__qualcos'altro e passare un metodo associato, ma ciò rende il codice che crea il thread leggermente meno ovvio e non aggiunge alcun valore.


3
Probabilmente è utile usare qui la frase "typing anatra" ( en.wikipedia.org/wiki/Duck_typing#In_Python ) - puoi imitare una funzione usando un oggetto di classe più complicato in questo modo.
Andrew Jaffe,

2
Come esempio correlato, ho visto __call__usare le istanze di classe (anziché le funzioni) come applicazioni WSGI. Ecco un esempio di "La guida definitiva ai piloni": Uso delle istanze delle classi
Josh Rosen,

5

I decoratori di classe utilizzano __call__per fare riferimento alla funzione incartata. Per esempio:

class Deco(object):
    def __init__(self,f):
        self.f = f
    def __call__(self, *args, **kwargs):
        print args
        print kwargs
        self.f(*args, **kwargs)

C'è una buona descrizione delle varie opzioni qui su Artima.com


Raramente vedo i decoratori di classe, dato che richiedono un codice di caldaia standard per funzionare con i metodi.

4

Il __call__metodo e le chiusure IMHO ci offrono un modo naturale per creare un modello di progettazione STRATEGY in Python. Definiamo una famiglia di algoritmi, li incapsuliamo, li rendiamo intercambiabili e alla fine possiamo eseguire una serie comune di passaggi e, ad esempio, calcolare un hash per un file.


4

Mi sono appena imbattuto in un uso __call__()in concerto con il __getattr__()quale penso sia bello. Consente di nascondere più livelli di un'API JSON / HTTP / (comunque_serializzata) all'interno di un oggetto.

Il __getattr__() parte si occupa di restituire iterativamente un'istanza modificata della stessa classe, compilando un attributo in più alla volta. Quindi, dopo che tutte le informazioni sono state esaurite, __call__()prende il sopravvento con qualsiasi argomento tu abbia passato.

Utilizzando questo modello, è possibile ad esempio effettuare una chiamata come api.v2.volumes.ssd.update(size=20) , che finisce in una richiesta PUT a https://some.tld/api/v2/volumes/ssd/update.

Il codice specifico è un driver di archiviazione a blocchi per un determinato backend di volume in OpenStack, puoi verificarlo qui: https://github.com/openstack/cinder/blob/master/cinder/volume/drivers/nexenta/jsonrpc.py

EDIT: aggiornato il collegamento per puntare alla revisione principale.


Bello. Una volta ho usato lo stesso meccanismo per attraversare un albero XML arbitrario usando l'accesso agli attributi.
Petri,

1

Specificare a __metaclass__e sovrascrivere il __call__metodo e fare in modo che il metodo delle meta-classi specificato __new__restituisca un'istanza della classe, viola si ha una "funzione" con i metodi.


1

Possiamo usare il __call__metodo per usare altri metodi di classe come metodi statici.

    class _Callable:
        def __init__(self, anycallable):
            self.__call__ = anycallable

    class Model:

        def get_instance(conn, table_name):

            """ do something"""

        get_instance = _Callable(get_instance)

    provs_fac = Model.get_instance(connection, "users")             

0

Un esempio comune è __call__in functools.partial, ecco una versione semplificata (con Python> = 3.5):

class partial:
    """New function with partial application of the given arguments and keywords."""

    def __new__(cls, func, *args, **kwargs):
        if not callable(func):
            raise TypeError("the first argument must be callable")
        self = super().__new__(cls)

        self.func = func
        self.args = args
        self.kwargs = kwargs
        return self

    def __call__(self, *args, **kwargs):
        return self.func(*self.args, *args, **self.kwargs, **kwargs)

Uso:

def add(x, y):
    return x + y

inc = partial(add, y=1)
print(inc(41))  # 42

0

L'operatore di chiamata della funzione.

class Foo:
    def __call__(self, a, b, c):
        # do something

x = Foo()
x(1, 2, 3)

Il metodo __call__ può essere utilizzato per ridefinire / inizializzare nuovamente lo stesso oggetto. Facilita anche l'uso di istanze / oggetti di una classe come funzioni passando argomenti agli oggetti.


Quando sarebbe utile? Foo (1, 2, 3) sembra più chiaro.
Yaroslav Nikitenko,

0

Trovo un posto buono da utilizzare oggetti callable, quelle che definiscono __call__(), è quando si utilizzano le funzionalità di programmazione funzionale in Python, come ad esempio map(), filter(), reduce().

Il momento migliore per utilizzare un oggetto richiamabile su una funzione semplice o una funzione lambda è quando la logica è complessa e deve conservare un certo stato o utilizzare altre informazioni che non sono state passate alla __call__()funzione.

Ecco un po 'di codice che filtra i nomi dei file in base all'estensione del loro nome usando un oggetto richiamabile e filter().

callable:

import os

class FileAcceptor(object):
    def __init__(self, accepted_extensions):
        self.accepted_extensions = accepted_extensions

    def __call__(self, filename):
        base, ext = os.path.splitext(filename)
        return ext in self.accepted_extensions

class ImageFileAcceptor(FileAcceptor):
    def __init__(self):
        image_extensions = ('.jpg', '.jpeg', '.gif', '.bmp')
        super(ImageFileAcceptor, self).__init__(image_extensions)

Uso:

filenames = [
    'me.jpg',
    'me.txt',
    'friend1.jpg',
    'friend2.bmp',
    'you.jpeg',
    'you.xml']

acceptor = ImageFileAcceptor()
image_filenames = filter(acceptor, filenames)
print image_filenames

Produzione:

['me.jpg', 'friend1.jpg', 'friend2.bmp', 'you.jpeg']

0

È troppo tardi ma sto dando un esempio. Immagina di avere una Vectorclasse e una Pointlezione. Entrambi prendono x, ycome argomenti posizionali. Immaginiamo che tu voglia creare una funzione che sposta il punto da mettere sul vettore.

4 soluzioni

  • put_point_on_vec(point, vec)

  • Rendilo un metodo sulla classe vettoriale. per esempio my_vec.put_point(point)

  • Rendilo un metodo sulla Pointclasse.my_point.put_on_vec(vec)
  • Vectorimplementa __call__, quindi puoi usarlo comemy_vec_instance(point)

Questo in realtà fa parte di alcuni esempi a cui sto lavorando per una guida per i metodi di sicurezza spiegati con la matematica che prima o poi pubblicherò.

Ho lasciato la logica di spostare il punto stesso perché non si tratta di questa domanda

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.