C#のプログラムから機械学習などの処理をPythonで実装したい場合がある。
C#とPythonの連携方法について調べたところいくつか方法があったが、gRPCが良さそうだったので試してみた。
調べた方法
Python for .NET
C#とPythonを直接連携させるには、「Python for .NET」があるが、リリースされているバージョンは最新のPythonと.NETのバージョンに追従できていないようなので、自分でビルドが必要になる(.NET Coreで動かそうと試したがうまくいかずあきらめた)。
リダイレクト
プロセスをリダイレクトで連携するという方法もあるが、受け渡すデータの構造が複雑な場合、データのシリアライズとデシリアライズの処理の作りこみが必要になる。
また、C#から起動したPythonプログラムのデバッグが行いにくいという課題がある。
RESTサービス
RESTサービスにして連携する場合は、バイナリデータをテキストで渡すためオーバヘッドが大きくなり、呼び出し頻度によってはパフォーマンスが課題になる。
gRPC
gRPCを使用した場合、これらの課題をすべて解消できる。
gRPCについて
gRPCは、Googleが標準化を主導しているHTTP/2をベースにしたオープンソースのRPCライブラリだ。
様々な言語に対応しており、C#とPythonにも対応している。
インターフェースは、protobufで定義する。
プログラミング言語で使用できる標準的なスカラ型、配列や構造体など複雑なデータ構造が定義できる。
シリアライズとデシリアライズのコードは、使用するプログラミング言語向けに自動生成されるため、自分で作りこむ必要がない。
サンプルプログラム
C#とPythonの連携を行うサンプルプログラムを作成した。
GitHub - TadaoYamaoka/gRPCSample
公式でC#とPythonそれぞれのサンプルが提供されており、C#のクライアントとPythonのサーバを連携しただけなので、詳細は公式の説明を参照。
https://grpc.io/docs/quickstart/csharp/
https://grpc.io/docs/quickstart/python/
C#側(gRPCSample)
C#側の.NETのバージョンは、.NET Core 2.1を使用した。
Visual Studioで.NET Coreのコンソールアプリを作成して、NuGetで、「Grpc」(gRPCのAPI)と「Grpc.Toos」(protocを含む)と「Google.Protobuf」(protobufに必要なクラス)を参照に追加する。
Visual Studioを使用しないでCLIから追加するには以下の通り実行する。
>dotnet add package Grpc >dotnet add package Grpc.Tools >dotnet add package Google.Protobuf
作成した.protoファイルをVisual Studioで、右クリックしてプロパティを表示して、Build Actionを「protobuf compiler」に設定すると、プロジェクトをビルドすると自動で.protoからC#コードが生成される。
namespaceやクラスのプロパティなどの先頭は大文字になるため注意が必要。
クライアントの処理は、Program.csに実装している。
ほぼ公式のサンプル通りのため説明は割愛。
Python側(PyServerSample)
Python向けgrpcはpipからインストールする。
>pip install grpcio >pip install grpcio-tools
.protoからPythonコードの生成は、手動でコマンドを実行して生成した。
python -m grpc_tools.protoc -I../gRPCSample --python_out=. --grpc_python_out=. ../gRPCSample/Sample.proto
サーバの処理は、PyServerSample.pyに実装している。
ほぼ公式のサンプル通りのため説明を割愛。
max_workersで、スレッド数を指定できるが、PythonにはGILがあるため、並列化のパフォーマンスはあまり期待できそうにない。
並列化する場合は、マルチプロセス構成にして、負荷分散の仕組みを検討した方がよさそうだ(詳しくないがNginxでHTTP/2の負荷分散ができるようだ)。
2019/8/7 追記
.porotoから生成したクラスに、C#のオブジェクトから値を詰め込む際に、JavaでいうDozerのような機能が欲しくなる。
C#では、AutoMapperというライブラリがデファクトのようだ。
フィールド名が一致する場合は、自動でフィールドの値をコピーしてくれる。
ただし注意点がいくつかある。
名前規則
.protoで単語間をアンダーバーで区切っている場合、C#ではキャメルケースで区切る規則に変換される。
そのため、名前が一致せずコピーされなくなる。
その際は、以下のように設定すればよい。
Mapper.Initialize(cfg => { cfg.CreateMap<Class1, Class2>(); cfg.SourceMemberNamingConvention = new LowerUnderscoreNamingConvention(); cfg.DestinationMemberNamingConvention = new PascalCaseNamingConvention(); });
参考:c# - Automapper naming convention with underscore/PascalCase properties - Stack Overflow
repeated
.porotoで、repeatedとして定義したフィールドは、readonoyのコレクションとして生成される。
そのため、値がコピーされない。
AfterMapでカスタマイズする必要がある。
例:https://gist.github.com/TadaoYamaoka/7791fcfe274b9bb2fc5a4318e2f4dd15