例外情况下通过屈服进行双向交互



我编写C++DLL,它是Meta Trader 4应用程序(MT4)和Python脚本(将Python嵌入MT4)之间的闸门。MT4向这个DLL发送请求,并等待执行指令(例如字符串命令数组)。DLL将MT4请求的解析传递给Python脚本。Python脚本需要从MT4中获得一些信息来解析每个请求。因此,MT4和Python具有双向通信。

但MT4不支持双向通信,它只能解析自己DLL请求的结果,并用新的参数对DLL进行新的调用。因此,我需要中断Python控制流,以便从Python向DLL(和MT4)临时返回部分结果,并等待来自MT4的新请求。

我怎样才能用Python风格创建这种(丑陋的)预设双向交互?我需要一些延续函数,但仅在函数范围内进行yield工作,当我需要类似yield的异常时:从调用堆栈的底部到顶部,以及通过调用顶部Python脚本中的main.next()将控制流返回到上一个yield点的能力。

MT4伪码:

new_args = ...
while (true) {
cmds = DLL_GetCommands(new_args);
if (! cmd) { // No commands from Python
break;
}
new_args = _parseCommand(cmds);
}

DLL伪代码:

char* __declspec(dllexport) DLL_GetCommands(char* args) {
// Python already initialized.
// Some python script already executed
// and I have local scope of this execution.
PyObject var = ...search some object variable in Python...
// var is instance of Advert class
return PyObject_CallMethodObjArgs(var, "parse_tick", args, NULL);
}

Python代码:

class Handler():
def some_cpp_request(self, a, b):
yield 'some_cpp_request'
# After second call to Advert.parse_tick() control flow should return here.
class Advert():
def __init__(self):
self.h = Handler()
# This method and all what it call should work as single generator
def parse_tick(self):
for i in range(2):
self._some_method(i, i)
def _some_method(self, a, b):
self.h.some_cpp_request(a, b)

[更新]在收到abarnert的建议后,我有了工作解决方案:

class Handler():
def __init__(self):
self.cmds = []
self.cmds_results = []
def some_cpp_request(self, a, b):
self.cmds.append(("SOME_CPP_REQUEST", a, b))
yield
# Here self.cmds_results contains MT4 response.
class Advert():
def __init__(self):
self.h = Handler()
def parse_tick(self):
for i in range(2):
yield from self._some_method(i, i)
return 'xxx'
def _some_method(self, a, b):
yield from self.h.some_cpp_request(a, b)
parser = Advert()
gen = parser.parse_tick()
# This loop should be written in DLL layer.
while True:
next(gen)
parser.h.cmds_results.clear()
for cmd in parser.h.cmds:
# Adding some results
parser.h.cmds_results.append((cmd, 'SOME RESULT'))
parser.h.cmds.clear()

p.S.更舒适的解决方案是创建并行线程(或进程),而不是调用和捕获收益:这不需要将所有返回替换为收益。两个线程可以通过两个阻塞队列进行通信。队列:

  • 子线程:
    1. 将请求放入请求队列
    2. 阻止等待响应队列中的新元素
  • 主螺纹:
    1. 无限阻塞等待请求队列中的新元素
    2. 将响应放入请求队列

如果请求为None,则中断无限循环以完成运行时:子线程和主线程。

如果您想在Python代码中使用yield,请执行以下操作:

def parse_tick(self):
for i in range(2):
yield self._some_method(i, i)

调用Python生成器函数——无论是从Python还是从C调用——都会给您一个迭代器。每次从迭代器获得next值时,它都会在最后一个yield点之后重新激活生成器函数。

使用C中的迭代器很容易。以下是一些伪代码(如现有的伪代码、跳过错误处理、引用计数等):

PyObject *iterator = PyObject_CallMethodObjArgs(var, "parse_tick", args, NULL);
PyObject *item = NULL;
while (item = PyIter_Next(iterator)) {
do_stuff_with(item);
}

例外情况如何?没问题。如果您在Python中迭代的生成器函数引发异常,next会将该异常引发给调用者。如果你在C中迭代它,PyIter_Next返回的NULL与它对正在完成的迭代器所做的相同,那么你如何区分它们呢?通过检查PyErr_Occurred()


或者,您可以将回调函数传递到Python代码中,然后您的Python代码使用每个值调用该回调,而不是使用每个值的yield。这是嵌入Python的应用程序传统上所做的。但是,如果你已经在考虑发电机,你不需要回到旧的方式。


首先,听起来您想重新调整异常的用途,以便raise自动处理异常,就像可以从中恢复的yield一样。这行不通。不是生成器函数的函数无法恢复,句号。即使是生成器函数的函数,也只能在yield之后恢复,而不能在raise(或return)之后恢复。这是没有办法的;它将从根本上改变raise(和return)的语义

因此,您将不得不将您想要驱动的较低级别的函数(如生成器)转换为实际的生成器。(或者将它们分解成更小的函数并按顺序调用它们,或者将它们转换成保持显式状态并在__call__上恢复的对象,或者其他比惯用方式更麻烦的事情。)


接下来,听起来好像您想要隐式嵌套生成器。Python生成器不是这样工作的。如果你调用一个生成器函数,你会得到一个迭代器。即使你自己也是一个生成器函数,它也不能代表你产生值。如果你想要嵌套,你必须明确它,比如:

def _some_method(self, a, b):
yield a
yield b
def parse_tick(self):
for i in range(2):
yield from self._some_method(i, i)

请注意,调用者(无论是C还是Python)不必知道parse_tick实际上是在委托其他生成器来完成它的工作,或者_some_method在某个地方被挂起。它只是向parse_tick请求下一个值,它所看到的只是它得到了0,然后是下一次0,然后是1,再是1,然后就完成了。

parse_tick也不必记住它有一个悬挂的_some_method,因为它悬挂在yield from的中间;下一次恢复时,它将自动恢复_some_method调用——或者,如果该调用已用完,则继续到下一行代码。

PEP380试图解释这个设计背后的原理,以及如何使用它,如果我还没有说清楚的话。Greg Ewing还有一个关于使用yield from的很棒的教程。即使我的解释能力只有他一半,我也无法在SO答案中解释他的例子


或者,您可以在生成器上使用send方法。如果仔细观察,yieldyield from是表达式,包含值,而不是语句。如果通过调用生成器上的next来驱动生成器,则表达式的值仅为None,这不是很有用。但是,通过在生成器上调用send方法,您不仅可以要求它恢复,还可以给它一个值来恢复。(如果需要的话,你也可以调用throw在生成器中引发一个异常。)这让你可以根据自上而下的协同程序来编写控制流——我认为这不是你想要的,但请阅读PEP342并查看。


您也可以在yield from上构建自下而上的协同程序,我认为这可能是您想要的。如果你回到Greg Ewing的页面,最后一个例子展示了如何做到这一点。如果你把最后两个功能和一个简单的可组合的"未来"抽象放在一起……那么,看看3.4的新asyncio模块,了解一下你能做什么

最新更新