这一切都非常好,因为它是准确的,但是它并没有真正使任何事情变得更清晰,并且在没有输入数千个几乎肯定会犯错误的单词的情况下,我可能更好地引用一个知道的人关于 Python 的一些东西。
吉多·范·罗森 (Guido van Rossum) 谈延期 https://groups.google.com/forum/#!topic/python-tulip/ut4vTG-08k8/discussion:
这是我尝试解释 Deferred 的伟大想法(并且有很多
)面向以前没有 Twisted 经验的高级 Python 用户。
我还假设您之前考虑过异步调用。只是
为了惹恼 Glyph,我使用 5 星系统来表示重要性
想法,其中 1 星是“好主意,但相当明显”,5 星是
是“辉煌”的。
我展示了很多代码片段,因为有些想法是最好的
那样表达——但我故意省略了很多细节,
有时我会展示有错误的代码,如果修复它们会减少
理解代码背后的想法。 (我会指出这些错误。)
我正在使用Python 3。
特别针对 Glyph 的注释:(a) 将其视为博客草稿
邮政。我非常乐意接受更正和建议
改进。 (b) 这并不意味着我要将 Tulip 更改为
更像 Deferred 的模型;但这是针对不同的线程的。
想法 1:返回一个特殊对象而不是接受回调参数
在设计异步生成结果的 API 时,您会发现
你需要一个回调系统。通常是第一个出现的设计
要记住的是传递一个回调函数,当
异步操作完成。我什至见过一些设计,如果你不这样做的话
传入一个回调,操作是同步的——这已经够糟糕的了
我会给它零星。但即使是一星版本也会污染所有
带有额外参数的 API 必须繁琐地传递。
Twisted 的第一个伟大想法是,最好返回一个特殊的
调用者收到后可以添加回调的对象。我
给这个三颗星,因为从中萌芽出许多其他的好东西
想法。这当然类似于期货和期货的基本思想
许多语言和库中都存在 Promise,例如蟒蛇的
并发.futures(PEP 3148,紧随 Java Futures,两者
这适用于线程世界),现在是 Tulip(PEP 3156,使用
类似的设计适用于无线程异步操作)。
想法2:将结果从回调传递到回调
我认为最好先展示一些代码:
class Deferred:
def __init__(self):
self.callbacks = []
def addCallback(self, callback):
self.callbacks.append(callback) # Bug here
def callback(self, result):
for cb in self.callbacks:
result = cb(result)
最有趣的部分是最后两行:每行的结果
回调被传递给下一个。这与事情的运作方式不同
在并发.futures 和 Tulip 中,结果(一旦设置)是固定的
作为未来的一个属性。这里的结果可以由每个人修改
打回来。
当一个函数返回 Deferred 时,这将启用一种新模式
调用另一个并转换其结果,这就是收益
这个想法三颗星。例如,假设我们有一个异步函数
读取一组书签,我们想要编写一个异步函数
调用它然后对书签进行排序。而不是发明一个
一个异步函数可以等待另一个异步函数的机制(我们
无论如何稍后会做:-),第二个异步函数可以简单地添加一个
对第一个返回的 Deferred 的新回调:
def read_bookmarks_sorted():
d = read_bookmarks()
d.addCallback(sorted)
return d
该函数返回的 Deferred 表示一个排序列表
书签。如果其调用者想要打印这些书签,则必须添加
另一个回调:
d = read_bookmarks_sorted()
d.addCallback(print)
在异步结果由 Future 表示的世界中,这也是一样的
示例将需要两个单独的 Futures:一个由
read_bookmarks() 表示未排序的列表,以及一个单独的 Future
由 read_bookmarks_sorted() 返回,表示排序列表。
该版本的类中有一个不明显的错误:如果
addCallback() 在 Deferred 已经触发后被调用(即它的
调用了callback()方法)然后通过addCallback()添加了回调
永远不会被调用。解决这个问题很容易,但是很乏味,而且
你可以在 Twisted 源代码中查找它。我会携带这个bug
通过连续的例子——假装你生活在一个世界里
结果永远不会太快。还有其他问题
也采用这种设计,但我宁愿将解决方案称为改进
比错误修复。
旁白:Twisted 的术语选择不佳
我不知道为什么,但是,从项目自己的名称 Twisted 开始
它对事物的名称选择常常让我感到不舒服。为了
例如,我真的很喜欢类名应该是名词的准则。
但“Deferred”是一个形容词,而不仅仅是一个形容词,它是一个
动词的过去分词(而且过长:-)。为什么是
它在一个名为twisted.internet的模块中吗?
然后是“回调”,用于两个相关但不同的
用途:它是用于表示函数的首选术语,该函数将被
当结果准备好时调用,但它也是方法的名称
您调用“触发”延迟,即设置(初始)结果。
别让我开始谈论“errback”这个新词/混成词,
这导致我们...
想法 3:集成错误处理
这个想法只得到两颗星(我相信这会让很多人失望)
扭曲的粉丝)因为这让我很困惑。我还注意到
Twisted 文档在解释它是如何工作时遇到了一些困难——在这种情况下
特别是我发现阅读代码比阅读代码更有帮助
文档。
基本想法很简单:如果解雇的承诺会怎样?
延期结果无法实现?当我们写的时候
d = pod_bay_doors.open()
d.addCallback(lambda _: pod.launch())
HAL 9000 应该如何说“对不起,戴夫。恐怕我不能
去做” ?
即使我们不关心这个答案,如果其中之一我们应该怎么做
回调引发异常?
Twisted 的解决方案是将每个回调分为一个回调和
一个“错误”。但这还不是全部——为了处理异常
由回调引发,它还引入了一个新类“Failure”。 ID
其实喜欢先介绍后者,不介绍
错误返回:
class Failure:
def __init__(self):
self.exception = sys.exc_info()[1]
(顺便说一句,很棒的类名。我的意思是,我不是
讽刺。)
现在我们可以重写callback()方法,如下所示:
def callback(self, result):
for cb in self.callbacks:
try:
result = cb(result)
except:
result = Failure()
这本身我会给两颗星;回调可以使用
isinstance(result, Failure) 区分常规结果
失败。
顺便说一句,在 Python 3 中,也许可以取消
单独的 Failure 类封装异常,然后使用
内置 BaseException 类。通过阅读代码中的注释,
Twisted 的 Failure 类主要存在,以便它可以容纳所有
sys.exc_info()返回的信息,即异常类/类型,
异常实例和回溯,但在 Python 3 中,异常对象
已经保存了对回溯的引用。有一些调试内容
Twisted 的 Failure 类执行标准异常不执行的操作,但是
尽管如此,我认为引入单独的类的大多数原因是
已解决。
但我们不要忘记错误返回。我们更改列表
回调到回调函数对的列表,我们重写
再次callback()方法,如下:
def callback(self, result):
for (cb, eb) in self.callbacks:
if isinstance(result, Failure):
cb = eb # Use errback
try:
result = cb(result)
except:
result = Failure()
为了方便起见,我们还添加了一个 errback() 方法:
def errback(self, fail=None):
if fail is None:
fail = Failure()
self.callback(fail)
(真正的errback()函数还有一些特殊情况,可以是
以异常或失败作为参数调用,并且
失败类采用可选的异常参数来防止它
使用 sys.exc_info()。但这些都不是必要的,它使得
代码片段更复杂。)
为了确保 self.callbacks 是一个对的列表,我们还必须
更新 addCallback() (之后调用时仍然无法正常工作
Deferred 已被解雇):
def addCallback(self, callback, errback=None):
if errback is None:
errback = lambda r: r
self.callbacks.append((callback, errback))
如果仅使用回调函数调用此函数,则 errback 将是
传递结果(即失败实例)的虚拟对象
不变。这会保留后续错误的错误条件
处理程序。为了方便添加错误处理程序而无需处理
常规结果,我们添加addErrback(),如下:
def addErrback(self, errback):
self.addCallback(lambda r: r, errback)
在这里,该对的回调一半将传递(非失败)结果
通过不变到下一个回调。
如果您想获得全部动力,请阅读 Twisted 的简介
延期;最后我会注意到一个错误返回并替换一个
仅通过返回非失败值来获得失败的常规结果
(包括无)。
在我继续讨论下一个想法之前,让我指出有
真正的 Deferred 类中有更多细节。例如,您可以指定
要传递给回调和错误返回的附加参数。但在
紧要关头你可以用 lambda 来做到这一点,所以我把它省略了,因为
用于进行管理的额外代码并未阐明
基本思想。
想法 4:链接延迟
这是一个五星级的想法!有时确实需要一个
回调以等待额外的异步事件,然后才能生成
想要的结果。例如,假设我们有两个基本的异步
操作,read_bookmarks() 和sync_bookmarks(),我们想要一个
联合操作。如果这是同步代码,我们可以编写:
def sync_and_read_bookmarks():
sync_bookmarks()
return read_bookmarks()
但是如果所有操作都返回 Deferreds,我们该如何编写呢?随着
链式的想法,我们可以这样做:
def sync_and_read_bookmarks():
d = sync_bookmarks()
d.addCallback(lambda unused_result: read_bookmarks())
return d
之所以需要 lambda,是因为所有回调都是通过结果调用的
值,但 read_bookmarks() 不带参数。