Python __call__特殊方法实用例子

Python __call__ special method practical example

我知道类中的__call__方法是在调用类的实例时触发的。但是,我不知道什么时候可以使用这个特殊的方法,因为可以简单地创建一个新方法,并在__call__方法中执行相同的操作,而不是调用实例,您可以调用这个方法。

如果有人给我这个特殊方法的实际用法,我会非常感激的。


这个例子使用memoization,基本上是将值存储在一个表中(本例中是字典),这样您以后就可以查找它们,而不是重新计算它们。

这里我们使用一个带有__call__方法的简单类来计算阶乘(通过一个可调用对象),而不是包含静态变量的阶乘函数(因为在python中这是不可能的)。

1
2
3
4
5
6
7
8
9
10
11
12
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对象,就像其他所有函数一样。例如

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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

它也是庄严的。


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

1
2
def custom_validator(value):
    #your validation logic

Django有一些默认的内置验证器,如电子邮件验证器、URL验证器等,这些验证器大体上属于regex验证器的保护范围。为了干净地实现这些,Django使用可调用类(而不是函数)。它在regexvalidator中实现默认的regex验证逻辑,然后扩展这些类以进行其他验证。

1
2
3
4
5
6
7
8
9
10
11
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。

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

如您所见,Django中的这个实现类似于其他人在下面的答案中所解释的。这能以任何其他方式实现吗?您可以,但imho对于像Django这样的大型框架来说,它的可读性和可扩展性都不高。


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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# 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函数类似的方式使用函数:

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

当然,我可以用不同的方式实现它,但在本例中,它看起来像是一种简单的方法。


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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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()

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

此外,有时您将对象用于复杂任务(在这里编写专用类很有意义),将对象用于简单任务(已经存在于函数中,或者更容易编写为函数)。要有一个公共接口,要么编写用预期接口包装这些函数的小类,要么保留函数函数并使更复杂的对象可调用。让我们以线程为例。来自标准libary模块threadingThread对象需要一个可调用的target参数(即在新线程中要执行的操作)。对于可调用对象,您不受函数限制,也可以传递其他对象,例如相对复杂的工作线程,它从其他线程获取任务并按顺序执行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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__重命名为其他类型,并传递一个绑定方法,但这会使创建线程的代码稍微不那么明显,并且不会增加任何值。


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

1
2
3
4
5
6
7
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上可以很好地描述各种选项。


我只是偶然发现了一种使用__call__()__getattr__()的用法,我认为它很漂亮。它允许您在一个对象中隐藏多个级别的JSON/HTTP/(不过是序列化的)API。

__getattr__()部分负责迭代返回同一类的修改实例,一次再填充一个属性。然后,在所有的信息都用尽之后,__call__()就接管了你所传递的任何论据。

使用此模型,您可以进行一个类似于api.v2.volumes.ssd.update(size=20)的调用,该调用的结果是向https://some.tld/api/v2/volumes/ssd/update发出一个Put请求。

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

编辑:更新链接指向主版本。


imho __call__方法和闭包为我们在Python中创建策略设计模式提供了一种自然的方法。我们定义了一系列算法,封装每个算法,使它们可以互换,最后我们可以执行一组常见的步骤,例如,计算文件的散列值。


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

1
2
3
4
5
6
7
8
9
10
11
12
13
    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")

指定一个__metaclass__并重写__call__方法,并让指定的元类的__new__方法返回该类的一个实例,如果您有一个"函数"和方法。


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

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

下面是一些使用可调用对象和filter()根据文件扩展名筛选文件名的代码。

Callable:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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)

用途:

1
2
3
4
5
6
7
8
9
10
11
filenames = [
    'me.jpg',
    'me.txt',
    'friend1.jpg',
    'friend2.bmp',
    'you.jpeg',
    'you.xml']

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

输出:

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


函数调用运算符。

ZZU1〔4〕

_uuu call_uuu方法可用于重新定义/重新初始化同一对象。它还通过向对象传递参数,方便将类的实例/对象用作函数。


一个常见的例子是functools.partial中的__call__,这里是一个简化版本(python大于等于3.5):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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)

用途:

1
2
3
4
5
def add(x, y):
    return x + y

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