MENU

【Python×C#】pythonnet でDLL連携!実務で使えるパフォーマンス最適とは?

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

STEP
Python環境にpythonnetをインストール

pip コマンドで pythonnet をインストールします。

pip install pythonnet

STEP
C#のクラスを作成

Visual Studio .NET で「クラスライブラリ(.NET Framework)」のプロジェクトを作成します。
.NET Core 6/7/8/9とは相性が悪いので、.NET Frameworkのクラスを作成しましょう。

STEP
Pythonから呼び出したいクラスを含むDLLを作成

いつもと同じように、Pythonから呼び出したいクラスとメソッドをVisual Studio .NET で記述し、ビルドします。

たとえば、「CsharpLib」という名前でプロジェクトを作成した場合、ビルドに成功すると次のDLLが生成されます。

STEP
PythonからDLLを参照

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
STEP
Python の任意の場所からC#の関数をコール

通常の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 の listtuple は非ジェネリックな System.Collections.IListSystem.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 は大きな武器になります。
ぜひ、皆さんの現場でも一歩進んだパフォーマンス最適化の手段として活用してみてください。

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

この記事を書いた人

コメント

コメントする

目次