Exemplo prático do método especial Python __call__

159

Eu sei que o __call__método em uma classe é acionado quando a instância de uma classe é chamada. No entanto, não tenho idéia de quando posso usar esse método especial, porque é possível simplesmente criar um novo método e executar a mesma operação feita no __call__método. Em vez de chamar a instância, você pode chamar o método.

Eu realmente aprecio isso se alguém me der um uso prático desse método especial.

mohi666
fonte
8
a funcionalidade de _call_ é como o operador sobrecarregado de () no C ++ . Se você simplesmente criar um novo método fora da classe, poderá não acessar os dados internos de uma classe.
andy
2
O uso mais comum de __call__está oculto à vista de todos; é como você instancia uma classe: x = Foo()é realmente x = type(Foo).__call__(Foo), onde __call__é definido pela metaclasse de Foo.
chepner

Respostas:

88

O módulo de formulários do Django usa o __call__método para implementar uma API consistente para validação de formulários. Você pode escrever seu próprio validador para um formulário no Django como uma função.

def custom_validator(value):
    #your validation logic

O Django possui alguns validadores internos padrão, como validadores de email, validadores de URL, etc., que são amplamente abrangidos pelos validadores RegEx. Para implementar isso de maneira limpa, o Django recorre a classes que podem ser chamadas (em vez de funções). Ele implementa a lógica padrão de Validação de Regex em um RegexValidator e, em seguida, estende essas classes para outras validações.

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

Agora, sua função personalizada e o EmailValidator interno podem ser chamados com a mesma sintaxe.

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

Como você pode ver, essa implementação no Django é semelhante ao que outros explicaram em suas respostas abaixo. Isso pode ser implementado de qualquer outra maneira? Você poderia, mas IMHO não será tão legível nem tão facilmente extensível para uma grande estrutura como o Django.

Praveen Gollakota
fonte
5
Portanto, se usado corretamente, pode tornar o código mais legível. Suponho que se for usado no lugar errado, tornaria o código muito ilegível também.
precisa saber é o seguinte
15
Este é um exemplo de como ele pode ser usado, mas não é bom na minha opinião. Não há vantagem nesse caso em ter uma instância que pode ser chamada. Seria melhor ter uma interface / classe abstrata com um método, como .validate (); é a mesma coisa apenas mais explícita. O valor real de __call__ é poder usar uma instância em um local em que uma chamada é esperada. Uso __call__ com mais frequência ao criar decoradores, por exemplo.
Daniel
120

Este exemplo usa memorização , basicamente armazenando valores em uma tabela (neste caso, dicionário) para que você possa procurá-los mais tarde, em vez de recalculá-los.

Aqui, usamos uma classe simples com um __call__método para calcular fatoriais (por meio de um objeto que pode ser chamado ) em vez de uma função fatorial que contém uma variável estática (como isso não é possível no 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()

Agora você tem um factobjeto que pode ser chamado, assim como todas as outras funções. Por exemplo

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

E também é com estado.

S.Lott
fonte
2
Prefiro ter um factobjeto indexável, pois sua __call__função é essencialmente um índice. Também usaria uma lista em vez de um ditado, mas sou apenas eu.
28811 Chris Lutz
4
@ delnan - Quase tudo pode ser feito de várias maneiras distintas. Qual é o mais legível depende do leitor.
31811 Chris Lutz
1
@ Chris Lutz: Você é livre para contemplar esses tipos de mudanças. Para memorização em geral , um dicionário funciona bem porque você não pode garantir a ordem na qual as coisas preenchem sua lista. Nesse caso, uma lista pode funcionar, mas não será mais rápida ou mais simples.
S.Lott
8
@ delnan: Este não se destina a ser o mais curto. Ninguém ganha no código de golfe. Pretende mostrar __call__, ser simples e nada mais.
S.Lott
3
Mas isso meio que arruina o exemplo quando a técnica demonstrada não é ideal para as tarefas, não é? (E eu não estava falando sobre o "vamos salvar linhas para o inferno" - curto, eu estava falando sobre o "escreva desta maneira igualmente clara e salve algum código padrão" - curto. Tenha certeza de que não sou um daqueles loucos que tentam escrever o código mais curto possível, eu simplesmente quer evitar código clichê que não acrescenta nada para o leitor).
40

Acho útil, pois me permite criar APIs fáceis de usar (você tem algum objeto que requer alguns argumentos específicos) e é fácil de implementar porque é possível usar práticas orientadas a objetos.

A seguir, é o código que escrevi ontem, que cria uma versão dos hashlib.foométodos que misturam arquivos inteiros em vez de seqüências de caracteres:

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

Esta implementação me permite usar as funções de maneira semelhante às hashlib.foofunções:

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

É claro que eu poderia ter implementado de uma maneira diferente, mas, neste caso, parecia uma abordagem simples.

bradley.ayers
fonte
7
Mais uma vez, os fechamentos arruinam este exemplo. pastebin.com/961vU0ay é de 80% das linhas e da mesma forma clara.
8
Não estou convencido de que seria sempre tão claro para alguém (por exemplo, talvez alguém que tenha usado apenas Java). Funções aninhadas e pesquisa / escopo variáveis ​​podem ser confusos. Suponho que meu argumento foi que __call__você fornece uma ferramenta que permite usar técnicas de OO para resolver problemas.
Bradley.ayers
4
Eu acho que a pergunta "por que usar X sobre Y" quando ambos fornecem funcionalidade equivalente é terrivelmente subjetiva. Para algumas pessoas, a abordagem OO é mais fácil de entender, para outras, a abordagem de fechamento é. Não há argumento convincente para usar um sobre o outro, a menos que você tenha uma situação em que precise usar isinstanceou algo semelhante.
Bradley.ayers
2
@ delnan Seu exemplo de fechamento é menos linhas de código, mas é claro que é mais difícil argumentar.
Dennis
8
Um exemplo de onde você prefere usar um __call__método em vez de um fechamento é quando você está lidando com o módulo de multiprocessamento, que usa decapagem para passar informações entre processos. Você não pode selecionar um fechamento, mas pode selecionar uma instância de uma classe.
John Peter Thompson Garcés
21

__call__também é usado para implementar classes decoradoras em python. Nesse caso, a instância da classe é chamada quando o método com o decorador é chamado.

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
fonte
9

Sim, quando você sabe que está lidando com objetos, é perfeitamente possível (e em muitos casos aconselhável) usar uma chamada de método explícita. No entanto, às vezes você lida com código que espera objetos que podem ser chamados - normalmente funciona, mas graças a __call__você pode criar objetos mais complexos, com dados de instância e mais métodos para delegar tarefas repetitivas, etc.

Além disso, às vezes você está usando objetos para tarefas complexas (onde faz sentido escrever uma classe dedicada) e objetos para tarefas simples (que já existem em funções ou são mais facilmente escritas como funções). Para ter uma interface comum, é necessário escrever pequenas classes envolvendo essas funções com a interface esperada ou manter as funções das funções e tornar os objetos mais complexos que podem ser chamados. Vamos dar tópicos como exemplo. Os Threadobjetos do módulo libary padrãothreading querem uma chamada como targetargumento (ou seja, como ação a ser realizada no novo thread). Com um objeto que pode ser chamado, você não está restrito a funções, também pode passar outros objetos, como um trabalhador relativamente complexo que obtém tarefas para executar em outros encadeamentos e os executa sequencialmente:

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)

Este é apenas um exemplo, mas acho que já é complexo o suficiente para justificar a classe. Fazer isso apenas com funções é difícil, pelo menos requer o retorno de duas funções e isso está se tornando lentamente complexo. Um poderia mudar o nome __call__para outra coisa e passar por um método vinculado, mas que faz com que o código criando o fio um pouco menos óbvio, e não adiciona qualquer valor.

yann.kmm
fonte
3
Provavelmente é útil usar a frase "digitação de pato" ( en.wikipedia.org/wiki/Duck_typing#In_Python ) aqui - você pode imitar uma função usando um objeto de classe mais complicado dessa maneira.
Andrew Jaffe
2
Como exemplo relacionado, eu já __call__usei instâncias de classe (em vez de funções) como aplicativos WSGI. Aqui está um exemplo de "The Definitive Guide to Pilões": Usando instâncias de classes
Josh Rosen
5

Decoradores baseados em classe usam __call__para referenciar a função agrupada. Por exemplo:

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

Há uma boa descrição das várias opções aqui em Artima.com

rorycl
fonte
Porém, raramente vejo decoradores de classe, pois eles exigem algum código clichê não óbvio para trabalhar com métodos.
4

O __call__método IMHO e os fechamentos nos fornecem uma maneira natural de criar um padrão de design de ESTRATÉGIA em Python. Definimos uma família de algoritmos, encapsulamos cada um, os tornamos intercambiáveis ​​e, no final, podemos executar um conjunto comum de etapas e, por exemplo, calcular um hash para um arquivo.

ady
fonte
4

Acabei de me deparar com um uso __call__()em conjunto com o __getattr__()qual acho bonito. Ele permite ocultar vários níveis de uma API JSON / HTTP / (porém_serializada) dentro de um objeto.

A __getattr__()parte cuida de retornar iterativamente uma instância modificada da mesma classe, preenchendo mais um atributo de cada vez. Depois que todas as informações tiverem sido esgotadas, __call__()retomará os argumentos que você passou.

Usando esse modelo, você pode, por exemplo, fazer uma chamada como api.v2.volumes.ssd.update(size=20), que termina em uma solicitação PUT para https://some.tld/api/v2/volumes/ssd/update.

O código específico é um driver de armazenamento em bloco para um determinado back-end de volume no OpenStack, você pode conferir aqui: https://github.com/openstack/cinder/blob/master/cinder/volume/drivers/nexenta/jsonrpc.py

EDIT: Atualizado o link para apontar para a revisão principal.

Peter Slovak
fonte
Isso é bom. Uma vez eu usei o mesmo mecanismo para percorrer uma árvore XML arbitrária usando acesso a atributos.
Petri
1

Especifique __metaclass__ae substitua o __call__método e faça com que o método das meta classes especificadas __new__retorne uma instância da classe, viola, você tem uma "função" com os métodos.

user2772852
fonte
1

Podemos usar o __call__método para usar outros métodos de classe como métodos estáticos.

    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
fonte
0

Um exemplo comum é o __call__in functools.partial, aqui está uma versão simplificada (com 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
sem fim
fonte
0

O operador de chamada de função.

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

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

O método __call__ pode ser usado para redefinir / reinicializar o mesmo objeto. Também facilita o uso de instâncias / objetos de uma classe como funções, passando argumentos para os objetos.


fonte
Quando isso seria útil? Foo (1, 2, 3) parece mais claro.
Yaroslav Nikitenko
0

I encontrar um bom lugar para usar objetos que podem ser chamados, aqueles que definem __call__(), é quando usando os recursos de programação funcional em Python, tais como map(), filter(), reduce().

O melhor momento para usar um objeto que pode ser chamado em uma função simples ou lambda é quando a lógica é complexa e precisa reter algum estado ou usar outras informações que não são passadas para a __call__()função.

Aqui está um código que filtra nomes de arquivos com base em sua extensão de nome de arquivo usando um objeto que pode ser chamado 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

Resultado:

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

É tarde demais, mas estou dando um exemplo. Imagine que você tem uma Vectoraula e uma Pointaula. Ambos tomam x, ycomo argumentos posicionais. Vamos imaginar que você queira criar uma função que mova o ponto a ser colocado no vetor.

4 Soluções

  • put_point_on_vec(point, vec)

  • Crie um método na classe de vetor. por exemplo my_vec.put_point(point)

  • Faça disso um método para a Pointclasse.my_point.put_on_vec(vec)
  • Vectorimplementa __call__, então você pode usá-lo comomy_vec_instance(point)

Na verdade, isso é parte de alguns exemplos nos quais estou trabalhando para obter um guia para métodos de dunder explicados com o Maths que lançarei mais cedo ou mais tarde.

Deixei a lógica de mudar o ponto em si, porque não é disso que se trata esta questão

Ahmed I. Elsayed
fonte