gpt4 book ai didi

python - 为什么回调是 "ugly"?

转载 作者:太空狗 更新时间:2023-10-30 00:45:26 26 4
gpt4 key购买 nike

关闭。这个问题是opinion-based .它目前不接受答案。












想改善这个问题吗?更新问题,以便可以通过 editing this post 用事实和引文回答问题.

8年前关闭。




Improve this question




最近我听了 Guido van Rossum 关于 Python3 中的异步 I/O 的演讲。我对开发人员“讨厌”回调的概念感到惊讶,据说是因为它很丑。我还发现了协程的概念,并开始阅读 David Beazley 的协程教程。到目前为止,协程对我来说仍然看起来很深奥——比那些“讨厌的”回调太晦涩难懂了。

现在我试图找出为什么有些人认为回调很难看。诚然,有了回调,程序看起来不再像一段线性代码,执行单个算法。但是,好吧,它不是——只要它有异步 I/O——并且假装它是没有好处的。相反,我认为这样的程序是事件驱动的——你通过定义它如何对相关事件使用react来编写它。

或者除了使程序“非线性”之外,还有其他关于协程的东西,被认为是不好的?

最佳答案

考虑以下用于读取协议(protocol) header 的代码:

def readn(sock, n):
buf = ''
while n > len(buf):
newbuf = sock.recv(n - len(buf))
if not newbuf:
raise something
buf += newbuf
return buf

def readmsg(sock):
msgtype = readn(sock, 4).decode('ascii')
size = struct.unpack('!I', readn(sock, 4))
data = readn(sock, size)
return msgtype, size, data

显然,如果你想一次处理多个用户,你不能循环阻塞 recv那样的称呼。所以,你可以做什么?

如果你使用线程,你不需要对这段代码做任何事情;只需在单独的线程上运行每个客户端,一切都很好。这就像魔术。线程的问题是你不能同时运行 5000 个线程,而不会让你的调度程序慢下来,分配这么多的堆栈空间,你会进入交换 hell 等等。 所以,问题是,我们如何得到没有问题的线程的魔力?

隐式 greenlets 是该问题的一种解决方案。基本上,您编写线程代码,它实际上由协作调度程序运行,每次您进行阻塞调用时都会中断您的代码。问题是这涉及对所有已知的阻塞调用进行猴子修补,并希望您安装的库不会添加任何新的库。

协程是这个问题的答案。如果您通过删除 yield from 显式标记每个阻塞函数调用在此之前,没有人需要修补任何东西。您仍然需要调用与异步兼容的函数,但不再可能在没有预料到的情况下阻塞整个服务器,并且从您的代码中可以更清楚地了解正在发生的事情。缺点是幕后的 react 器代码必须更复杂......但这是你编写一次(或者,更好,零次,因为它来自框架或标准库)。

使用回调,您编写的代码最终将与协程完全相同,但复杂性现在在您的协议(protocol)代码中。您必须有效地将控制流程由内而外转变。相比之下,最明显的翻译相当可怕:
def readn(sock, n, callback):
buf = ''
def on_recv(newbuf):
nonlocal buf, callback
if not newbuf:
callback(None, some error)
return
buf += newbuf
if len(buf) == n:
callback(buf)
async_read(sock, n - len(buf), on_recv)
async_read(sock, n, on_recv)

def readmsg(sock, callback):
msgtype, size = None, None
def on_recv_data(buf, err=None):
nonlocal data
if err: callback(None, err)
callback(msgtype, size, buf)
def on_recv_size(buf, err=None):
nonlocal size
if err: callback(None, err)
size = struct.unpack('!I', buf)
readn(sock, size, on_recv_data)
def on_recv_msgtype(buf, err=None):
nonlocal msgtype
if err: callback(None, err)
msgtype = buf.decode('ascii')
readn(sock, 4, on_recv_size)
readn(sock, 4, on_recv_msgtype)

现在,显然,在现实生活中,任何以这种方式编写回调代码的人都应该被 Gunicorn ;有更好的方法来组织它,比如使用 Futures 或 Deferreds,使用带有方法的类而不是一堆以相反顺序定义的本地闭包,等等。

但关键是,没有办法以一种看起来更像同步版本的方式来编写它。控制流本质上是中心的,协议(protocol)逻辑是次要的。使用协程,因为控制流总是“向后”的,它在你的代码中根本不明确,协议(protocol)逻辑就是读写。

话虽如此,在很多地方,使用回调编写某些东西的最佳方式比协程(或同步)版本更好,因为代码的重点是将异步事件链接在一起。

如果您通读了 Twisted 教程,您会发现让这两种机制很好地协同工作并不难。如果您围绕 Deferreds 编写所有内容,则可以自由使用 Deferred 组合函数、显式回调和 @inlineCallbacks风格的协程。在你的代码的某些部分,控制流很重要,逻辑很琐碎;在其他部分,逻辑很复杂,您不希望它被控制流所掩盖。因此,您可以在每种情况下使用任何有意义的方法。

事实上,值得将生成器作为协同程序与生成器作为迭代器进行比较。考虑:
def squares(n):
for i in range(n):
yield i*i

def squares(n):
class Iterator:
def __init__(self):
self.i = 0
def __iter__(self):
return self
def __next__(self):
i, self.i = self.i, self.i+1
return i*i
return Iterator(n)

第一个版本隐藏了很多“魔法”—— next 之间迭代器的状态调用在任何地方都不明确;它隐含在生成器函数的局部框架中。每次你做 yield ,整个程序的状态可能在 yield 之前已经改变返回。然而,第一个版本显然更清晰、更简单,因为除了产生 N 个平方的操作的实际逻辑之外,几乎没有什么可阅读的。

显然,您不想将所有状态放入您曾经编写的每个程序中的生成器中。但是拒绝使用生成器因为它们隐藏了状态转换就像拒绝使用 for循环,因为它隐藏了程序计数器跳转。这与协程完全相同。

关于python - 为什么回调是 "ugly"?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/18542484/

26 4 0
Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
广告合作:1813099741@qq.com 6ren.com