Pythonはその手軽さと高機能さゆえに、あらゆるシーンで活躍する言語ですが、処理速度がボトルネックになることも少なくありません。
インタプリタ型であるPythonは、コードを逐次解釈しながら実行するため、特にループ処理や計算量の多い演算ではオーバーヘッドが大きく、動作が重くなりがちです。
こうした課題への対策として、「【Python】数十倍は速くなる?Cythonで高速化に挑戦!」という記事では、Cythonによる高速化手法を紹介しました。
ただし、C言語ライクな記述が求められることや、Pythonの標準機能やオブジェクトとの相性の問題もあり、導入には工夫が必要です。
そこで今回は、C#で作成したDLLをPythonから呼び出すことで処理を高速化する方法をご紹介します。
単なる高速化だけではなく、C#側の豊富な機能やリソースを活用できるという点でも、Pythonnetを用いたDLL連携は実務レベルで非常に有用です。
Pythonの限界を突破するアプローチにご興味がある方は、ぜひ最後までお読みください。
pythonnetとは
pythonnet(正式名:Python for .NET)は、.NET のランタイム(CLR)上で Python を動作させ、Python から直接 .NET/C# のアセンブリやクラス、メソッドを呼び出せるようにするライブラリです。
pythonnetのドキュメントは、Python.NET公式ページで公開されています。
- NET→Python 双方向呼び出し
Python から C# のクラス・メソッドをそのままインポート&実行
逆に C# から Python スクリプトをホストして呼び出すことも可能 - CLR 上でのネイティブ実行
CPython インタプリタを CLR に埋め込む方式ではなく、CLR から CPython を呼び出す仕組み
ガベージコレクションやメモリ管理は .NET 側と Python 側それぞれで協調 - ValueTuple やジェネリック型にも対応
System.Collections.Generic.List>
のような高度な型も扱える
NumPy や Pandas の配列を .NET 側に渡す工夫も可能
pythonnet導入のメリット
- Python の豊富な科学計算・機械学習ライブラリを併用できる
NumPy や Pandas のデータを .NET 側で処理し、その結果を Python で分析・可視化できる - 双方向呼び出しによる柔軟性の高さ
C#→Python、Python→C# の両方でメソッドやクラスを呼び出せ、テストやスクリプト実行に柔軟性が高い - 開発生産性の向上
言語間の境界を意識せずに実装できるため、プロトタイプから実運用までの開発サイクルが短縮される
pythonnet導入のデメリット
- 型変換のコストが大きい
Python のリストや辞書をList>
やDictionary
に変換する際、数百万件のデータでは秒単位のコストが発生する - ガベージコレクションの協調が複雑
Python 側と .NET 側で別々の GC が動くため、リソース管理やメモリリークに注意が必要 - デバッグ・例外ハンドリングの難易度
呼び出し先が CLR/CPython どちらかわかりにくく、トレースログやスタックトレースの解読が煩雑になる場合がある
pythonnet を使ったC# DLL との連携手順
pip install pythonnet
pip コマンドで pythonnet をインストールします。
pip install pythonnet
Python 上で、 clr
モジュールをimport し、clr.Addreference
メソッドに、生成されたDLLのパスを指定します。これで C#のDLLがPythonから参照できるようになるため、使いたいDLL(例: CSharpLib)から必要なクラスをインポート(例:DataProcessing)してください。
import clr # pythonnetのモジュール
# C# DLL をロード
clr.AddReference(
r"C:\Users\xxxxxx\source\repos\WpfApp1\CSharpLib\bin\Debug\CSharpLib.dll"
)
from CSharpLib import DataProcessing
通常のPythonクラスと同様に、呼び出したい場所でインスタンスを生成し、メソッドをコールします。ただし、PythonとC#間でPythonオブジェクト(リストや辞書など)を直接やり取りする際には、必ず型変換(データのマッピング)が必要となりますのでご注意ください(詳細については後述します)。
DataProcessing.TupleToDictNormal(data)
Pythonとpythonnet におけるデータ型のマッピング
Python から .NET/C# のコードを呼び出す pythonnet では、データ型のマッピングと呼び出しオーバーヘッドに留意が必要です。特にリストや辞書を渡す場合の手順と注意点をまとめました。
基本的な型マッピング
- Python の基本型(int, float, bool, str)は自動で対応する CLR 型(System.Int32, System.Double, System.Boolean, System.String)にマッピングされる
- Python の
list
やtuple
は非ジェネリックなSystem.Collections.IList
/System.Collections.IEnumerable
として扱われる - Python の
dict
は非ジェネリックなSystem.Collections.IDictionary
として扱われる - 複合的なジェネリック型(
List
,Dictionary
)をそのまま受け取る場合は明示的に .NET のコレクションを構築して渡す必要がある
非ジェネリック(型指定無し)のサンプル
//下記の様に型指定無しなら、Pythonの型をそのまま受け取れる
// Pythonの List を IEnumerable 型での受け取る
public void ProcessItems(IEnumerable items)
{ ~処理~ }
// Pythonの List を IList 型での受け取る
public void ProcessMap(IList items)
{ ~処理~ }
// Pythonの List を ICollection 型での受け取る
public void ProcessMap(ICollection items)
{ ~処理~ }
// Python の Dict を IDictionary 型での受け取る
public void ProcessMap(IDictionary map)
{ ~処理~ }
ジェネリック(型指定有り)のサンプル
//下記の様に型指摘する場合、型変換が必要
// Python側で list⇒List[str] への型変換が必要
public void ProcessItems(IEnumerable<string> items)
{ ~処理~ }
// Python側で list⇒List[str] への型変換が必要
public void ProcessMap(IList<string> items)
{ ~処理~ }
// Python側で list⇒List[str] への型変換が必要
public void ProcessMap(List<string> items)
{ ~処理~ }
// Python側で dict⇒IDictionary[str, int] への型変換が必要
public void ProcessMap(IDictionary<string, int> map)
{ ~処理~ }
// Python側で dict⇒Dictionary[str, int] への型変換が必要
public void ProcessMap(Dictionary<string, int> map)
{ ~処理~ }
Python list → C# List の受け渡し
Python で .NET 用の空の List
を生成し、Python の list
をループで要素を Add
してから、そのList
をメソッドに渡します。
import clr
from System.Collections.Generic import List
# 例: List[int] を作成して渡す
py_list = [1, 2, 3, 4, 5]
cs_list = List[int]() # System.Collections.Generic.List<int>
for x in py_list:
cs_list.Add(x)
result = DataProcessing.ProcessInts(cs_list)
Pythonで受け取る際は、通常のList
としてそのまま利用できます。
# C# メソッドが List<int> を返すと仮定
cs_list = DataProcessing.GetIntList()
# Python からは通常のシーケンスとして扱える
for x in cs_list:
print(x)
Python dict → C# Dictionary の渡し方
Python の dict.items()
を反復して Add(key, value)
を呼び出してから、C#メソッドに渡します。
from System.Collections.Generic import Dictionary
py_dict = {"a": 1, "b": 2}
cs_dict = Dictionary[str, int]()
for k, v in py_dict.items():
cs_dict.Add(k, v)
result = DataProcessing.ProcessDict(cs_dict)
辞書についても、そのまま受け取って利用できます。
# C# メソッドが Dictionary<string,int> を返すと仮定
cs_dict = DataProcessing.GetDict()
# Python の dict と同じようにキー/値参照できる
print(cs_dict["key42"])
print("key99" in cs_dict)
呼び出しオーバーヘッドとパフォーマンス
- 呼び出し回数
小さい関数を何度も呼ぶより、まとめて一度に処理できるバルクAPIの方が効率的 - GC の協調
.NET のガベージコレクタと CPython の GC が別々に動作し、メモリリークやリソース解放タイミングに注意が必要
例外ハンドリングとデバッグ
スタックトレースは CLR サイドの情報が混ざるため、両言語のトレースを併記してログ出力すると原因追跡しやすくなります。
try:
DataProcessing.TupleToDictNormal(cs_data)
except Exception as e:
print("C# 側例外:", e)
ベストプラクティス
適切なデータ構造の設計と渡し方を工夫することで、pythonnet を介した Python×C# ハイブリッド開発でも高いパフォーマンスを維持できます。
- IDictionary/IEnumerable を受ける設計
非ジェネリックなインタフェースで受け取ると Python 側の変換が不要 - 型安全なラッパー関数
Python 側に変換ユーティリティをまとめて、呼び出し元コードを簡潔に保つ - 呼び出し回数を最小化
Python ↔ CLR の境界越えを減らし、オーバーヘッドを抑制
Python/Cython/pythonnetの速度比較
タプルから辞書への変換処理と素因数分解関数の処理速度を、Python、Cython、およびpythonnetを用いて計測しました。
タプルから辞書への変換処理においては、pythonnetが最も高速であり、Pythonの約4倍、Cythonの約2.4倍の速度を記録しました。しかしながら、この変換処理では、pythonnetがPythonのオブジェクト(リストや辞書など)を直接受け取れないため型変換が必要となり、そのオーバーヘッドとして17.67秒を要しました。
一方、素因数分解においては、pythonnetとPythonはほぼ同程度の処理速度を示し、Cythonと比較すると約40倍も遅い結果となりました。
以上の結果から、C#のDLLをPythonから呼び出す際に型変換が必要な場合、その変換にかかるオーバーヘッドを相殺できる程度の重い処理でなければ、かえってパフォーマンスが低下する可能性があることが示唆されます。また、単純なループ計算のような処理ではC#のメリットをほとんど享受できないため、より複雑な計算や特定のライブラリの利用など、それ以外の部分での高速化や利便性を狙う必要があると考えられます。
検証内容 | Python関数 | Cython関数 | pythonnet関数 |
---|---|---|---|
タプル⇒辞書変換 | 0.269765 秒 | 0.161243 秒 | 0.066780 秒 |
素因数分解 | 0.000141 秒 | 0.000003 秒 | 0.000121 秒 |
import time
import cython_demo # Cythonでビルドされたモジュール
import clr # pythonnet
from System import ValueTuple
from System.Collections.Generic import List
# C# DLL をロード
clr.AddReference(
r"C:\Users\a-maeda\source\repos\WpfApp1\CSharpLib\bin\Debug\CSharpLib.dll"
)
from CSharpLib import DataProcessing
# 汎用ベンチマーク関数
def benchmark(label, func, *args, **kwargs):
start = time.perf_counter()
result = func(*args, **kwargs)
end = time.perf_counter()
print(f"{label:<20} time: {end - start:.6f} seconds")
return result
#====================================================================================
# タプル⇒辞書変換の速度比較
#====================================================================================
# Python側のダミーデータ
py_data = [(f"key{i}", i) for i in range(1, 1_000_000)]
# C# 用 List<ValueTuple<string,int>> に変換
start = time.perf_counter()
cs_data = List[ValueTuple[str, int]]()
for k, v in py_data:
cs_data.Add(ValueTuple[str, int](k, v))
print(f"データ変換 time: {time.perf_counter() - start:.6f} seconds")
# Python 評価用
def tuple_to_dict(data):
result = {}
for k, v in data:
result[k] = v
return result
print("★★★ タプル⇒辞書変換の速度比較 ★★★")
benchmark("Python関数 ", tuple_to_dict, py_data)
benchmark("Cython関数 ", cython_demo.tuple_to_dict_normal, py_data)
benchmark("Pythonnet関数 ", DataProcessing.TupleToDictNormal, cs_data)
#====================================================================================
# 素因数分解の速度比較
#====================================================================================
def prime_factor_count(n):
count = 0
d = 2
while n > 1:
while n % d == 0:
n //= d
count += 1
d += 1
return count
n = 1_000_000_000
print("\n★★★ 素因数分解の速度比較 ★★★")
benchmark("Python関数 ", prime_factor_count, n)
benchmark("Cython関数 ", cython_demo.prime_factor_count_normal, n)
benchmark("Pythonnet関数 ", DataProcessing.PrimeFactorCountNormal, n)
データ変換 time: 17.667990 seconds
★★★ タプル⇒辞書変換の速度比較 ★★★
Python関数 time: 0.269765 seconds
Cython関数 time: 0.161243 seconds
Pythonnet関数 time: 0.066780 seconds
★★★ 素因数分解の速度比較 ★★★
Python関数 time: 0.000141 seconds
Cython関数 time: 0.000003 seconds
Pythonnet関数 time: 0.000121 seconds
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace CSharpLib
{
public static class DataProcessing
{
// タプル→辞書変換
public static Dictionary<string, int> TupleToDictNormal(List<(string key, int value)> data)
{
var result = new Dictionary<string, int>();
foreach (var pair in data)
{
result[pair.key] = pair.value;
}
return result;
}
// 素因数分解
public static int PrimeFactorCountNormal(int n)
{
int count = 0;
int d = 2;
while (n > 1)
{
while (n % d == 0)
{
n /= d;
count++;
}
d++;
}
return count;
}
}
}
まとめ
Python は開発スピードとライブラリ資産が魅力的ですが、処理速度に課題がある場面も少なくありません。一方、C# は高速な処理や厳格な型チェック、豊富なUIや業務系ライブラリを持ち、堅牢なロジック実装に強みがあります。
本記事では、pythonnet
を活用することで、Python から C# の DLL を直接呼び出す方法を解説し、両言語の長所を組み合わせる実務的なテクニックをご紹介しました。単なる速度の向上だけでなく、処理の役割分担・保守性・再利用性の向上にもつながるアプローチです。
ただし、データ型変換のオーバーヘッドやメモリ管理の違いには十分な注意が必要です。導入前には実際の処理内容に応じた検証を行い、C# に任せる処理が本当にボトルネック解消に貢献するのかを見極めましょう。
Python の「書きやすさ」と C# の「動かしやすさ」をハイブリッドに活かしたい方にとって、pythonnet は大きな武器になります。
ぜひ、皆さんの現場でも一歩進んだパフォーマンス最適化の手段として活用してみてください。
コメント