Praktyczny przykład metody specjalnej w Pythonie __call__

159

Wiem, że __call__metoda w klasie jest wyzwalana, gdy wywoływana jest instancja klasy. Nie mam jednak pojęcia, kiedy mogę użyć tej specjalnej metody, ponieważ można po prostu utworzyć nową metodę i wykonać tę samą operację, co w __call__metodzie i zamiast wywoływać instancję, można wywołać metodę.

Byłbym bardzo wdzięczny, gdyby ktoś dał mi praktyczne zastosowanie tej specjalnej metody.

mohi666
źródło
8
funkcjonalność _call_ jest taka sama, jak przeciążonego operatora () w C ++ . Jeśli po prostu utworzysz nową metodę poza klasą, możesz nie uzyskać dostępu do wewnętrznych danych w klasie.
andy
2
Najczęstsze użycie __call__jest ukryte na widoku; tak tworzysz instancję klasy: x = Foo()jest naprawdę x = type(Foo).__call__(Foo), gdzie __call__jest zdefiniowana przez metaklasę Foo.
chepner

Odpowiedzi:

88

Moduł formularzy Django __call__ładnie wykorzystuje metodę do implementacji spójnego API do walidacji formularzy. Możesz napisać swój własny walidator dla formularza w Django jako funkcję.

def custom_validator(value):
    #your validation logic

Django ma kilka domyślnych wbudowanych walidatorów, takich jak walidatory poczty e-mail, walidatory adresów URL itp., Które zasadniczo należą do walidatorów RegEx. Aby zaimplementować je w czysty sposób, Django ucieka się do wywoływanych klas (zamiast funkcji). Implementuje domyślną logikę walidacji Regex w RegexValidator, a następnie rozszerza te klasy o inne walidacje.

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

Teraz zarówno twoja funkcja niestandardowa, jak i wbudowany EmailValidator mogą być wywoływane z tą samą składnią.

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

Jak widać, ta implementacja w Django jest podobna do tego, co inni wyjaśnili w swoich odpowiedziach poniżej. Czy można to zaimplementować w inny sposób? Mógłbyś, ale IMHO nie będzie tak czytelny ani tak łatwo rozszerzalny dla dużego frameworka, takiego jak Django.

Praveen Gollakota
źródło
5
Więc jeśli jest używany poprawnie, może uczynić kod bardziej czytelnym. Zakładam, że jeśli zostanie użyty w niewłaściwym miejscu, kod będzie również bardzo nieczytelny.
mohi666
15
To przykładem tego, jak można go używać, ale nie jeden dobry moim zdaniem. W tym przypadku nie ma żadnej korzyści z posiadania wywoływalnej instancji. Byłoby lepiej mieć interfejs / klasę abstrakcyjną z metodą, taką jak .validate (); jest to ta sama rzecz, tylko bardziej wyraźna. Prawdziwą wartością __call__ jest możliwość użycia instancji w miejscu, w którym oczekuje się wywołania. Na przykład __call__ używam najczęściej podczas tworzenia dekoratorów.
Daniel
120

W tym przykładzie użyto zapamiętywania , zasadniczo przechowując wartości w tabeli (w tym przypadku w słowniku), dzięki czemu można je później wyszukać, zamiast ponownie je obliczać.

Tutaj używamy prostej klasy z __call__metodą do obliczania silni (za pomocą wywoływalnego obiektu ) zamiast funkcji silni, która zawiera zmienną statyczną (ponieważ nie jest to możliwe w Pythonie).

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()

Teraz masz factobiekt, który jest wywoływalny, tak jak każda inna funkcja. Na przykład

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

I jest również stanowy.

S.Lott
źródło
2
Wolałbym mieć factobiekt, który można indeksować, ponieważ twoja __call__funkcja jest zasadniczo indeksem. Użyłby też listy zamiast dyktowania, ale to tylko ja.
Chris Lutz,
4
@delnan - Prawie wszystko można zrobić na kilka różnych sposobów. Który z nich jest bardziej czytelny, zależy od czytelnika.
Chris Lutz
1
@Chris Lutz: Możesz rozważać tego rodzaju zmiany. Ogólnie w przypadku zapamiętywania słownik działa dobrze, ponieważ nie można zagwarantować kolejności, w jakiej rzeczy wypełniają listę. W takim przypadku lista może działać, ale nie będzie ani szybsza, ani prostsza.
S.Lott
8
@delnan: To nie ma być najkrótsze. Nikt nie wygrywa w Code Golf. Ma pokazać __call__, być prostym i niczym więcej.
S.Lott
3
Ale to trochę rujnuje przykład, kiedy zademonstrowana technika nie jest idealna do zadań, prawda? (I nie chodziło mi o „zapiszmy wiersze do cholery” - krótko, mówiłem o „zapisz to w równie jasny sposób i zachowaj jakiś standardowy kod” - krótki. Zapewniam, że nie jestem jeden z tych szaleńców, którzy próbują napisać jak najkrótszy kod, po prostu chcę uniknąć standardowego kodu, który nic nie dodaje czytelnikowi.)
40

Uważam to za przydatne, ponieważ pozwala mi tworzyć interfejsy API, które są łatwe w użyciu (masz obiekt wywoływalny, który wymaga określonych argumentów) i są łatwe do wdrożenia, ponieważ możesz używać praktyk zorientowanych obiektowo.

Poniżej znajduje się kod, który napisałem wczoraj, który tworzy wersję hashlib.foometod, która haszuje całe pliki zamiast ciągów:

# 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)

Ta implementacja pozwala mi korzystać z funkcji w podobny sposób do hashlib.foofunkcji:

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

Oczywiście mogłem to zaimplementować w inny sposób, ale w tym przypadku wydawało się to prostym podejściem.

bradley.ayers
źródło
7
Ponownie, zamknięcia psują ten przykład. pastebin.com/961vU0ay to 80% linii i tak samo wyraźne.
8
Nie jestem przekonany, że zawsze będzie to tak samo jasne dla kogoś (np. Dla kogoś, kto używał tylko Javy). Zagnieżdżone funkcje i wyszukiwanie / zakres zmiennych mogą być mylące. Przypuszczam, że chodziło mi o to, że __call__dostaniesz narzędzie, które pozwoli ci używać technik OO do rozwiązywania problemów.
bradley.ayers
4
Myślę, że pytanie „po co używać X nad Y”, skoro oba zapewniają równoważną funkcjonalność, jest strasznie subiektywne. Dla niektórych osób podejście OO jest łatwiejsze do zrozumienia, dla innych podejście zamykające. Nie ma przekonującego argumentu, aby użyć jednego nad drugim, chyba że miałeś sytuację, w której musiałeś użyć isinstancelub coś podobnego.
bradley.ayers
2
@delnan Twój przykład zamknięcia to mniej wierszy kodu, ale to, że jest tak samo jasne, jest trudniejsze do argumentowania.
Dennis,
8
Przykładem, w którym wolisz użyć __call__metody zamiast zamknięcia, jest sytuacja, w której masz do czynienia z modułem wieloprocesowym, który wykorzystuje wytrawianie do przekazywania informacji między procesami. Nie możesz wytrawić zamknięcia, ale możesz wytrawić wystąpienie klasy.
John Peter Thompson Garcés
21

__call__jest również używany do implementowania klas dekoratorów w Pythonie. W tym przypadku instancja klasy jest wywoływana, gdy wywoływana jest metoda z dekoratorem.

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()
Kris
źródło
9

Tak, kiedy wiesz, że masz do czynienia z obiektami, jest całkowicie możliwe (aw wielu przypadkach zalecane) użycie jawnego wywołania metody. Czasami jednak masz do czynienia z kodem, który oczekuje wywoływalnych obiektów - zwykle funkcji, ale dzięki temu __call__możesz budować bardziej złożone obiekty, z danymi instancji i większą liczbą metod do delegowania powtarzalnych zadań itp., Które są nadal wywoływalne.

Ponadto czasami używasz zarówno obiektów do złożonych zadań (gdzie sensowne jest napisanie dedykowanej klasy), jak i obiektów do prostych zadań (które już istnieją w funkcjach lub są łatwiejsze do zapisania jako funkcje). Aby mieć wspólny interfejs, musisz albo napisać małe klasy opakowujące te funkcje w oczekiwany interfejs, albo zachować funkcje funkcji i sprawić, by bardziej złożone obiekty były wywoływalne. Weźmy jako przykład wątki. Te Threadobiekty ze standardowego modułu libarythreading chcą wywoływalnym jako targetargumentu (czyli jako działanie do wykonania w nowym wątku). W przypadku wywoływalnego obiektu nie jesteś ograniczony do funkcji, możesz również przekazywać inne obiekty, takie jak stosunkowo złożony proces roboczy, który pobiera zadania z innych wątków i wykonuje je sekwencyjnie:

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)

To tylko przykład z góry mojej głowy, ale myślę, że jest już wystarczająco złożony, aby uzasadnić klasę. Robienie tego tylko z funkcjami jest trudne, przynajmniej wymaga zwrócenia dwóch funkcji, a to powoli się komplikuje. Jeden mógł zmienić nazwę __call__na coś innego i przekazać metodę związaną, ale sprawia, że kod tworząc nieco mniej oczywisty wątek, a nie dodaje żadnej wartości.

yann.kmm
źródło
3
Prawdopodobnie przydałoby się tutaj użycie wyrażenia „kaczka typing” ( en.wikipedia.org/wiki/Duck_typing#In_Python ) - w ten sposób można naśladować funkcję przy użyciu bardziej skomplikowanego obiektu klasy.
Andrew Jaffe
2
Jako pokrewny przykład widziałem, __call__że używam instancji klas (zamiast funkcji) jako aplikacji WSGI. Oto przykład z „The Definitive Guide to Pylons”: Using Instances of Classes
Josh Rosen,
5

Dekoratory oparte na klasach używają __call__do odwoływania się do opakowanej funkcji. Na przykład:

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

W witrynie Artima.com znajduje się dobry opis różnych opcji

rorykl
źródło
Rzadko jednak widzę dekoratory klas, ponieważ wymagają one pewnego nieoczywistego kodu standardowego do pracy z metodami.
4

__call__Metoda IMHO i domknięcia dają nam naturalny sposób tworzenia wzorca projektowego STRATEGY w Pythonie. Definiujemy rodzinę algorytmów, hermetyzujemy każdy z nich, sprawiamy, że są wymienne i na koniec możemy wykonać wspólny zestaw kroków i na przykład obliczyć hash dla pliku.

ady
źródło
4

Właśnie natknąłem się na użycie __call__()na koncercie, z __getattr__()którym uważam, że jest piękny. Pozwala na ukrycie wielu poziomów API JSON / HTTP / (jednak_serializowanego) wewnątrz obiektu.

__getattr__()Część zajmuje iteracyjnie powrocie zmodyfikowanej wystąpienie tej samej klasy, wypełniając w jednym więcej atrybutów naraz. Następnie, po wyczerpaniu wszystkich informacji, __call__()przejmuje wszystkie podane przez Ciebie argumenty.

Korzystając z tego modelu, możesz na przykład wykonać połączenie typu api.v2.volumes.ssd.update(size=20), które kończy się żądaniem PUT do https://some.tld/api/v2/volumes/ssd/update.

Konkretny kod jest sterownikiem pamięci blokowej dla określonego zaplecza woluminu w OpenStack, możesz to sprawdzić tutaj: https://github.com/openstack/cinder/blob/master/cinder/volume/drivers/nexenta/jsonrpc.py

EDYCJA: Zaktualizowano łącze, aby wskazywało wersję główną.

Piotr Słowacki
źródło
To miłe. Kiedyś użyłem tego samego mechanizmu do przechodzenia przez dowolne drzewo XML za pomocą dostępu do atrybutów.
Petri
1

Określ a __metaclass__i przesłoń __call__metodę, a metoda określonych meta-klas __new__zwróci instancję klasy, albowiem masz „funkcję” z metodami.

user2772852
źródło
1

Możemy użyć __call__metody, aby użyć innych metod klas jako metod statycznych.

    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")             
Abhishek Jain
źródło
0

Jednym z typowych przykładów jest __call__in functools.partial, tutaj jest uproszczona wersja (z Pythonem> = 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)

Stosowanie:

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

inc = partial(add, y=1)
print(inc(41))  # 42
nieskończony
źródło
0

Operator wywołania funkcji.

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

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

Metoda __call__ może zostać użyta do przedefiniowania / ponownej inicjalizacji tego samego obiektu. Ułatwia również używanie instancji / obiektów klasy jako funkcji poprzez przekazywanie argumentów do obiektów.


źródło
Kiedy byłoby to przydatne? Foo (1, 2, 3) wydaje się jaśniejsze.
Jarosław Nikitenko
0

I znaleźć miejsce dobre do wykorzystania wywołalnych przedmioty, takie, które określają __call__(), czy podczas korzystania z możliwości funkcjonalnych programowania w Pythonie, takie jak map(), filter(), reduce().

Najlepszym momentem na użycie wywoływalnego obiektu zamiast zwykłej funkcji lub funkcji lambda jest sytuacja, gdy logika jest złożona i musi zachować pewien stan lub używa innych informacji, które nie są przekazywane do __call__()funkcji.

Oto kod, który filtruje nazwy plików na podstawie ich rozszerzenia przy użyciu wywoływalnego obiektu i filter().

Możliwość wywołania:

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)

Stosowanie:

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

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

Wynik:

['me.jpg', 'friend1.jpg', 'friend2.bmp', 'you.jpeg']
cycgrog
źródło
0

Jest już za późno, ale podaję przykład. Wyobraź sobie, że masz Vectorklasę i Pointklasę. Obie przyjmują x, yjako argumenty pozycyjne. Wyobraźmy sobie, że chcesz utworzyć funkcję, która przesuwa punkt do umieszczenia na wektorze.

4 Rozwiązania

  • put_point_on_vec(point, vec)

  • Uczyń z tego metodę klasy wektorowej. na przykład my_vec.put_point(point)

  • Zrób z tego metodę w Pointklasie.my_point.put_on_vec(vec)
  • Vectornarzędzia __call__, więc możesz go używać jakmy_vec_instance(point)

W rzeczywistości jest to część przykładów, nad którymi pracuję, w celu uzyskania przewodnika po metodach dunder wyjaśnionych w matematyce, które wcześniej czy później opublikuję.

Porzuciłem logikę przesunięcia samego punktu, ponieważ nie o to chodzi w tym pytaniu

Ahmed I. Elsayed
źródło