Введение

На сегодняшний день 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.