我编写的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风格中创建这个(丑陋的)presudo-two-way inteaction?我需要一些continuation函数,但只在函数范围内生成函数,当我需要像异常一样的函数:从调用堆栈的底部到顶部,以及通过调用main.next将控制流返回到前一个屈服点的能力( )在顶级Python脚本中。
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。更舒适的解决方案是创建并行线程(或进程)而不是调用和捕获产量:这不需要将所有返回替换为产量。两个线程可以通过两个阻塞队列进行通信.Queue's:
如果request为None,则为完成运行时打破无限循环:子线程和主线程。
答案 0 :(得分:1)
如果您想在Python代码中使用yield
,请执行以下操作:
def parse_tick(self):
for i in range(2):
yield self._some_method(i, i)
调用Python生成器函数 - 无论是从Python还是从C-执行此操作都会为您提供迭代器。每次从迭代器获得next
值时,它会在最后yield
点之后重新激活生成器函数。
working with iterators from 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}然后就完成了。
并且1
不必记住它有一个暂停的parse_tick
,因为它暂停在_some_method
的中间;下次恢复时,它会自动恢复yield from
呼叫 - 或者,如果呼叫耗尽,则继续下一行代码。
PEP 380试图解释这个设计背后的基本原理,以及如何使用它,如果我没有说清楚的话。 Greg Ewing在使用_some_method
时也a great tutorial了。在SO回答中,我无法像他的例子那样解释,即使我的解释是他的一半。
或者,您可以在生成器上使用yield from
方法。如果仔细观察,send
和yield
是表达式,包含值,而不是语句。如果您通过调用yield from
来驱动生成器,则表达式的值仅为next
,这不是很有用。但是通过在生成器上调用None
方法,您不仅可以要求它恢复,还可以给它一个值来恢复。 (如果需要,你也可以调用send
将异常引入生成器。)这可以让你根据自上而下的协同程序编写你的控制流 - 我认为这不是你想要的,但请阅读PEP 342并查看。
您还可以构建throw
来构建自下而上的协同程序,我认为这可能是您想要的。如果你回到Greg Ewing的页面,最后一个例子展示了如何做到这一点。如果你将最后两个特征与一个简单的可组合“未来”抽象结合在一起......那么,看看3.4的新asyncio
模块,了解你可以做什么。