没想到距离上次更新已经十几年了,今天心血来潮更新一下PythonNet的使用感受吧
前因:
因为项目需要,得实现C#调用Python脚本,上网逛了一圈发现都在推荐PythonNet,遂决定用这个。
基本需求:
1、Python脚本需要批量按次运行,各脚本间没有业务关联
2、每个脚本的库路径可能不一样
3、需要C#提供一些对象供Python使用,各脚本间使用的对象可能是同一个C#静态对象
最大的坑:
PythonNet实际上在Shutdown中并没有做Finalize。
1、这就导致Py_SetPath无法生效(就是PythonNet的PythonEngine.PythonPath属性赋值),调用后虽然是新的路径,但是仍然按照上一个脚本的路径来找,那自然找不到
2、因为没有真正的Finalize,所以PythonNet在Shutdown的时候会把当前的运行时数据全都序列化,然后在下次PythonEngine.Initialize()反序列化还原这些运行时数据,然后这就引出了一个很莫名其妙的问题。我导入C#对象后,在第二次执行脚本反序列化时提示“已添加了具有相同键的项”。翻了一天PythonNet的代码,根据自己浅薄的理解,简单粗暴的解决了一下上面的两个问题:
既然没有做Finalize,那就手动给来一次,让所有数据都刷新
在Runtime.cs的Initialize里面可以看到有interpreterAlreadyInitialized来判断是否已经做过Initialize,修改方法就是在下面增加Finalize,然后重新来一遍上面的流程
if (!interpreterAlreadyInitialized)
{
Py_InitializeEx(initSigs ? 1 : 0);
NewRun();
if (PyEval_ThreadsInitialized() == 0)
{
PyEval_InitThreads();
}
RuntimeState.Save();
}
else
{
if (!HostedInPython)
{
PyGILState_Ensure();
}
BorrowedReference pyRun = PySys_GetObject(RunSysPropName);
if (pyRun != null)
{
run = checked((int)PyLong_AsSignedSize_t(pyRun));
}
else
{
NewRun();
}
Py_Finalize();
Py_InitializeEx(initSigs ? 1 : 0);
NewRun();
if (PyEval_ThreadsInitialized() == 0)
{
PyEval_InitThreads();
}
RuntimeState.Save();
}
然后这样还不够,这样会导致在后面Shutdown时抛“Python object finalization failed”,原因是当前运行时的run值与实际要Dispose的对象run值不一致,具体为啥没细看,直接把三个异常Handler干掉,位置在Finalizer.cs的DisposeAll(),一共三个HandleFinalizationException
至此,完成项目需求。
PS:这种改法感觉并不安全,但没有那么多闲工夫去了解PythonNet库的底层机制,所以就凑合着这么弄了,有可能会有内存泄露之类的风险,待后续压力测试出问题再说。
PPS:我懒得换成英语去GitHub上面给原作者提Issue,而且我也懒得Fork工程,就这么地吧。