我有个用C#编写的程序,需要提供其中的一部分功能给使用Python的同事使用,于是研究了一下如何在Python中调用C#的方法,特别是如何注册C#中的回调,这里记录一下。
使用Python for Net类库
使用Pythonnet类库可以在Python中调用C#编写的dll,要使用pythonnet首先要安装,安装方法很简单,在管理员权限下在命令行中,使用pip即可安装(pip在安装python的时候会自带,如果没有需要额外单独安装):
pip install pythonnet
安装完成之后,使用pip list查看一下:
C:\WINDOWS\system32>pip list
Package Version
---------- -------
cffi 1.15.1
clr-loader 0.2.5
pip 23.0.1
pycparser 2.21
pythonnet 3.0.1
setuptools 41.2.0
表示pythonnet已经成功安装。
编写C#类库
为了演示,这里编写了一个简单的C#类库,类库名称为PythonCalled,生成的dll为“PythonCalled.dll”,里面有一个名为MyTest的类,内容如下:
using System;
namespace PythonCalled
{
public class MyTest
{
public delegate void LogDelegate(string s);
private event LogDelegate LogEvent;
public void SetLogger(LogDelegate n)
{
LogEvent = n;
}
public void Print(string msg)
{
LogEvent?.Invoke($"Print(string msg) was called,msg: {msg}");
}
public double Add(double x, double y)
{
double result = x + y;
LogEvent?.Invoke($"Add(double x, double y) was called, msg: {x}+{y}= {result}");
return result;
}
}
}
这里面我定义了一个public类型的委托LogDelegate,然后定义了一个private的LogEvent,并且提供了一个public的SetLogger方法,用来设置回调,因为在pythonnet里似乎可以识别delegate但不识别event,所以这里没有直接定义一个Event:
public EventHandler<string> LogEvent;
如果这样定义,在python里面,无法给LogEvent赋值。C#代码写完之后编译成dll。这里必须要注意的是选用的.NET Framework版本,是.NET Frameowrk还是.NET Core。我这里选择的是.NET Core 5.0。
编写Python调用C#代码
我这里编写Python用的是自带的Python IDLE,Python是3.8版本,新建一个.py文件,然后编写以下代码:
import os
#如果是.net core必须加上下面两条语句
from pythonnet import load
load("coreclr")
import clr
import sys
#将当前.py文件所在的目录加到系统目录中来
sys.path.append(os.getcwd())
# 读取加载和引用dll
clr.FindAssembly("PythonCalled.dll")
dll = clr.AddReference("PythonCalled")
# 打印dll内容
print(dll)
from PythonCalled import *
def log_callback(arg):
print(arg)
def print_hi(name):
# 在下面的代码行中使用断点来调试脚本, 按 Ctrl+F8 切换断点。
print(f'Hi, {name}')
# 实例化类
instance = MyTest()
logger = instance.LogDelegate(log_callback)
instance.SetLogger(logger);
# 有输入及无返回
instance.Print("welcome to python call c#")
# 有输入及输出
add_data = instance.Add(1, 1)
print(add_data)
# main函数入口。
if __name__ == '__main__':
print_hi('yy')
需要的是,如果C#的类库使用的是.NET Core,那么在开头的地方必须要加上以下语句:
from pythonnet import load
load("coreclr")
另外,最好把C#编译后的dll和.py文件放在同一目录下,这样能减少加载dll时的路径查找错误。下面这句代码就是把当前.py文件所在的路径加入到系统路径中:
#将当前.py文件所在的目录加到系统目录中来
sys.path.append(os.getcwd())
代码里面,需要注意的是回调函数的设置。第一步要实例化一个代理,然后将代理设置到C#代码里面去:
logger = instance.LogDelegate(log_callback)
instance.SetLogger(logger);
log_callback,就是python里面的函数,签名需要跟C#里面代理一致,为了简化,这里都用string来作为参数类型,而没有用C#里面的自定义类型,建议简单话处理,就用string。
def log_callback(arg):
print(arg)
回调方法很简单,就是将参数打印出来。
整个运行的结果如下:
可以看到,很完美的执行了方法及其回调。
如果上述代码运行出错,报"clr找不到”或者“FindAssembly”找不到的错误,那就是clr有冲突,因为python中有clr模块,pythonnet中也有clr,在import中需要使用的是pythonnet中的clr,所以只需要用pip uninstall clr把clr模块卸载掉,再次运行就可以了。
上面的例子已经验证过了在Python中调用C#的方法以及回调,所以接下来就可以应用到实际项目中了。
实际项目应用
现在有一个用C#编写的接收股票实时行情的程序,支持订阅。比如订阅某只股票的行情,该dll就可以通过事件回调返回股票的买卖十档MarketData和逐比成交TAS数据。首先新建了一个类QuoteManagerForPython.cs,里面清楚不必要的代码,专门供Python调用,基本的代码结构如下:
using BaseLib;
using DataFeed;
using Newtonsoft.Json;
using StockSubscription;
using System;
namespace TradeLib
{
public class QuoteManagerForPython
{
private static ClientSubscription subscriber;
public delegate void MarketDataUpdatedDelegate(string s);
public delegate void TASUpdatedDelegate(string s);
private static event MarketDataUpdatedDelegate MarketDataUpdated;
private static event TASUpdatedDelegate TASUpdated;
public static void SetMarketDataUpdateCallBack(MarketDataUpdatedDelegate m)
{
MarketDataUpdated = m;
}
public static void SetTASUpdatedCallBack(TASUpdatedDelegate t)
{
TASUpdated = t;
}
public static void Init(string addr)
{
if (!inited)
{
subscriber = new ClientSubscription(addr);
subscriber.SetLogFunc(Logger.LogInfo);
subscriber.MarketDataUpdated += MarketDataUpdated;
subscriber.TASUpdated += TASUpdated;
subscriber.Connect();
inited = true;
}
}
private static void MarketDataUpdated(MarketData md)
{
if (null != md)
{
FireMarketDataUpdated(md);
}
}
private static void TASUpdated(TAS tas)
{
if (null != tas)
{
FireTASUpdated(tas);
}
}
public static void SubscribeQuote(string code)
{
if (null != subscriber)
{
subscriber.SubscribeSymbol(code);
}
}
private static void FireMarketDataUpdated(MarketData md)
{
if (null != MarketDataUpdated)
{
try
{
MarketDataUpdated(JsonConvert.SerializeObject(md));
}
catch (Exception ex) { BaseLib.Logger.LogError(ex); }
}
}
private static void FireTASUpdated(TAS tas)
{
if (null != TASUpdated)
{
try
{
TASUpdated(JsonConvert.SerializeObject(tas));
}
catch (Exception ex) { BaseLib.Logger.LogError(ex); }
}
}
}
}
以上代码略去了很多细节,主要的逻辑为:首先设置两个回调事件,它们分别来接收十档行情和逐比行情。然后调用Init方法,传入行情订阅服务器的地址。接下来就是调用SubscribeQuote方法,订阅需要接收的股票代码,当有新的行情来到时,就会触发回调。所有的逻辑实际都是在ClientSubscription这个类里面来完成,但这里这些细节不重要。
接下来编写python代码来演示如何通过调用和注册C#中的方法和事件来接收行情。需要注意的是.py文件需要放到上面的dll所在的目录下,因为这个dll比较复杂,它引用了许多其它的dll,这些都在同一目录下,将.py放在该目录下能减少clr加载dll出现错误的可能。
在.py中的完整代码如下:
import os
from pythonnet import load
#load("coreclr")
import clr
import sys
import json
sys.path.append(os.getcwd())
#print(os.getcwd())
# 读取dll文件
clr.FindAssembly(‘此处替换为QuoteManagerForPython所在的dll’)
dll = clr.AddReference(‘此处替换为QuoteManagerForPython所在的dll,不要.dll后缀’)
from TradeLib import *
def newMarketData_callback(arg):
# j = json.loads(arg)
print(arg)
def newTasData_callback(arg):
print(arg)
def subscribe(symbol):
QuoteManagerForPython.SubscribeQuote(symbol)
def init():
#设置回调函数
marketFunc=QuoteManagerForPython.MarketDataUpdatedDelegate(newMarketData_callback)
tasFunc=QuoteManagerForPython.TASUpdatedDelegate(newTasData_callback)
QuoteManagerForPython.SetMarketDataUpdateCallBack(marketFunc)
QuoteManagerForPython.SetTASUpdatedCallBack(tasFunc)
# 连接和初始化 tcp://ipaddress : port
QuoteManagerForPython.Init('tcp://xxx.xxx.x.x:xxx')
print('init success')
if __name__ == '__main__':
init()
subscribe('300059.SZ')
os.system("pause")
代码非常简单,在python中定义了两个func:newMarketData_callback和newTasData_callback。在init方法中实例化了两个委托marketFunc和tasFunc,最后将这两个委托通过SetMarketDataUpateCallBack和SetTASUpdatedCallback两个方法设置到C#里面去。接下来调用Init方法,传入订阅服务器地址以初始化订阅。
最后通过subscribe方法,传入需要订阅的股票代码。在subscribe方法内部调用C#的SubscribeQuote方法来订阅行情。当有行情到来时,在两个回调func里就能直接处理行情数据了,运行结果如下:
可以看到,能够在python里定义的两个回调函数里面,接收到C#中的股票行情数据。
pythonnet调试
通常,在使用Python代码调用C#方法的时候,可能不会那么顺利,这就需要用到调试。这里也简单介绍一下使用Visual Studio附加进程调试pythonnet的方法。
首先第一步就是设置调试环境,在python代码中,设置断点,然后打开调试环境,在Python IDLE中,点击Run->Python Shell,然后在Python Shell中->Debug->Debugger,弹出Debug Control。
在Debug Control中,选中Source Global,然后点击Python IDLE中的Run->Run Module,运行程序,此时,第一个断点init方法会被命中,如下图:
现在在Debug Control中,点击Step,可以进入到init方法里,逐步运行。现在我们假设要调试init方法里面的QuoteManagerForPython.Init这个方法。现在我们回到Visual Studio中,附加Python的调试进程。在Visual Studio中,点击 调试->附加到进程,就会弹出附加进程界面,在可用进程里面选择pythonw.exe这个进程,注意,一定要选择类型为“托管”这个进行。
然后在Visual Studio中QuoteManagerForPython.cs类的Init方法中设置好断点。
接下来回到Python Debug Control,继续Step逐步运行:
可以看到,已经逐步运行到了Init这句代码,正在运行的代码行会以暗色条纹显示。再次点击Step执行这一句语句,可以看到Visual Studio中在Init方法处设置的断点已经命中:
现在就可以调试python往C#方法传递的参数以及方法执行的过程是否正确了。如果我们想继续调试subscribe方法,同理现在C#代码中设置断点,然后在Python Debug Control中我们继续执行Step或者直接点击Go,就会直接跳到python代码或者C#代码中的下一处断点。
调试外部调用C#程序的方法一般是采用附加线程的方法,比如Matlab调用C# dll,远程桌面调试C# dll等等。
参考: