Python _ _ call _ _ 特殊方法实例

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

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

136699 次浏览

本例使用 备忘录,基本上是将值存储在一个表中(本例中为 dictionary) ,这样您可以在以后查找它们,而不必重新计算它们。

在这里,我们使用一个带有 __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

并且 它也是有状态的。

我发现它很有用,因为它允许我创建易于使用的 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')

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

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

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

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__来引用包装函数。例如:

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上有对各种选择的很好的描述

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 这样的大型框架,它将不那么可读,也不那么容易扩展。

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

指定一个 __metaclass__并重写 __call__方法,让指定的元类的 __new__方法返回类的一个实例,中提示您有一个带有方法的“函数”。

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

程序输出:

Entering hello
p1= foo bar
Hello
Leaving hello

我只是偶然发现了 __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

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

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

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

一个常见的例子是 functools.partial中的 __call__,下面是一个简化版本(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

函数调用运算符。

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


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

打电话方法可用于重新定义/重新初始化相同的对象。它还通过向对象传递参数来促进将类的实例/对象用作函数。

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

在普通函数或 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']

这是太晚了,但我给出了一个例子。假设您有一个 Vector类和一个 Point类。两者都以 x, y作为位置参数。假设你想创建一个函数来移动要放在向量上的点。

4种解决方案

  • put_point_on_vec(point, vec)

  • 使它成为向量类的一个方法

  • 使它成为 Point类的一个方法
  • Vector实现 __call__,所以你可以像使用 my_vec_instance(point)一样使用它

这实际上是我正在为 Dunder 方法指南工作的一些示例的一部分,我迟早会发布这些示例。

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

import random




class Bingo:


def __init__(self,items):
self._items=list(items)
random.shuffle(self._items,random=None)




def pick(self):
try:
return self._items.pop()
except IndexError:
raise LookupError('It is empty now!')


def __call__(self):
return self.pick()


b= Bingo(range(3))
print(b.pick())
print(b())
print(callable(b))

现在输出可以是. . (前两个答案一直在[0,3]之间移动)

2

1

没错


您可以看到 Class Bingo 实现了 _ 打电话方法,这是一种创建函数(如对象)的简单方法,这些对象具有内部状态,必须跨调用(如 Bingo 中的剩余项)保持这种状态。_ 打电话的另一个很好的用例是 Decorators。修饰符必须是可调用的,有时候“记住”也很方便 (例如,制表——缓存代价高昂的计算结果,供以后使用。)

我是一个新手,但这是我的看法: 拥有 __call__可以使作文更容易编码。如果 f, g是具有 eval(self,x)方法的类 Function的实例,那么使用 __call___可以编写 f(g(x))而不是 f.eval(g.eval(x))

一个神经网络可以由更小的神经网络组成,在 pytorch 中,我们有一个模块类的 __call__:

下面是在实践中使用 __call__的一个例子: 在 pytch 中,当将一个神经网络(例如称为 class MyNN(nn.Module))定义为 torch.nn.module 的子类时。模块,一个典型地为类定义一个 forward方法,但是典型地当应用一个输入张量 x到一个实例 model=MyNN()时,我们只写 model(x)而不是 model.forward(x),但是两者给出相同的答案。如果您深入研究 torch.nn.module 的源代码。模组在这里

Https://pytorch.org/docs/stable/_modules/torch/nn/modules/module.html#module

并搜索 __call__,人们最终可以追溯到(至少在某些情况下)对 self.forward的呼叫

import torch.nn as nn
import torch.nn.functional as F


class MyNN(nn.Module):
def __init__(self):
super().__init__()
self.layer1 = nn.Linear(784, 200)






def forward(self, x):
return F.ReLU(self.layer1(x))


x=torch.rand(10,784)
model=MyNN()
print(model(x))
print(model.forward(x))


将在最后两行中打印与 Module 类实现的 __call___相同的值,所以我相信 Python 在看到 model(x)时会转向这个值,而 __call__最终会调用 model.forward(x))