Введение
На сегодняшний день Python является одним из самых популярных языков программирования, но даже это не помогает ему покрыть все потребности программистов. Самый очевидный минус чистого CPython - это его скорость, поэтому некоторые программисты выбирают для своих задач другие языки программирования, а кто-то просто реализует узкие места на C/C++ и подключает их к Python.
Однако бывают случаи, когда есть некая база кода, написанного на C#, а возможности быстро переписать всё на Python/C/C++ нет. Тогда встает вопрос “как подключить C# к Python?”. Для этого была разработана библиотека pythonnet. В этой статье разберем: как запустить C# код из Python и что из этого может получиться.
Реализация
Для сравнения скорости выполнения C# и Python я буду ссылаться на одну из прошлых статей.
Библиотека pythonnet работает с .dll файлами, поэтому весь код необходимо будет преобразовывать в динамически подключаемые библиотеки. Чтобы создать .dll файл из C# необходимо установить visual studio и при создании проекта указать, что проект будет создан для библиотеки классов (я дал название проекту: “MyTestCS”, в будущем dll файл будет носить такое же название как и проект):
В качестве примера будем использовать магазин из прошлой статьи, который оптимизировали силами самого питона и других языков. Создадим структуру для одного товара на C#:
public struct DataGoods
{
public string name;
public int price;
public string unit;
public DataGoods(string name, int price, string unit)
{
this.name = name;
this.price = price;
this.unit = unit;
}
}
Теперь реализуем класс самого магазина. В нем создадим метод для заполнения магазина товарами:
public class ShopClass
{
public string name;
public List<DataGoods> listGoods;
public ShopClass(string name)
{
this.name = name;
this.listGoods = new List<DataGoods>();
}
/// <summary>
/// Метод для создания товаров в магазине
/// </summary>
/// <param name="numberGoods"> Количество объектов в магазине </param>
public void createShopClass(int numberGoods) {
List<DataGoods> lGoods = new List<DataGoods>();
for (int i = 0; i < numberGoods; i++) {
lGoods.Add(new DataGoods("телефон", 20000, "RUB"));
lGoods.Add(new DataGoods("телевизор", 45000, "RUB"));
lGoods.Add(new DataGoods("тостер", 2000, "RUB"));
}
this.listGoods = lGoods;
}
}
После того, как класс был создан, приступим к подключению C# кода к Python проекту. Сначала создадим .dll файл из C# проекта (достаточно нажать команду ctrl+shift+B). В папке bin->debug->netstandart2.0 проекта (путь зависит от того, какие конфигурации среды стоят у вас) появится файл с названием проекта и расширением .dll (именно этот файл будет подключаться к программе на Python).
Далее разберемся с проектом на Python. Необходимо установить библиотеку pythonnet, выполнив команду:
pip install pythonnet
В проекте создадим файл main.py, а также поместим библиотеку “MyTestCS.dll” в папку с проектом:
Теперь можно подключать библиотеку в main.py, для этого сначала импортируем clr (clr позволяет рассматривать пространства имен CLR как пакеты Python):
import clr
Укажем путь до нашего .dll файла:
pathDLL = os.getcwd() + "\\MyTestCS.dll"
Чтобы подгрузить нужную нам библиотеку необходимо прописать следующий код:
clr.AddReference(pathDLL)
После чего можно импортировать модуль и всё, что в нем содержится. Если напрямую сделать импорт MyTestCS:
import MyTestCS
print(MyTestCS)
>>> <module 'MyTestCS'>
То можно увидеть, что наш проект загрузился как модуль. Также можно напрямую импортировать необходимые данные, например, необходимые классы или структуры из проекта.
Создадим экземпляр класса ShopClass и DataGoods через Python и обратимся к полям этих классов.
from MyTestCS import ShopClass, DataGoods
shop = ShopClass("Тест магазин")
shop.createShopClass(1)
goods = DataGoods("чехол для телефона", 500, "RUB")
print(shop.name)
>>> Тест магазин
print(shop.listGoods)
>>> [<MyTestCS.DataGoods object at 0x000001D04C3FE3C8>, <MyTestCS.DataGoods object at 0x000001D04C3FE438>, <MyTestCS.DataGoods object at 0x000001D04C3FE400>]
print(shop.listGoods[1].name, shop.listGoods[1].price, shop.listGoods[1].unit)
>>> телевизор 45000 RUB
print(goods.name, goods.price, goods.unit)
>>> чехол для телефона 500 RUB
Как итог, получилось вызвать код C# из Python и поработать с классами. Теперь протестируем производительность создания 200*100000 товаров через метод createShopClass:
shop = ShopClass("Тест магазин")
s = time.time()
shop.createShopClass(200 * 100000)
print("СОЗДАНИЕ ТОВАРОВ НА C#:", time.time() - s)
>>> СОЗДАНИЕ ТОВАРОВ НА C#: 2.9043374061584473
В прошлой статье время создания такого количества товаров заняло примерно 44 секунды. Использование C# вместо Python позволило ускорить этот процесс примерно в 15 раз, что является очень хорошим результатом.
Проблемы
Однако не может же быть всё настолько хорошо, чтобы броситься переписывать куски кода Python на C#. И это так. Попробуем из Python вручную дополнить товарами магазин:
shop = ShopClass("Тест магазин 1")
s = time.time()
shop.createShopClass(500000)
print("СОЗДАЛИ ТОВАРЫ ЧЕРЕЗ C#:", time.time()-s)
>>> СОЗДАЛИ ТОВАРЫ ЧЕРЕЗ C#: 0.07325911521911621
shop = ShopClass("Тест магазин 2")
s = time.time()
for _ in range(500000):
goods1 = DataGoods("телефон", 20000, "RUB")
goods2 = DataGoods("телевизор", 45000, "RUB")
goods3 = DataGoods("тостер", 2000, "RUB")
shop.listGoods.extend([goods1, goods2, goods3])
print("СОЗДАЛИ ТОВАРЫ ЧЕРЕЗ PYTHON:", time.time()-s)
>>> СОЗДАЛИ ТОВАРЫ ЧЕРЕЗ PYTHON: 5.2899720668792725
И проверим аналогичный код, написанный на Python:
istGoods = []
class DataGoods2:
def __init__(self, name, price, unit):
self.name = name
self.price = price
self.unit = unit
s = time.time()
for _ in range(500000):
goods1 = DataGoods2("телефон", 20000, "RUB")
goods2 = DataGoods2("телевизор", 45000, "RUB")
goods3 = DataGoods2("тостер", 2000, "RUB")
listGoods.extend([goods1, goods2, goods3])
print("СОЗДАЛИ PYTHON ОБЪЕКТЫ:", time.time()-s)
>>> СОЗДАЛИ PYTHON ОБЪЕКТЫ: 1.2972710132598877
Код чистого питона работает быстрее, чем дополнение объекта, созданного из модуля C#. Это связано с тем, что доступ к объектам, написанным на C#, занимает довольно много времени. Чтобы избежать таких проблем, необходимо писать всю логику работы с классом внутри C# кода, и не выносить эту логику в Python. Изменение скорости выполнения кода будет заметно при подсчете суммы всех товаров. Реализуем функцию подсчета суммы товаров на C# (внутри класса ShopClass):
public long getSumGoods() {
long sumGoods = 0;
foreach (DataGoods goods in this.listGoods) {
sumGoods += goods.price;
}
return sumGoods;
}
А также на Python:
shop = ShopClass("Магазин 3")
shop.createShopClass(1000000)
s = time.time()
shop.getSumGoods()
print("ВРЕМЯ НА СУММУ ТОВАРОВ C#:", time.time()-s)
>>> ВРЕМЯ НА СУММУ ТОВАРОВ C#: 0.0419771671295166
sumGoods = 0
for goods in shop.listGoods:
sumGoods += goods.price
print("ВРЕМЯ НА СУММУ ТОВАРОВ PYTHON:", time.time()-s)
>>> ВРЕМЯ НА СУММУ ТОВАРОВ PYTHON: 6.205681085586548
Python код выполняется гораздо медленнее, чем внутренние методы C#.
Многопоточность
Так как в C# отсутствует GIL, то мне стало интересно протестировать работу многопоточности в C# и попробовать запустить потоки в C# через Python. Для начала протестируем протестируем создание 3х классов ShopClass последовательно и заполним их 3.000.000 товаров:
public class testShop
{
public void testSpeedNoThread(int count)
{
testShopClass(count);
testShopClass(count);
testShopClass(count);
}
public static void testShopClass(int count)
{
ShopClass shop = new ShopClass("Магазин");
shop.createShopClass(count);
}
}
Python код для запуска:
tshop = testShop()
s = time.time()
tshop.testSpeedNoThread(3000000)
print("СОЗДАЕМ ПОСЛЕДОВАТЕЛЬНО 3 МАГАЗИНА:", time.time()-s)
>>> СОЗДАЕМ ПОСЛЕДОВАТЕЛЬНО 3 МАГАЗИНА: 2.1849117279052734
Дополним класс testShop для работы с потоками новым методом:
public static void testThread()
{
ExThread obj = new ExThread();
Thread thr = new Thread(new ThreadStart(obj.mythread1));
Thread thr2 = new Thread(new ThreadStart(obj.mythread1));
Thread thr3 = new Thread(new ThreadStart(obj.mythread1));
thr.Start();
thr2.Start();
thr3.Start();
thr.Join();
thr2.Join();
thr3.Join();
}
И создадим новый вспомогательный класс:
public class ExThread
{
public void mythread1()
{
ShopClass shop = new ShopClass("Магазин");
shop.createShopClass(3000000);
}
}
Запустим Python код для проверки работы потоков:
s = time.time()
tshopThread = testShop()
tshopThread.testThread()
print("СОЗДАЕМ 3 ПОТОКА C# ДЛЯ 3х МАГАЗИНОВ:", time.time()-s)
>>> СОЗДАЕМ 3 ПОТОКА C# ДЛЯ 3х МАГАЗИНОВ: 0.6765928268432617
Вывод
Использование частей кода, написанных на C# в Python возможно, но при таком подходе есть и свои минусы, например, скорость доступа к объектам. Использование pythonnet целесообразно, если имеются какие-то части кода, которые нет возможности переписать на Python, но они требуют подключения к основному проекту на Python.
P.S. есть и другие способы ускорить python, например, написать библиотеку на C/C++ или переписать часть кода на Cython с меньшими проблемами. В данной статье лишь представлена возможность использования C# и Python вместе. Также существует реализация Python для платформы Microsoft.NET под названием IronPython.
lostmsu
Я один из двух главных разработчиков Python.NET (с 2018ого, до этого проект разрабатывался другими людьми). AMA
BkmzSpb
Не очень силен в Python, поэтому лучше спрошу напрямую -- я правильно понимаю что вы хостите свою clr, что-то типа [вот этого](https://docs.microsoft.com/en-us/dotnet/core/tutorials/netcore-hosting) метода?
Если да, есть ли какие-то подводные камни?
Если нет, можете рассказать как это под капотом работает (вызов .NET из Python)?
lostmsu
Да, именно так. .NET Core ещё только в preview, но Mono и .NET Framework загружаются похожим способом. Отдельно нужно упомянуть, что pythonnet поддерживает не только CLR в Python, но и Python в CLR.
Подводных камней на самом деле полно. Главные:
— т.к. Python — динамический язык, вызов методов .NET с учётом перегрузок по типу параметров может превратиться в угадайку.
— есть некоторая магия с типом int из Python: для удобства он автоматически преобразуется к System.Int32 или System.Int64 по необходимости, но на самом деле это не совсем верно, т.к. int из Python может принимать сколь угодно большие значения. Однако если бы он был замапан на .NET BigInteger, код на Python использующий Python.NET был бы перегружен приведениями типов
— многопоточность. СPython всё ещё активно использует Global Interpreter Lock и многопоточный код на .NET должен это учитывать. Я, честно говоря, так и не разобрался, нужно ли использовать какие-либо другие примитивы синхронизации помимо Py.GIL (C#), чтобы предотвратить чтение stale data. Вопреки ожиданиям, похоже, что да. Т.е. захват Py.GIL не гарантирует, что вы увидите результат действий другого потока, который его только что отпустил. В целом у питона плохо с многопоточностью, особенно не своей.
После загрузки хоста CLR Python вызывает функцию инициализации, написанную на C#. Та помимо прочего создаёт (с использованием Python C API) типы в Python «зеркалирующие» типы .NET, которые код на Python потом может вызывать как обычно.
BkmzSpb
Спасибо! Я не увидел
clr-loader
когда смотрел на репозиторий исходного проекта.Мне на самом деле была интересна именно интеграция clr, понятно что маршаллинг объектов между двумя языка это довольно сложною
Если можно еще один тупой вопрос: каким образом становятся доступны .NET неймспейсы из Python?
from System import String
? Вы где-то держите баиндинги для BCL типов или это как-то можно сгенерировать на лету?У меня сугубо практический интерес, я немного участвую в аналогичном проекте по скрещиванию R и Rust, хочется понять как типичные задачи подобного плана решают другие команды.
BTW, я не увидел этого в этой статье, но если pythonnet работает напрямую с MSIL .dll и вы хостите свою clr, то не должно быть никаких ограничений на использование только C#. Можно писать код на F# и даже на голом IL.
lostmsu
Да, pythonnet можно использовать хоть с Visual Basic .NET
BkmzSpb
Ха, так и думал что это магия с импортом, теперь стало понятнее.
Спасибо за объяснения!
С удовольствием прочитал бы более детальную статью о технической стороне pythonnet если вдруг вы задумаетесь о написании таковой, и я думаю я здесь такой не один.
lostmsu
del