Перейти к содержанию

Домашнее задание#

Задание

Название работы: "Разработка Модуля Python на С++"

  1. По вариантам реализовать модуль на С++ и произвести подключение в Python проект (дать формулировку задания, номер варианта, привести какой метод компиляции был выбран (скрины консольных команд или настройки проекта))
  2. Произвести замеры по скорости работы модуля по времени! (измерения времени производить внутри файла C++)
  3. Реализовать аналогичную функцию на чистом Python
  4. Произвести замеры по скорости работы чисто питоновской функции
  5. Замеры ввести в таблицу. Округление времени до 2 знаков после запятой.
№ Теста Количество итераций C++ (с) Python (с)
1 10000 5.23 12.45
2 50000 18.67 45.92
3 100000 36.89 90.75
  1. Построить график по вашим данным, пользуясь любым удобным способом (рекомендуется Excel)
  2. Сделать вывод во сколько раз и кто быстрее
  3. Приложить в виде Листинга 1 и Листинга 2 коды программ
  4. Дать ссылку на гитхаб

Задание повышенной сложности.

Методичские указания#

CTypes + Visual studio CODE#

В Windows библиотека динамической компоновки (DLL) является исполняемым файлом, который выступает в качестве общей библиотеки функций и ресурсов. Динамическая компоновка — это возможность операционной системы. Она позволяет исполняемому файлу вызывать функции или использовать ресурсы, хранящиеся в отдельном файле. Эти функции и ресурсы можно компилировать и развертывать отдельно от использующих их исполняемых файлов.

Библиотека DLL не является отдельным исполняемым файлом. Библиотеки DLL выполняются в контексте приложений, которые их вызывают. Операционная система загружает библиотеку DLL в область памяти приложения. Это делается либо при загрузке приложения (неявная компоновка), либо по запросу во время выполнения (явная компоновка). Библиотеки DLL также упрощают совместное использование функций и ресурсов различными исполняемыми файлами. Несколько приложений могут осуществлять одновременный доступ к содержимому одной копии библиотеки DLL в памяти.

Создайте новый проект в Visual studio CODE

https://ratcatcher.ru/media/inf/pr/prC/VS_CODE_1.png

Из списка вариантов выберите пункт Библиотека динамической компоновки DLL. Выберите место для проекта.

https://ratcatcher.ru/media/inf/pr/prC/VS_CODE_2.png

Автоматически сгененрируются несколько файлов pch.h и dllmain

Question
// dllmain.cpp : Определяет точку входа для приложения DLL.
#include "pch.h"

BOOL APIENTRY DllMain( HMODULE hModule,
                    DWORD  ul_reason_for_call,
                    LPVOID lpReserved
                    )
{
    switch (ul_reason_for_call)
    {
    case DLL_PROCESS_ATTACH:
    case DLL_THREAD_ATTACH:
    case DLL_THREAD_DETACH:
    case DLL_PROCESS_DETACH:
        break;
    }
    return TRUE;
}

Для дополнения кода необходимо добавить в проект (создать) файл хедера (например, Header.h) и соотвествующий ему файл cpp (например, Source.cpp)

Рассмотрим следующуюзадачу: пусть мне надо создать библиотеку из 3 функций (Функция для сложения целых чисел, Функция для сложения чисел с плавающей точкой, Функция вычисления синуса для N случайных чисел)

Приведем пример Header.h для данной задачи:

#pragma once

#ifdef MYLIBRARY_EXPORTS
#define MYLIBRARY_API __declspec(dllexport)  // Для компиляции библиотеки
#else
#define MYLIBRARY_API __declspec(dllimport) // Для использования библиотеки
#endif

extern "C" {
    MYLIBRARY_API int add_int(int num1, int num2);   // Функция для сложения целых чисел
    MYLIBRARY_API float add_float(float num1, float num2);  // Функция для сложения чисел с плавающей точкой
    MYLIBRARY_API float compute_sin(int N);  // Функция вычисления синуса для N случайных чисел
}

MYLIBRARY_API — это макрос, который используется для управления экспортом и импортом функций из DLL. Когда библиотека компилируется, этот макрос подставляет правильную директиву компилятора:
- __declspec(dllexport) используется для указания, что символ (функция или переменная) должен быть экспортирован из DLL.
- __declspec(dllimport) используется для указания, что символ будет импортирован из DLL.

Приведем пример соотвествующего cpp кода, в котором не будет ничего интересного, за исключением необходимости включения в код #include "pch.h"

#include "pch.h"
#include "Header.h"
#include <cmath>
#include <cstdlib>
#include <ctime>

#ifndef M_PI
#define M_PI 3.14159265358979323846
#endif

int add_int(int num1, int num2) {
    return num1 + num2;
}

float add_float(float num1, float num2) {
    return num1 + num2;
};

float compute_sin(int N) {
    clock_t start_time = clock(); // Начало замера времени

    float result = 0.0f;
    for (int i = 0; i < N; ++i) {
        float angle = static_cast<float>(rand()) / RAND_MAX * 2 * M_PI; // Случайный угол в радианах
        result += sin(angle); // Вычисление синуса
    }

    clock_t end_time = clock(); // Конец замера времени
    float elapsed_time = float(end_time - start_time) / CLOCKS_PER_SEC; // Время в секундах

    return elapsed_time; // Возвращаем время выполнения
}

После добавления файлов можно собрать решение

https://ratcatcher.ru/media/inf/pr/prC/VS_CODE_3.png

Затем найдите dll файл (мой проект назывался Dll1, поэтому название файла Dll1.dll)

Создайте проект на python (например в Visual studio Code)

Скопируйте *.dll файл в дирректорию, где будем писать python код

https://ratcatcher.ru/media/inf/pr/prC/VS_CODE_4.png

В Python-коде мы сначала импортируем модуль ctypes . Затем функция CDLL из того же модуля используется для загрузки C-библиотеки. Теперь функции из C-кода доступны для нас через переменную adder . Когда мы вызываем adder.add_int() , то автоматически вызывается C-функция add_int.

from ctypes import *
# Загружаем библиотеку
adder = CDLL('./adder.dll')
# Находим сумму целых чисел
res_int = adder.add_int(4,5)
print("Сумма 4 и 5 = " + str(res_int))
# Находим сумму действительных чисел
a = c_float(5.5)
b = c_float(4.1)
add_float = adder.add_float
add_float.restype = c_float
print("Сумма 5.5 и 4.1 = " + str(add_float(a, b)))

Интерфейс модуля ctypes позволяет использовать питоновские целые числа и строки при вызове C-функций.
Для других типов, например логического или действительных чисел, мы должны использовать корректные ctypes .

Python/C API#

C/Python API это, вероятно, наиболее широко применяемый метод - не благодаря своей простоте, а потому что он позволяет оперировать Python объектами из C кода.

Этот метод подразумевает написание C-кода специально для работы с Python. Все объекты Python представляются как структуры PyObject и заголовочный файл Python.h предоставляет различные функции для работы с объектами.

Например, если PyObject одновременно PyListType (список), то мы можем использовать функцию PyList_Size() , чтобы получить длину списка.
Это эквивалентно коду len(some_list) в Python. Большинство основных функций/операторов для стандартных Python объектов доступны в C через Python.h .

Для работы вам может понадобиться Техническая документация Python/C API

Давайте напишем С-библиотеку для суммирования всех элементов списка Python (все элементы являются числами).

Создайте новый проект в Visual studio CODE

https://ratcatcher.ru/media/inf/pr/prC/VS_CODE_1.png

Создайте консольное приложение.
Найдите где располагается Python.
Настройте среду VS code(вкладка проект->свойства) согласно таблице:

Вкладка и раздел Свойство Значение
Общие свойства > конфигурации Имя цели Укажите имя модуля, который будет ссылаться на него из Python в from...import инструкциях, таких как суперфасткод. Это же имя будет использоваться в C++ при определении модуля для Python. Чтобы применить имя проекта в качестве имени модуля, сохраните значение по умолчанию $<(ProjectName)>. Для python_d.exe добавьте _d в конец файла.
Тип конфигурации Динамическая библиотека (.dll)
C/C++ > Общие Дополнительные каталоги включаемых файлов Добавьте папку "Включить Python" в соответствии с вашей установкой (например, c:\Python310\include).
C/C++ > Препроцессор Определения препроцессора Если он присутствует, измените значение _DEBUG на NDEBUG, чтобы оно соответствовало версии CPython. При использовании python_d.exe оставьте это значение неизменным.
C/C++ > Создание кода Библиотека времени выполнения Многопоточные библиотеки DLL (/MD) для соответствия версии выпуска (nondebug) CPython. При использовании python_d.exe оставьте это значение как многопоточная библиотека DLL отладки (/MDd).
Базовые проверки среды выполнения По умолчанию
Компоновщик > Общие Дополнительные каталоги библиотек Добавьте подходящую для вашей установки папку Python libs с файлами .lib (например, c:\Python310\libs). Убедитесь, что указали папку libs, содержащую файлы .lib, но не папку Lib, содержащую файлы .py.

Пример настройки C/C++ > Общие| Дополнительные каталоги включаемых файлов

https://ratcatcher.ru/media/inf/pr/prC/VS_CODE_5.png

Пример настройки Компоновщик > Общие | Дополнительные каталоги библиотек
https://ratcatcher.ru/media/inf/pr/prC/VS_CODE_6.png

Tip

Не перепутайте директорию libs и Lib!

Напишем код в файле Sourse.cpp

#include <Python.h>

// Эта функция вызывается из Python кода
static PyObject* addList_add(PyObject* self, PyObject* args) {

    PyObject* listObj;

    // Входящие аргументы находятся в кортеже
    // В нашем случае есть только один аргумент - список, на который мы будем
    // ссылаться как listObj
    if (!PyArg_ParseTuple(args, "O", &listObj))
        return NULL;

    // Длина списка
    long length = PyList_Size(listObj);

    // Проходимся по всем элементам
    int i, sum = 0;
    for (i = 0; i < length; i++) {
        // Получаем элемент из списка - он также Python-объект
        PyObject* temp = PyList_GetItem(listObj, i);
        // Мы знаем, что элемент это целое число - приводим его к типу C long
        long elem = PyLong_AsLong(temp);  // Используем PyLong_AsLong вместо PyInt_AsLong
        sum += elem;
    }

    // Возвращаемое в Python-код значение также Python-объект
    // Приводим C long к Python integer
    return Py_BuildValue("i", sum);
}

// Немного документации для 'add'
static char addList_docs[] =
"add( ): add all elements of the list\n";

/*
Эта таблица содержит необходимую информацию о функциях модуля
<имя функции в модуле Python>, <фактическая функция>,
<ожидаемые типы аргументов функции>, <документация функции>
*/
static PyMethodDef addList_funcs[] = {
    {"add", (PyCFunction)addList_add, METH_VARARGS, addList_docs},
    {NULL, NULL, 0, NULL}
};

/*
addList имя модуля и это блок его инициализации.
<желаемое имя модуля>, <таблица информации>, <документация модуля>
*/
static struct PyModuleDef addList_module = {
    PyModuleDef_HEAD_INIT,
    "addList",   // Имя модуля
    "Add all the lists", // Документация модуля
    -1,           // Размер состояния модуля
    addList_funcs // Таблица функций
};

PyMODINIT_FUNC PyInit_addList(void) {  // Для Python 3 используем PyInit_<module>
    return PyModule_Create(&addList_module); // Используем PyModule_Create вместо Py_InitModule3
}
Tip

Комментарии к коду
- Заголовочный файл содержит все требуемые типы (для
представления типов объектов в Python) и определения функций (для работы с
Python-объектами).
- Дальше мы пишем функцию, которую собираемся вызывать из Python. По
соглашению, имя функции принимается {module-name}_{function-name}, которое в
нашем случае - addList_add . Подробнее об этой функции будет дальше.
- Затем заполняем таблицу, которая содержит всю необходимую информацию о
функциях, которые мы хотим иметь в модуле. Каждая строка относится к функции,
последняя - контрольное значение (строка из null элементов).
- Затем идёт блок инициализации модуля - PyMODINIT_FUNC init{module-name} .

Функция addList_add принимает аргументы типа PyObject (args также является
кортежем, но поскольку в Python всё является объектами, мы используем
унифицированный тип PyObject ). Мы парсим входные аргументы (фактически,
разбиваем кортеж на отдельные элементы) при помощи PyArg_ParseTuple() . Первый
параметр является аргументом для парсинга. Второй аргумент - строка,
регламентирующая процесс парсинга элементов кортежа args. Знак на N-ой позиции
строки сообщает нам тип N-ого элемента кортежа args, например - 'i' значит integer, 's' -
строка и 'O' - Python-объект. Затем следует несколько аргументов, где мы хотели бы
хранить выходные элементы PyArg_ParseTuple() . Число этих аргументов равно числу
аргументов, которые планируется передавать в функцию модуля и их позиционность
должна соблюдаться

Например, если мы ожидаем строку, целое число и список в
таком порядке, сигнатура функции будет следующего вида:

int n;
char *s;
PyObject* list;
PyArg_ParseTuple(args, "siO", &n, &s, &list);

В данном случае, нам нужно извлечь только объект списка и сохранить его в
переменной listObj . Затем мы используем функцию PyList_Size() чтобы получить
длину списка. Логика совпадает с len(some_list) в Python.

Теперь мы итерируем по списку, получая элементы при помощи функции
PyLint_GetItem(list, index) . Так мы получаем PyObject*. Однако, поскольку мы знаем,
что Python-объекты еще и PyIntType , то используем функцию PyInt_AsLong(PyObj *)
для получения значения. Выполняем процедуру для каждого элемента и получаем
сумму.

Затем переместим наш файл .cpp в директорию, где будет находится файл python.

Создаем файл setup.py

Tip

setup.py — это скрипт, используемый в Python для создания и установки пакетов и модулей. Он описывает, как собрать и установить пакеты Python, а также конфигурацию, необходимую для компиляции расширений на C или C++

Пример кода setup.py для Source.cpp ( Имя пакета млэете использовать любое, которое вам захочется импортировать в последствии, в моем случае addList)

from distutils.core import setup, Extension 

setup(
    name='addList',               # Имя пакета
    version='1.0',                 # Версия пакета
    ext_modules=[Extension('addList', ['Source.cpp'])]  # Указание расширения на C/C++, которое будет собрано из файла Source.cpp
)

Теперь необхоимо открыть консоль и прописать команду (пути к файлу не учитываются, считается, что у вас открыта текущая дирректория проекта)

    python setup.py build_ext --inplace

build_ext: Этот параметр указывает на сборку расширений, то есть компиляцию C/C++ кода, указанных в файле setup.py, в динамическую библиотеку (например, .pyd или .so).
--inplace: Этот параметр заставляет установку происходить непосредственно в текущую директорию, а не в стандартную директорию установки для Python.
--force: Пересобирает расширение, даже если оно уже было собрано ранее.

Если команда не работает, убедитесь, что у вас установлен следующий пакет:

    pip install setuptools

Теперь проект собран, осталось импортировать модуль cpp, как обычную библиотеку в py

    import addList

    l = [1,2,3,4,5, 9]
    print("Сумма элементов списка - " + str(l) + " = " +  str(addList.add(l)))  

MinGW (для тех, у кого не получается)#

Чтобы проверить есть ли MinGW сделайте команду в CMD:

    g++ --help

Пример, когда он не установлен:

https://ratcatcher.ru/media/inf/pr/prC/MinGw1.png

Установочный файл GCC 11.4.0 + MinGW-w64 11.0.0 (UCRT) release 1 можно найти на этой странице.

Вы выбираете версию Win32 и Win64 в зависимости от своей системы.

https://ratcatcher.ru/media/inf/pr/prC/MinGw2.png

Вы скачаете архив winlibs-x86_64-posix-seh-gcc-11.4.0-mingw-w64ucrt-11.0.0-r1.7z

https://ratcatcher.ru/media/inf/pr/prC/MinGw3.png

Данные этого архива вытаскиваем. Внутри единственная папка minGW64
Её необходимо переместить в корень диска С (C:/minGW64)

https://ratcatcher.ru/media/inf/pr/prC/MinGw4.png

Откройте меню “Система” в “Панели управления”:

https://ratcatcher.ru/media/inf/pr/prC/MinGw5.png

Из меню “Система” перейдите в “Дополнительные параметры системы”:

https://ratcatcher.ru/media/inf/pr/prC/MinGw6.png

Выберите “Переменные среды”:

https://ratcatcher.ru/media/inf/pr/prC/MinGw7.png

Выберите переменную Path и нажмите кнопку “Изменить...“

https://ratcatcher.ru/media/inf/pr/prC/MinGw8.png

Добавьте в новую строку полный путь до директории mingw32\bin и нажмите кнопку OK.

https://ratcatcher.ru/media/inf/pr/prC/MinGw9.png

Чтобы проверить, что настройка выполнена успешно, откройте консоль (не в директории mingw32\bin) и выполните команду g++ --help:.

https://ratcatcher.ru/media/inf/pr/prC/MinGw10.png

Теперь скомпилируем C-файл в .dll -файл (.so под Linux). Так мы получим файл
adder.dll.

Скомпелируем файл на C++ в динамическую библиотеку (.dll) в операционной системе Windows, воспользовавшись MinGW. Для этого в консоли нужно прописать команду:

g++ -shared -o ИМЯ_ВАШЕГО_ФАЙЛА.dll МЯ_ВАШЕГО_ФАЙЛА.cpp

Пример кода для компиляции adder.сpp

// Простой C-файл - суммируем целые и действительные числа
#include <stdio.h>
int add_int(int, int);
float add_float(float, float);
int add_int(int num1, int num2){
return num1 + num2;
}
float add_float(float num1, float num2){
return num1 + num2;
}

После копмиляции C-файл в .dll -файл (.so под Linux). Так мы получим файл adder.dll.

Теперь мы можем легко подключить наш файл и вызвать функцию (при условии, что мы поместили его в файлы проекта!!!)

https://ratcatcher.ru/media/inf/pr/prC/VS_CODE_4.png

from ctypes import *
# Загружаем библиотеку
adder = CDLL('./adder.dll')
# Находим сумму целых чисел
res_int = adder.add_int(4,5)
print("Сумма 4 и 5 = " + str(res_int))
# Находим сумму действительных чисел
a = c_float(5.5)
b = c_float(4.1)
add_float = adder.add_float
add_float.restype = c_float
print("Сумма 5.5 и 4.1 = " + str(add_float(a, b)))

В примере выше, C-файл содержит простой код - две функции: одна для нахождения
суммы двух целых чисел, другая - действительных.

В Python-коде мы сначала импортируем модуль ctypes . Затем функция CDLL из того
же модуля используется для загрузки C-библиотеки. Теперь функции из C-кода
доступны для нас через переменную adder . Когда мы вызываем adder.add_int() , то
автоматически вызывается C-функция add_int . Интерфейс модуля ctypes позволяет
использовать питоновские целые числа и строки при вызове C-функций.
Для других типов, например логического или действительных чисел, мы должны
использовать корректные ctypes .

Мы делаем это при передаче параметров в
adder.add_float() . Сначала мы создаём требуемый тип c_float из float , затем
используем в качестве аргумента для C-кода. Этот метод простой и аккуратный, но
ограниченный. Мы, к примеру, не можем оперировать объектами на стороне C-кода.

Пример замера времени#

Вариант начала файла Сpp:

#include <Python.h>
#include <iostream>
#include <cmath>
#include <ctime>

// Функция для вычисления синуса N раз и измерения времени выполнения
double calculateSinusNTimes(int N) {
    clock_t start_time = clock(); // Запуск таймера

    // Вычисление синуса N раз
    for (int i = 0; i < N; ++i) {
        double result = std::sin(0.5); // Вычисление синуса числа
        // Здесь можно использовать другие значения для вычисления синуса
    }

    clock_t end_time = clock(); // Завершение таймера
    double time_taken = double(end_time - start_time) / CLOCKS_PER_SEC; // Вычисление времени выполнения в секундах

    return time_taken;

Вариант функции для Python

import time
import math

def calculate_sin_n_times(N):
    start_time = time.time()  # Запуск таймера

    # Вычисление синуса N раз
    for i in range(N):
        result = math.sin(0.5)  # Вычисление синуса числа
        # Здесь можно использовать другие значения для вычисления синуса

    end_time = time.time()  # Завершение таймера
    time_taken = end_time - start_time  # Вычисление времени выполнения в секундах

    return time_taken

Варианты#

Вариант Метод Задача
1 CTypes Сложение двух массивов
2 Python/C API Вычитание двух массивов
3 CTypes Умножение вектора на число
4 Python/C API Деление вектора на число
5 CTypes Поиск максимума в массиве
6 Python/C API Поиск минимума в массиве
7 CTypes Скалярное произведение двух векторов
8 Python/C API Вычисление среднего арифметического по массиву
9 CTypes Вычисление медианного значения массива
10 Python/C API Вычисление математического ожидания
11 CTypes Вычисление стандартного отклонения
12 Python/C API Вычисление суммы элементов массива
13 CTypes Реализовать функцию Distinct по заданному массиву, возвращающую список только различных элементов массива
14 Python/C API Умножение вектора на число
15 CTypes Вычисление среднего арифметического по массиву
16 CTypes Вычисление математического ожидания
17 Python/C API Вычисление медианного значения массива
18 CTypes Вычисление суммы элементов массива
19 Python/C API Вычисление стандартного отклонения
20 CTypes Деление вектора на число
21 Python/C API Вычисление длины вектора
22 CTypes Создать нулевой массив заданного размера
23 Python/C API Вычисление расстояния между векторами
24 CTypes Вычисление длины вектора
25 Python/C API Создать нулевой массив заданного размера
26 CTypes Вычисление расстояния между векторами
27 Python/C API Создать единичный массив заданного размера
28 CTypes Подсчет количества четных значений в массиве
29 Python/C API Реализовать функцию Distinct, по заданному массиву, возвращающую список только различных элементов массива
30 CTypes Подсчет количества совпадающих элементов между двумя массивами