Python __call__特殊方法的实际示例


159

我知道__call__调用类的实例时会触发类中的方法。但是,我不知道何时可以使用这种特殊方法,因为一个人可以简单地创建一个新方法并执行在__call__方法中完成的相同操作,而无需调用实例,而可以调用该方法。

如果有人给我这种特殊方法的实际用法,我将不胜感激。



8
_call_的功能就像C ++中()的重载运算符一样。如果仅在类外部创建新方法,则可能无法访问类中的内部数据。
安迪2014年

2
最常见的用法__call__隐藏在普通视图中;实例化类的方法:x = Foo()实际上x = type(Foo).__call__(Foo)__call__是由的元类定义的位置Foo
chepner

Answers:


88

Django表单模块__call__很好地使用了方法来实现用于表单验证的一致API。您可以在Django中将自己的验证器作为函数编写。

def custom_validator(value):
    #your validation logic

Django有一些默认的内置验证器,例如电子邮件验证器,URL验证器等,它们大体上属于RegEx验证器。为了清晰地实现这些,Django求助于可调用类(而不是函数)。它在RegexValidator中实现默认的Regex验证逻辑,然后扩展这些类以进行其他验证。

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

现在,可以使用相同的语法调用自定义函数和内置的EmailValidator。

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

如您所见,Django中的此实现类似于其他人在下面的答案中解释的实现。可以通过其他任何方式实现吗?您可以,但是恕我直言,对于像Django这样的大型框架,它不那么易读或易于扩展。


5
因此,如果使用正确,它可使代码更具可读性。我假设如果在错误的地方使用它,那么代码也将变得非常不可读。
mohi666

15
这是如何使用它的一个例子,但我认为这不是一个好例子。在这种情况下,拥有可调用实例没有任何优势。最好让接口/抽象类带有一个方法,例如.validate();。这是同一件事,只是更加明确。__call__的真正价值在于能够在预期可调用的地方使用实例。例如,在创建装饰器时,我最常使用__call__。
丹尼尔(Daniel)

120

本示例使用备忘录,基本上将值存储在表(在这种情况下为字典)中,因此您以后可以查找它们,而无需重新计算它们。

在这里,我们使用带有__call__方法的简单类(通过可调用对象)来计算阶乘,而不是包含静态变量的阶乘函数(这在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()

现在,您拥有一个fact可以调用的对象,就像其他所有函数一样。例如

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

而且它也是有状态的。


2
我宁愿有一个fact可索引的对象,因为您的__call__函数本质上是一个索引。也将使用列表而不是字典,但这就是我。
克里斯·卢兹

4
@delnan-几乎所有方法都可以通过几种不同的方法来完成。哪个更具可读性取决于读者。
克里斯·卢茨

1
@Chris Lutz:您可以自由地考虑这些变化。对于一般的记忆,字典可以很好地工作,因为您不能保证列表中所有内容的排列顺序。在这种情况下,列表可能会起作用,但不会更快或更简单。
S.Lott

8
@delnan:这并不是最短的。没有人在标准高尔夫比赛中获胜。它旨在显示__call__,保持简单,仅此而已。
S.Lott

3
但是,当演示的技术不适合完成任务时,这会毁了示例,不是吗?(而且,我并不是在说“让我们节省很多钱”-简短,而是在谈论“以同样清晰的方式编写并保存一些样板代码”-简短。请放心,我不会那些试图编写最短代码的狂人之一,我只想避免为读者增加任何东西的样板代码。)

40

我发现它很有用,因为它允许我创建易于使用的API(您有一些需要一些特定参数的可调用对象),并且易于实现,因为您可以使用面向对象的实践。

以下是我昨天编写的代码,该代码使hashlib.foo散列整个文件而不是字符串的方法的版本变了:

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

此实现使我能够以类似于功能的方式使用hashlib.foo功能:

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

当然,我可以用其他方式实现它,但是在这种情况下,这似乎是一种简单的方法。


7
同样,闭包破坏了这个例子。pastebin.com/961vU0ay是80%的行,也很清晰。

8
我不相信对于某人(例如,也许只使用过Java的人),它总是一样清晰的。嵌套函数和变量查找/作用域可能会造成混淆。我想我的目的是__call__为您提供一个工具,让您可以使用OO技术解决问题。
bradley.ayers 2011年

4
我认为当两者都提供等效功能时,“为什么在X上使用X而不是Y”这个问题非常主观。对于某些人来说,OO方法更容易理解,而对于另一些人,闭包方法则更容易理解。没有一个令人信服的论据来使用一个在另一个之上,除非您遇到必须使用isinstance或类似情况的情况。
bradley.ayers 2011年

2
@delnan您的闭包示例使用的代码较少,但是说起来也更加困难。
丹尼斯

8
__call__在处理多处理模块时,您更希望使用方法而不是闭包的一个示例,该模块使用酸洗在进程之间传递信息。您不能腌制闭包,但可以腌制类的实例。
John Peter ThompsonGarcés'13

21

__call__还用于在python中实现装饰器类。在这种情况下,当调用带有装饰器的方法时,将调用类的实例。

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

是的,当您知道要处理对象时,完全有可能(在很多情况下建议使用)显式方法调用。但是,有时您要处理期望可调用对象的代码-通常是函数,但是由于__call__您可以构建更复杂的对象,实例数据和更多方法来委派仍可调用的重复性任务等。

另外,有时您同时将对象用于复杂的任务(编写专用类是有意义的)和对象将用于简单任务(已存在于函数中,或者更容易编写为函数)使用。要拥有一个通用的接口,您要么必须编写用期望的接口包装这些函数的微型类,要么保留这些函数并使更复杂的对象可调用。让我们以线程为例。Thread来自标准库模块threading对象需要一个可调用的target参数(即在新线程中执行的操作)。对于可调用对象,您不仅可以使用函数,还可以传递其他对象,例如相对复杂的工作程序,该工作程序从其他线程获取要执行的任务并依次执行:

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)

这只是我脑中浮现的一个例子,但我认为它已经足够复杂,足以保证上课。仅使用函数很难做到这一点,至少它需要返回两个函数,并且这种情况正在逐渐变得复杂。一个可以重命名__call__到别的东西,并通过绑定方法,但是这使得代码稍微不太明显的创建线程,并且不会增加任何价值。


3
在此处使用短语“鸭子打字”(en.wikipedia.org/wiki/Duck_typing#In_Python)可能很有用-您可以通过这种方式使用更复杂的类对象来模拟函数。
Andrew Jaffe

2
作为一个相关示例,我已经看到__call__曾经使用类实例(而不是函数)作为WSGI应用程序。这是《定向塔指南》中的示例:使用类的实例
Josh Rosen

5

基于类的装饰器__call__用来引用包装的函数。例如:

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

Artima.com上的各种选项都有很好的描述


我很少看到类装饰器,因为它们需要一些非显而易见的样板代码才能与方法一起使用。

4

IMHO __call__方法和闭包为我们提供了一种在Python中创建STRATEGY设计模式的自然方法。我们定义了一系列算法,将每个算法封装在一起,使其可互换,最后我们可以执行一组通用步骤,例如,计算文件的哈希值。


4

我只是偶然发现了一种我认为很美的__call__()音乐会__getattr__()。它允许您在对象内部隐藏JSON / HTTP /(however_serialized)API的多个级别。

__getattr__()部分负责迭代地返回相同类的修改后的实例,并一次填充一个以上的属性。然后,在用尽所有信息之后,__call__()接管您传入的所有参数。

例如,使用该模型,您可以进行调用,例如api.v2.volumes.ssd.update(size=20),最终导致对的PUT请求https://some.tld/api/v2/volumes/ssd/update

特定的代码是OpenStack中特定卷后端的块存储驱动程序,您可以在此处查看:https : //github.com/openstack/cinder/blob/master/cinder/volume/drivers/nexenta/jsonrpc.py

编辑:更新了链接以指向主修订。


真好。我曾经使用相同的机制通过属性访问遍历任意XML树。
Petri

1

指定一个__metaclass__并覆盖该__call__方法,并让指定的元类的__new__方法返回该类的实例,中提琴表示您具有带有方法的“函数”。


1

我们可以使用__call__method来将其他类方法用作静态方法。

    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

一个常见的示例是__call__in functools.partial,这是一个简化的版本(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)

用法:

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

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

0

函数调用运算符。

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

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

__call__方法可用于重新定义的/重新初始化相同的对象。通过将参数传递给对象,还有助于将类的实例/对象用作函数。


什么时候有用?Foo(1、2、3)似乎更清晰。
雅罗斯拉夫·尼基琴科

0

我发现使用可调用对象的好地方,那些定义__call__(),是使用Python中的函数式编程功能时,比如map()filter()reduce()

在普通函数或lambda函数上使用可调用对象的最佳时间是逻辑复杂且需要保留某些状态或使用其他未传递给__call__()函数的信息时。

以下是一些代码,它们使用可调用对象和,根据文件名的扩展名过滤文件名filter()

可致电:

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)

用法:

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

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

输出:

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

0

现在为时已晚,但我举一个例子。假设您有一Vector堂课和一Point堂课。两者都x, y作为位置参数。假设您要创建一个函数来移动要放在矢量上的点。

4个解决方案

  • put_point_on_vec(point, vec)

  • 使其成为vector类的方法。例如 my_vec.put_point(point)

  • 使它成为Point该类的一个方法。my_point.put_on_vec(vec)
  • Vector工具__call__,因此您可以像my_vec_instance(point)

这实际上是我正在研究的一些示例的一部分,该指南针对用数学解释的Dunder方法指南(我迟早要发布)。

我离开了移动点本身的逻辑,因为这不是这个问题的目的

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.