.NET (C#)からpythonを呼び出す
pythonnetを使って、.NETからpython(cpython)コードを呼び出す方法。
例えば、「Chainerで作ったAI処理を、.NETでつくったGUIから叩きたい!」とかいうときに有用。
python -> .NET(C#)の資料はたくさんあるのに、.NET -> pythonの資料がほとんどない1ので作成。
前提
この記事は以下の知識があることを前提に作成した。
- C#がある程度わかること。
- pythonがある程度(pip等の環境管理の仕組みも含め)わかること。
手順
作業目標は、「Windows(x64)環境で、VS2017を使い、C#からpython3.7(x64)のコードを呼び出すサンプルを作る」とする。
なお、pythonnetは、本記事執筆時点(2019/07)で、python 3.7まで対応している。
pythonnetの設定
pythonnetをビルドするための設定を行う。
※nugetにビルド済のパッケージがあるが、あまり更新されていないようなので使用しない。
pythonnetのgithubリポジトリから最新コードをcloneし、pythonnet.slnを開く。
VS2015の場合はpythonnet.15.slnを使う。vs2019以降を使う場合は適当に変換してがんばる(未確認)。
ソリューションのビルド構成がいろいろあるので、適切なものを選ぶ。
構成名は、(Debug|Release)(Mono|Win)(|PY3)
という構造になっている。
※ただし、この設定を廃止し、実行時に動的に選択可能にする改善が検討されているようなので、このあたりの手順は、そのうち大きく変わるかも。
-
Debug|Release
おなじみの、Debug/Releaseビルドの選択。 -
Mono|Win
Mono(Linux)用か通常のWindows用か。 -
|PY3
なにも付かないほうはpython2系向け、PY3はpython3系向けにビルド。
今回はWindows環境でpython3.7を呼び出すのが目的なので、ReleaseWinPY3
を選ぶ。
なお、プラットフォーム(x86/x64)はどちらでもよい(今回ビルドしたいPython.Runtime
プロジェクトはAnyCPU設定になっているため)。
選んだら、プロジェクトPython.Runtime
のプロジェクト プロパティを開き、コンパイルシンボルを確認する。
PYTHON3;PYTHON37;UCS2
のような文字列が定義されているので、狙ったpythonバージョンになっているか確認する。
今回はpython3.7を動かすのでPYTHON37でよいが、python3.5にしたい場合はPYTHON35のように書き換える。
有効な設定値は、src/runtime/runtime.cs
の#if節を参照するとわかる。
pythonnetのビルド
設定を済ませたプロジェクトをビルドする。
src/runtime/bin
以下にPython.Runtime.dll
が出力されるので、回収しておく。
同時に、Python.Runtime.pdb
(デバッグ情報)とPython.Runtime.xml
(ドキュメントコメントの情報)も回収しておくと、あとで少し幸せになれるかも。
呼び出し元プロジェクトの作成
pythonコードの呼び出し元となるプロジェクト / ソリューションを作る。
サンプルでは、.NET Framework 4.6.1向けのC#コンソール アプリとした。
※pythonnetは.NET Framework 4をターゲットに作られているので、それを呼び出せる設定にすること。
プラットフォームの変更
64bit用のPythonのDLLを呼び出すには、呼び出し元のexeも64bit向けにビルドされている必要があるため、その設定を行う。
作成したソリューションの構成マネージャーを開く。
プロジェクトのプラットフォームがAnyCPUとなっているので、x64を新規作成する。
※このとき、"新しいソリューション プラットフォームを作成する"にチェックを入れておくこと。
作成したら、プロジェクト プラットフォームとソリューション プラットフォームの両方から、AnyCPU設定を削除しておく。
※残しておくと、うっかりAnyCPU設定を使ってしまってドハマリする場合があるため。
参照設定
pythonnetのビルド時に回収しておいたDLLを、作成したプロジェクトのディレクトリにコピーする。
プロジェクトの参照設定から、コピーしたDLLに参照を設定する。
実装
以下を実装する。
using Python.Runtime;
using System;
using System.IO;
using System.Linq;
namespace PythonTest
{
class Program
{
/// <summary>
/// プロセスの環境変数PATHに、指定されたディレクトリを追加する(パスを通す)。
/// </summary>
/// <param name="paths">PATHに追加するディレクトリ。</param>
public static void AddEnvPath(params string[] paths)
{
var envPaths = Environment.GetEnvironmentVariable("PATH").Split(Path.PathSeparator).ToList();
foreach (var path in paths)
{
if (path.Length > 0 && !envPaths.Contains(path))
{
envPaths.Insert(0, path);
}
}
Environment.SetEnvironmentVariable("PATH", string.Join(Path.PathSeparator.ToString(), envPaths), EnvironmentVariableTarget.Process);
}
/// <summary>
/// プログラムのエントリポイント。
/// </summary>
/// <param name="args">コマンドライン引数。</param>
static void Main(string[] args)
{
// *-------------------------------------------------------*
// * python環境の設定
// *-------------------------------------------------------*
// python環境にパスを通す
// TODO: 環境に合わせてパスを直すこと
var PYTHON_HOME = Environment.ExpandEnvironmentVariables(@"%userprofile%\Anaconda3\envs\pythonnet_test");
// pythonnetが、python本体のDLLおよび依存DLLを見つけられるようにする
AddEnvPath(
PYTHON_HOME,
Path.Combine(PYTHON_HOME, @"Library\bin")
);
// python環境に、PYTHON_HOME(標準pythonライブラリの場所)を設定
PythonEngine.PythonHome = PYTHON_HOME;
// python環境に、PYTHON_PATH(モジュールファイルのデフォルトの検索パス)を設定
PythonEngine.PythonPath = string.Join(
Path.PathSeparator.ToString(),
new string[] {
PythonEngine.PythonPath,// 元の設定を残す
Path.Combine(PYTHON_HOME, @"Lib\site-packages"), //pipで入れたパッケージはここに入る
Path.Combine(@"C:\foo\bar\my_packages"), //自分で作った(動かしたい)pythonプログラムの置き場所も追加
}
);
// 初期化 (明示的に呼ばなくても内部で自動実行されるようだが、一応呼ぶ)
PythonEngine.Initialize();
// *-------------------------------------------------------*
// * pythonコードの実行
// *-------------------------------------------------------*
// Global Interpreter Lockを取得
using (Py.GIL())
{
// モジュールの探索パスを表示 (pythonコードを直接指定して実行)
PythonEngine.RunSimpleString(@"
import sys
import pprint
print('module path =')
pprint.pprint(sys.path)
");
// numpyのオブジェクトを取得し、呼び出してみる
dynamic np = Py.Import("numpy");
Console.WriteLine($"np.cos(np.pi * 2) = {np.cos(np.pi * 2)}");
// 自作コードも叩ける
dynamic myMath = Py.Import("my_awesome_lib.my_math"); // "from my_awesome_lib import my_math"
dynamic calculator = myMath.Calculator(5, 7); // クラスのインスタンスを生成
Console.WriteLine($"5 + 7 = {calculator.add()}"); // クラスのメソッド呼び出し
Console.WriteLine($"sum(1,2,3,4,5) = {myMath.Calculator.sum(new[] { 1, 2, 3, 4, 5 })}"); //staticメソッドも当然呼べる
dynamic dict = myMath.GetDict(); // 辞書型を返す関数呼び出し
Console.WriteLine(dict[3]); // 辞書からキーを指定して読み取り
}
// python環境を破棄
PythonEngine.Shutdown();
Console.WriteLine("Press any key to continue...");
Console.ReadKey();
}
}
}
ただし、このままではまだ動かない。
python環境を作る
python環境を導入する。
今回は、Anacondaで、"pythonnet_test"という名前のpython3.7環境(64bit)を作り、利用する。
よって、python環境のルートディレクトリは%userprofile%\anaconda\envs\pythonnet_test
となる(環境によって異なる可能性があるので適宜調整のこと)。
また、C#からpythonの追加パッケージが利用できることを確認するため、numpyを追加インストールした。
python環境を別の方法で導入する場合は、ルートディレクトリのパスを確認し、numpyもインストールしておくこと。
自前パッケージを配置
自作pythonコードが呼び出せることを確認するため、呼び出し先となるpythonプログラムを準備する。
以下コードを、<任意のディレクトリ>\my_awesome_lib\my_math.pyに配置する。
※配置したディレクトリは後で使うので覚えておくこと。サンプルではC:\foo\bar\my_packages\my_awesome_lib\my_math.py
に置いた。
# よくありがちなしょうもないテストコード
class Calculator:
def __init__(self, x, y):
self.x = x
self.y = y
def add(self):
return self.x + self.y
@staticmethod
def sum(num_list):
return sum(num_list)
def GetDict():
return {
0 : "this",
1 : "is",
2 : "my",
3 : "dictionary",
}
python環境にパスを通す
作成したpython環境にパスを通す。これが一番難しい(というか情報がない)。
ソースコード中、"python環境の設定"というコメント以降がこの設定に相当するので、コードを修正する。
pythonnetがpython本体のDLLを見つけられるようにする
pythonnetがpython本体のDLLを見つけられるよう、環境変数(PATH)にディレクトリを追加する。
サンプルでは、ユーティリティ関数AddEnvPath()
によって、以下を設定している:
-
python環境のルート
pythonXX.dll(python3.7ならpython37.dll)のあるディレクトリ。
-
python環境のルート\Library\bin
このディレクトリに、pythonXX.dllが依存しているDLLの一部が格納されていた。
python環境の作り方によっては必要ないか、別のディレクトリを指定する必要があるかも。
パスの通し方には複数の方法があるが、この例と同様、プログラム中(コード)で設定することをお勧めする。
-
プログラム中(コード)で設定
きちんと設定されていることを保証できる。おすすめ。
ただし、サンプルではパスをハードコードしているが、普通は設定ファイルとかから読むようにすべき。 -
Windowsのシステム環境変数に設定
システム全体に効くため楽だが、複数のpython環境が運用できなくなる。
-
exe起動前に(bat等で)事前設定する
デバッグ実行などがやりにくい。
python本体が、pythonのモジュール(標準モジュールを含む)を見つけられるようにする
python本体に、pythonのモジュールを探索するパスを設定する。
これが正しく設定されていないと、pythonの標準ライブラリやpip等で入れたライブラリ、自作コードを見つけることができない。
設定すべき項目は2種類ある。
-
PYTHON_HOME
python環境のルート。
-
PYTHON_PATH
pythonモジュールの探索場所。セミコロンで区切って指定する。
PYTHON_PATHには、デフォルトで以下が設定されている:
- %PYTHON_HOME%\pythonXX.zip
- %PYTHON_HOME%\DLLs
- %PYTHON_HOME%\lib
- カレントディレクトリ
サンプルでは、これらに加え、以下を設定している。
- pipのインストール先ディレクトリ (Lib\site-packages)
- 自作コードの存在するディレクトリ (~\my_packages、実際に置いた場所に合わせて適宜修正すること)
必要なら、さらに追加することもできる。
実行
ここまで設定すると、サンプルが動くようになる(はず)。
ビルドして実行してみて、以下のように出力されれば成功。
module path =
['C:\\~~~',
...
'C:\\Windows\\Microsoft.NET\\Framework64\\v4.0.30319\\']
np.cos(np.pi * 2) = 1.0
5 + 7 = 12
sum(1,2,3,4,5) = 15
dictionary
Press any key to continue...
Pythonの関数やクラスが呼び出せ、値をやり取りできていることが分かる。
ハマりどころ(基本編)
よくあるトラブル。
Python.Runtime.PythonException: ModuleNotFoundError : No module named '~~'
が起きる
pythonモジュールを見つけられていない。おそらく、PythonPathの設定をミスしている。
見つからないと怒られたモジュールの実体(ファイル)がどこにあるか調べ、PythonPathが適切に設定されているか確認する。
BadImageFormatException
が起きる
DLL関連の問題は、だいたい何でもこの例外になる。
原因が多岐にわたるので調査が難しいが、以下のどれかが原因のことが多い。
-
pythonのDLLがあるディレクトリにパスが通っていない
-
pythonnetのビルド時に、適切なpythonバージョンを指定しなかった
python3.5の環境で動かしたいのにpython3.7向けにビルドしてしまった、など。
pythonnetが探しに行くDLL名はビルド時に決まるので(python3.7向けにビルドした場合はpython37.dllを探しに行く)、ビルド時の環境と実環境が不一致だとDLLの読み込みに失敗する。 -
pythonのDLLが依存しているDLLにパスが通っていない(見つからない)
DLLが見つかっても、そのDLLが依存しているDLLが見つからない場合は、やはり読み込みエラーとなる。 -
呼び出し元プロジェクト・python本体のプラットフォーム(x86/x64)が一致してない
特に、 呼び出し元のプロジェクトをAnyCPU設定にするのは混乱の元になるので避けたほうがよい(もともとの挙動がややこしい上、.NET 4.5以前と以後での挙動が異なる)。
※pythonnet本体は、AnyCPUでビルドされるため、呼び出し元プロジェクトの設定に従う。
この手の問題が発生してしまった場合は、以下のような調査手段で何とかする。
-
DependenciesでDLLの依存関係を追っかける
-
dumpbinコマンドで各DLLのプラットフォーム(x86/64)を確認する
e.g.dumpbin /headers hoge.dll
-
Process Monitorでロードに失敗したDLLを特定する
IRP_MJ_CREATEが何度も失敗しており(DLLの探索先パスを全て探すため)、最後までLoad Imageが実行されていない(= DLLが見つからなかった)DLLが犯人。
また、Load Imageした結果がSUCCESSでないDLLも怪しい。
フィルタ設定を以下にするとわかりやすい。- Process:python呼び出し元のexe
- Operation:
IRP_MJ_CREATE
(ファイルのオープン)とLoad Image
(DLLの読み込み)をInclude
なお、
IRP_MJ_CREATE
はFiler -> Enable advanced outputを有効にしていないと表示されない。
ハマりどころ(応用編)
ちょっと難しいことをやろうとするとハマること。
複数スレッドからの実行
C#で、複数のスレッド(pythonnetを初期化した以外のスレッド)からPythonコードを呼び出す場合には、注意が必要。
Python(CPython)は、設計上、GILを採用している(GIL自体の説明は本題とずれるので割愛)。
また、明示的に開放処理を行わない限り、pythonnetはメインスレッド(= pythonnetを初期化したスレッド)でGILを保持したままにする。
よって、C#の別スレッドからPythonのコードを実行しようとした場合、GILの取得(using(Py.GIL())でデッドロックする。
これを回避するには、pythonnetの初期化後(PythonEngine.Initialize()の呼び出し後)に、PythonEngine.BeginAllowThreads()を呼び出し、明示的にGILを解放する。
これにより、他スレッドからもGILが取得できるようになり、C#の複数スレッドからPythonのコードが呼び出せるようになる。
ただし、GILで排他されることには変わりがないことには注意が必要。
Python側コードの並列実行はされず、単に複数スレッドから呼び出せるようになるだけ。
なお、pythonnetのBeginAllowThreads()メソッドが何をやっているのかは、PythonのC APIのドキュメントを読むとイメージがつかめる。
巨大データの扱い
C#からPythonに巨大な配列やリストを単純に渡すと、要素全てが個別にpythonのオブジェクトに変換されるため、とてつもなく遅くなる。
※ためしに4k解像度の画像データ(uint8の画素値)が入った配列を渡したら、なにもしないpython関数を呼び出すだけでも、秒単位の時間がかかった…
こういう場合には、ポインタ経由での引き渡しが有効。
たとえば8bit グレースケールの画像を受け渡す場合、以下のようにする。
import numpy as np
import ctypes.util
from ctypes import *
def my_func(img_ptr, width, stride, height):
# ポインタからnumpyのarrayを作る
img = np.ctypeslib.as_array((stride * height * ctypes.c_uint8).from_address(img_ptr)).reshape(1, height, stride)
# imgに対していろいろ処理する…
C#からは、以下のように呼び出す。
// この画像を引き渡したい
var hugeImage = new Bitmap(65536, 65536, PixelFormat.Format8bppIndexed);
BitmapData bitmapData = null;
try
{
// 画像をロック
bitmapData = hugeImage.LockBits(
new Rectangle(new Point(0, 0), image.Size),
ImageLockMode.ReadOnly,
PixelFormat.Format8bppIndexed
);
using (Py.GIL())
{
// pythonモジュールを読み込む。ファイル名等は適宜変更すること
dynamic myPython = Py.Import("myPython");
myPython.my_func(
bitmapData.Scan0.ToInt64(), // 画像バッファの先頭ポインタを引き渡す
bitmapData.Width, //当然ながら画像サイズとストライドも必要
bitmapData.Stride,
bitmapData.Height
);
}
}
finally
{
// ロックした画像を解放
if (bitmapData != null) hugeImage.UnlockBits(bitmapData);
}
画像ではなく配列やリストを渡したい場合は、Marshalクラスを使ってIntPtrにさえ変換してしまえば、同じように渡せる。
unsafeコードを使えるなら、fixedを使っても、もちろんよい。
pythonnetを使わない方法
pythonnetを使わない方法としては、以下のような方法での呼び出し方が考えられる。
python側をコマンドラインプログラムとして設計し、C#側から起動する
データのやりとりは、標準入出力やファイルを経由して行う。
pros:
- シンプルでわかりやすい。
- pythonランタイム環境と疎結合にできる。
cons:
- 起動終了などのオーバーヘッドが大きい。
- 細かい制御がしにくい。
python側をサーバとして設計し、C#側からサーバに対して要求を投げる
python側をサービスにしてしまう。
WebAPIサーバにしてもよいし、Apache ThriftやgRPCのようなRPCフレームワークを使ってもよい。
pros:
- 設計自由度が高い。
- オーバーヘッドが比較的小さい(ただしプロセス間通信やシリアライゼーションは発生するので、同一プロセスで処理するよりはやや遅いはず)。
- pythonランタイム環境と疎結合にできる。
cons:
- プロセスが2個になり、起動終了管理などが必要になる。
- RPC部分を別途設計・実装・メンテしないといけない。
Ironpythonを使う
cpythonと決別する。
「既存のpythonコードをC#から叩きたい」という用途には、おそらく茨の道。
pros:
- Ironpython自体が.NET上で動くpython処理系なので、.NETとの相性がいい。
cons:
- cpythonとの互換性に難がある(例えば、*.pydを使うライブラリはほぼ使えないと思ったほうが良い)。
- python2.x系しかサポートしていない(一応、3.xをサポートするIronpython 3も開発中らしいが、コミットログを見る限りほとんど停滞している)。
-
※動かなかった…という記事はあったのでそっとリンク。 ↩