Python __call__ método especial ejemplo práctico


159

Sé que el __call__método en una clase se activa cuando se llama a la instancia de una clase. Sin embargo, no tengo idea de cuándo puedo usar este método especial, porque uno simplemente puede crear un nuevo método y realizar la misma operación realizada en el __call__método y en lugar de llamar a la instancia, puede llamar al método.

Realmente agradecería si alguien me da un uso práctico de este método especial.



8
La funcionalidad de _call_ es como el operador sobrecargado de () en C ++ . Si simplemente crea un nuevo método fuera de la clase, no podrá acceder a los datos internos de una clase.
Andy

2
El uso más común de __call__está oculto a simple vista; es cómo instanciar una clase: x = Foo()es realmente x = type(Foo).__call__(Foo), donde __call__está definida por la metaclase de Foo.
chepner

Respuestas:


88

El módulo de formularios de Django utiliza el __call__método muy bien para implementar una API consistente para la validación de formularios. Puede escribir su propio validador para un formulario en Django como una función.

def custom_validator(value):
    #your validation logic

Django tiene algunos validadores integrados predeterminados, como validadores de correo electrónico, validadores de URL, etc., que en general se encuentran bajo el paraguas de los validadores RegEx. Para implementarlos limpiamente, Django recurre a clases invocables (en lugar de funciones). Implementa la lógica de validación Regex predeterminada en un RegexValidator y luego extiende estas clases para otras validaciones.

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

Ahora tanto su función personalizada como el EmailValidator incorporado se pueden invocar con la misma sintaxis.

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

Como puede ver, esta implementación en Django es similar a lo que otros han explicado en sus respuestas a continuación. ¿Se puede implementar de alguna otra manera? Podría, pero en mi humilde opinión, no será tan legible ni tan fácilmente extensible para un gran marco como Django.


55
Entonces, si se usa correctamente, puede hacer que el código sea más legible. Supongo que si se usa en el lugar equivocado, también haría que el código sea muy ilegible.
mohi666

15
Este es un ejemplo de cómo se puede usar, pero no es bueno en mi opinión. No hay ninguna ventaja en este caso para tener una instancia invocable. Sería mejor tener una interfaz / clase abstracta con un método, como .validate (); Es lo mismo, pero más explícito. El valor real de __call__ es poder usar una instancia en un lugar donde se espera una llamada. Utilizo __call__ con mayor frecuencia cuando creo decoradores, por ejemplo.
Daniel

120

Este ejemplo utiliza la memorización , básicamente almacenando valores en una tabla (diccionario en este caso) para que pueda buscarlos más tarde en lugar de volver a calcularlos.

Aquí usamos una clase simple con un __call__método para calcular factoriales (a través de un objeto invocable ) en lugar de una función factorial que contiene una variable estática (ya que eso no es posible en 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()

Ahora tiene un factobjeto invocable, como cualquier otra función. Por ejemplo

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

Y también tiene estado.


2
Prefiero tener un factobjeto que sea indexable ya que su __call__función es esencialmente un índice. También usaría una lista en lugar de un dict, pero solo soy yo.
Chris Lutz

44
@delnan: casi todo se puede hacer de varias maneras distintas. Cuál es más legible depende del lector.
Chris Lutz

1
@ Chris Lutz: Eres libre de contemplar ese tipo de cambios. Para la memorización en general , un diccionario funciona bien porque no puede garantizar el orden en que las cosas llenan su lista. En este caso, una lista podría funcionar, pero no será más rápida ni más simple.
S.Lott

8
@delnan: Esto no pretende ser el más corto. Nadie gana en el código de golf. Está destinado a mostrar __call__, ser simple y nada más.
S.Lott

3
Pero arruina el ejemplo cuando la técnica demostrada no es ideal para las tareas, ¿no? (Y no me refería al breve "guardar líneas por el gusto de hacerlo", estaba hablando del "escribirlo de esta manera igualmente clara y guardar un poco de código repetitivo". Tenga la seguridad de que no estoy uno de esos locos que intentan escribir el código más corto posible, simplemente quiero evitar el código repetitivo que no agrega nada para el lector.)

40

Lo encuentro útil porque me permite crear API que son fáciles de usar (tiene algún objeto invocable que requiere algunos argumentos específicos) y es fácil de implementar porque puede usar prácticas orientadas a objetos.

El siguiente es el código que escribí ayer que hace una versión de los hashlib.foométodos que procesan archivos enteros en lugar de cadenas:

# 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 implementación me permite usar las funciones de manera similar a las hashlib.foofunciones:

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

Por supuesto, podría haberlo implementado de una manera diferente, pero en este caso parecía un enfoque simple.


77
Nuevamente, los cierres arruinan este ejemplo. pastebin.com/961vU0ay es el 80% de las líneas e igual de claro.

8
No estoy convencido de que siempre sea tan claro para alguien (por ejemplo, tal vez alguien que solo ha usado Java). Las funciones anidadas y la búsqueda / alcance variable pueden ser confusas. Supongo que mi punto era que __call__te brinda una herramienta que te permite usar técnicas OO para resolver problemas.
bradley.ayers

44
Creo que la pregunta "por qué usar X sobre Y" cuando ambos proporcionan una funcionalidad equivalente es terriblemente subjetiva. Para algunas personas, el enfoque OO es más fácil de entender, para otras es el enfoque de cierre. No hay un argumento convincente para usar uno sobre el otro, a menos que haya tenido una situación en la que tuvo que usar isinstanceo algo similar.
bradley.ayers

2
@delnan Su ejemplo de cierre es menos líneas de código, pero que sea igual de claro es más difícil de discutir.
Dennis

8
Un ejemplo de dónde preferiría usar un __call__método en lugar de un cierre es cuando se trata del módulo de multiprocesamiento, que utiliza el decapado para pasar información entre procesos. No puede encurtir un cierre, pero puede encurtir una instancia de una clase.
John Peter Thompson Garcés

21

__call__también se usa para implementar clases de decorador en python. En este caso, se llama a la instancia de la clase cuando se llama al método con el decorador.

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í, cuando sabes que estás tratando con objetos, es perfectamente posible (y en muchos casos aconsejable) usar una llamada a un método explícito. Sin embargo, a veces se trata de código que espera objetos invocables, por lo general, funciones, pero gracias a __call__él puede construir objetos más complejos, con datos de instancia y más métodos para delegar tareas repetitivas, etc. que todavía son invocables.

Además, a veces está utilizando tanto objetos para tareas complejas (donde tiene sentido escribir una clase dedicada) como objetos para tareas simples (que ya existen en funciones o que se escriben más fácilmente como funciones). Para tener una interfaz común, debe escribir clases pequeñas que envuelvan esas funciones con la interfaz esperada, o mantener las funciones de las funciones y hacer que los objetos más complejos sean invocables. Tomemos los hilos como ejemplo. Los Threadobjetos del módulo de biblioteca estándarthreading quieren un llamado como targetargumento (es decir, como acción a realizar en el nuevo hilo). Con un objeto invocable, no está restringido a funciones, también puede pasar otros objetos, como un trabajador relativamente complejo que obtiene tareas de otros subprocesos y las ejecuta secuencialmente:

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 es solo un ejemplo fuera de mi cabeza, pero creo que ya es lo suficientemente complejo como para justificar la clase. Hacer esto solo con funciones es difícil, al menos requiere devolver dos funciones y eso se vuelve lentamente complejo. Se podría cambiar el nombre __call__a otra cosa y pasar un método enlazado, pero eso hace que el código que crea el hilo sea un poco menos obvio, y no agrega ningún valor.


3
Probablemente sea útil usar la frase "tipear pato" ( en.wikipedia.org/wiki/Duck_typing#In_Python ) aquí: puede imitar una función utilizando un objeto de clase más complicado de esta manera.
Andrew Jaffe

2
Como ejemplo relacionado, he visto que __call__solía usar instancias de clase (en lugar de funciones) como aplicaciones WSGI. Aquí hay un ejemplo de "La guía definitiva para pilones": Uso de instancias de clases
Josh Rosen

5

Los decoradores basados ​​en clases usan __call__para hacer referencia a la función envuelta. P.ej:

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

Hay una buena descripción de las diversas opciones aquí en Artima.com


Sin embargo, rara vez veo decoradores de clase, ya que requieren un código repetitivo no obvio para trabajar con los métodos.

4

El __call__método IMHO y los cierres nos dan una forma natural de crear un patrón de diseño STRATEGY en Python. Definimos una familia de algoritmos, encapsulamos cada uno, los hacemos intercambiables y al final podemos ejecutar un conjunto común de pasos y, por ejemplo, calcular un hash para un archivo.


4

Me topé con un uso de __call__()concierto con el __getattr__()que creo que es hermoso. Le permite ocultar múltiples niveles de una API JSON / HTTP / (sin embargo, serializada) dentro de un objeto.

La __getattr__()parte se encarga de devolver iterativamente una instancia modificada de la misma clase, completando un atributo más a la vez. Luego, después de que toda la información se haya agotado, se __call__()hace cargo de cualquier argumento que haya pasado.

Con este modelo, puede, por ejemplo, realizar una llamada como api.v2.volumes.ssd.update(size=20), que termina en una solicitud PUT a https://some.tld/api/v2/volumes/ssd/update.

El código particular es un controlador de almacenamiento en bloque para un cierto back-end de volumen en OpenStack, puede consultarlo aquí: https://github.com/openstack/cinder/blob/master/cinder/volume/drivers/nexenta/jsonrpc.py

EDITAR: Se actualizó el enlace para apuntar a la revisión maestra.


Eso es bueno. Una vez utilicé el mismo mecanismo para atravesar un árbol XML arbitrario utilizando el acceso a atributos.
Petri

1

Especifique __metaclass__ay anule el __call__método, y haga que el método de las metaclases especificadas __new__devuelva una instancia de la clase, si tiene una "función" con los métodos.


1

Podemos usar el __call__método para usar otros métodos de clase 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")             

0

Un ejemplo común es __call__in functools.partial, aquí hay una versión simplificada (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

El operador de llamada de función.

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

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

El método __call__ puede usarse para redefinir / reinicializar el mismo objeto. También facilita el uso de instancias / objetos de una clase como funciones al pasar argumentos a los objetos.


¿Cuándo sería útil? Foo (1, 2, 3) parece más claro.
Yaroslav Nikitenko

0

Me parece un lugar bueno para usar los objetos que se puede llamar, los que definen __call__(), es decir cuando se utilizan las capacidades de programación en Python funcionales, tales como map(), filter(), reduce().

El mejor momento para usar un objeto invocable sobre una función simple o una función lambda es cuando la lógica es compleja y necesita retener algún estado o utiliza otra información que no se pasa a la __call__()función.

Aquí hay un código que filtra los nombres de archivos según su extensión de nombre de archivo utilizando un objeto invocable y filter().

Llamable:

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

Salida:

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

0

Esto es demasiado tarde pero estoy dando un ejemplo. Imagina que tienes una Vectorclase y una Pointclase. Ambos toman x, ycomo argumentos posicionales. Imaginemos que desea crear una función que mueva el punto que se colocará en el vector.

4 soluciones

  • put_point_on_vec(point, vec)

  • Que sea un método en la clase de vector. p.ej my_vec.put_point(point)

  • Que sea un método en la Pointclase.my_point.put_on_vec(vec)
  • Vectorimplementa __call__, para que pueda usarlo comomy_vec_instance(point)

En realidad, esto es parte de algunos ejemplos en los que estoy trabajando para obtener una guía de métodos dunder explicada con Maths que lanzaré tarde o temprano.

Dejé la lógica de mover el punto en sí porque esto no es de lo que se trata esta pregunta

Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.