Одной из основных проблем в Cython является отсутствие поддержки шаблонов из файлов python. У меня есть система моделирования, написанная на С++, и я обертываю различные классы с помощью Cython и запускаю их с помощью python.
При использовании шаблона С++ невозможно отправить класс шаблона методу оболочки из python - вместо этого я заканчиваю отправку строк в Cython, которые затем должны проверять строку на известные значения, вручную передавая С++ для базового метода С++. Это делает абсолютный смысл, поскольку Cython действительно должен знать возможные аргументы шаблона для компиляции С++, но тем не менее это проблема.
Это становится настоящей досадой, поскольку список кандидатов для этих шаблонных методов растет - особенно в случае двух или трех шаблонов для одного метода С++, где я должен выполнять два или три уровня операторов if внутри cython.
К счастью, я нахожусь в счастливой позиции, будучи единственным автором и пользователем этой кодовой базы на данный момент. Я очень рад рефакторингу и хочу воспользоваться возможностью, чтобы сохранить головные боли в будущем. Я особенно ищу совет относительно некоторых способов, которыми я могу избежать использования шаблонов на стороне С++ (как проблема шаблона проектирования), вместо того, чтобы полагаться на какой-то хакерский подход на стороне cython. Если у cython есть эти ограничения на шаблоны.
Я написал минимальный рабочий пример, чтобы подчеркнуть тот процесс, который происходит в моей программе. На самом деле, это моделирование с уплотненными веществами, которое очень сильно выигрывает от параллельной обработки (с использованием OMP), и именно здесь мне, как мне кажется, нужен мой шаблон. В то же время пытаясь сохранить его минимальным, как в простоте, он компилирует и производит выходные данные, чтобы вы могли видеть, что происходит. Он скомпилирован с g++, и я связываюсь с OMP с помощью -lgomp (или удаляю прагмы и включаю) и использую флаг std = С++ 11.
#include <vector>
#include <map>
#include <algorithm>
#include <omp.h>
#include <iostream>
#include <iomanip>
/*
* Just a class containing some components to run through
* a Modifier (see below)
*/
class ToModify{
public:
std::vector<double> Components;
ToModify(std::vector<double> components) : Components(components){}
};
/*
* An abstract class which handles the modification of ToModify
* components in an arbitrary way.
* It is, however, known that child classes have a parameter
* (here, unimaginatively called Parameter).
* These parameters have a minimum and maximum value, which is
* to be determined by the child class.
*/
class Modifier{
protected:
double Parameter;
public:
Modifier(double parameter = 0) : Parameter(parameter){}
void setParameter(double parameter){
Parameter = parameter;
}
double getParameter(){
return Parameter;
}
virtual double getResult(double component) = 0;
};
/*
* Compute component ratios with a pre-factor.
* The minimum is zero, such that getResult(component) == 0 for all components.
* The maximum is such that getResult(component) <= 1 for all components.
*/
class RatioModifier : public Modifier{
public:
RatioModifier(double parameter = 0) : Modifier(parameter){}
double getResult(double component){
return Parameter * component;
}
static double getMaxParameter(const ToModify toModify){
double maxComponent = *std::max_element(toModify.Components.begin(), toModify.Components.end());
return 1.0 / maxComponent;
}
static double getMinParameter(const ToModify toModify){
return 0;
}
};
/*
* Compute the multiple of components with a factor f.
* The minimum parameter is the minimum of the components,
* such that f(min(components)) == min(components)^2.
* The maximum parameter is the maximum of the components,
* such that f(max(components)) == max(components)^2.
*/
class MultipleModifier : public Modifier{
public:
MultipleModifier(double parameter = 0) : Modifier(parameter){}
double getResult(double component){
return Parameter * component;
}
static double getMaxParameter(const ToModify toModify){
return *std::max_element(toModify.Components.begin(), toModify.Components.end());
}
static double getMinParameter(const ToModify toModify){
return *std::min_element(toModify.Components.begin(), toModify.Components.end());
}
};
/*
* A class to handle the mass-calculation of a ToModify objects' components
* through a given Modifier child class, across a range of parameters.
* The use of parallel processing highlights
* my need to generate multiple classes of a given type, and
* hence my (apparent) need to use templating.
*/
class ModifyManager{
protected:
const ToModify Modify;
public:
ModifyManager(ToModify modify) : Modify(modify){}
template<class ModifierClass>
std::map<double, std::vector<double>> scanModifiers(unsigned steps){
double min = ModifierClass::getMinParameter(Modify);
double max = ModifierClass::getMaxParameter(Modify);
double step = (max - min)/(steps-1);
std::map<double, std::vector<double>> result;
#pragma omp parallel for
for(unsigned i = 0; i < steps; ++i){
double parameter = min + step*i;
ModifierClass modifier(parameter);
std::vector<double> currentResult;
for(double m : Modify.Components){
currentResult.push_back(modifier.getResult(m));
}
#pragma omp critical
result[parameter] = currentResult;
}
return result;
}
template<class ModifierClass>
void outputScan(unsigned steps){
std::cout << std::endl << "-----------------" << std::endl;
std::cout << "original: " << std::endl;
std::cout << std::setprecision(3);
for(double component : Modify.Components){
std::cout << component << "\t";
}
std::cout << std::endl << "-----------------" << std::endl;
std::map<double, std::vector<double>> scan = scanModifiers<ModifierClass>(steps);
for(std::pair<double,std::vector<double>> valueSet : scan){
std::cout << "parameter: " << valueSet.first << ": ";
std::cout << std::endl << "-----------------" << std::endl;
for(double component : valueSet.second){
std::cout << component << "\t";
}
std::cout << std::endl << "-----------------" << std::endl;
}
}
};
int main(){
ToModify m({1,2,3,4,5});
ModifyManager manager(m);
manager.outputScan<RatioModifier>(10);
return 0;
}
Я надеюсь, что это не слишком много кода - я чувствовал, что пример использования был необходим. Я могу сделать урезанную версию, если это поможет.
Чтобы использовать этот вид в python, я бы (в моем текущем подходе) должен передать "RatioModifier"
или "MultipleModifier"
на cython через аргумент, который затем проверяет строку на известные значения и затем запускает scanModifier
с соответствующим классом в качестве шаблона. Все это хорошо и хорошо, но на стороне cython проблематично, когда я иду, добавляю тип модификатора или имею несколько шаблонов - особенно плохо, если у меня есть несколько вариантов scanModifier
с разными аргументами.
Общая идея заключается в том, что у меня есть набор модификаторов (в реальном приложении они имитируют магнитные/электрические поля и деформацию на решетках, а не просто выполняют основные математические операции над списком чисел), которые действуют на значения, находящиеся в пределах объект. Эти модификаторы имеют ряд потенциальных значений, и важно, чтобы модификаторы имели состояние (параметр "параметр", используемый и доступный в другом месте, используется не для сканирования по диапазонам). Объект ToModify (решетка) занимает много оперативной памяти, поэтому создание копий невозможно.
Каждый класс модификатора имеет различный диапазон значений для данного объекта ToModify. Это определяется природой модификации, а не самим экземпляром, поэтому я не могу (семантически) оправдывать установку их как нестатических методов объектов. Кажется, слишком сложно взломать экземпляр классов модификатора методу сканирования, поскольку его состояние не имеет смысла.
Я рассмотрел использование шаблона factory, но опять же, поскольку у него нет причин держать какое-либо состояние, он будет статичным - и передача статическому классу методу все еще требует шаблонирования, что возвращает меня к проблема перевода шаблона в cython. Я мог бы создать класс factory, который принимает строки имен классов и выбирает правильный класс для использования, но это, похоже, просто переводит мою проблему на сторону С++.
Потому что я всегда стараюсь писать значащий код, у меня есть дилемма. Кажется, самый простой способ обойти эту проблему - предоставить состояние объектам, которые ей не нужны, но мне совсем не нравится этот подход. Какие существуют другие подходы к решению этой проблемы? Должен ли я изменить способ работы на самом деле, или переместить его в свой класс? С этой целью я застрял.
Изменить
Я думаю, было бы неплохо представить пример стороны cython, чтобы показать, как это может быть таким кошмаром.
Представьте, что у меня есть метод, такой как выше, но с двумя параметрами шаблона. Например, один из них должен быть дочерним элементом Модификатора, а второй - SecondaryModifier, который дополнительно изменяет результат (для использования любого заинтересованного: в случае с реальной программой один "модификатор" является EdgeManager, который модифицирует вес края для имитации влияния деформации или внешнего магнитного поля, другой может быть SimulationType - например, подход с жесткой привязкой к поиску энергий/состояний или что-то более активное).
И скажем, мои модификаторы ModifierA1
, ModifierA2
, ModifierA3
, а мои вторичные модификаторы - ModifierB1
, ModifierB2
, ModifierB3
. И, ради того, чтобы стать действительно уродливым, давайте проведем три метода, которые используют два аргумента шаблона: method1
, method2
, method3
и дают им две подписи (один из которых принимает двойной, а один берет целое число), Это, в обычной настройке С++, очень распространено и не требует ужасающего кода, который следует.
cdef class SimulationManager:
cdef SimulationManager_Object* pointer
def __cinit__(self, ToModify toModify):
self.pointer = new SimulationManager_Object(<ToModify_Object*>(toModify.pointer))
def method1(self, str ModifierA, str ModifierB, someParameter):
useInt = False
if isinstance(someParameter, int):
useInt = True
elif not isinstance(someParameter, str):
raise NotImplementedError("Third argument to method1 must be an int or a string")
if ModifierA not in ["ModifierA1", "ModifierA2", "ModifierA3"]:
raise NotImplementedError("ModifierA '%s' not handled in SimulationManager.method1" % ModifierA)
if ModifierB not in ["ModifierB1", "ModifierB2", "ModifierB3"]:
raise NotImplementedError("ModifierB '%s' not handled in SimulationManager.method1" % ModifierB)
if ModifierA == "ModifierA1":
if ModifierB == "ModifierB1":
if useInt:
return self.pointer.method1[ModifierA1, ModifierB1](<int>someParameter)
else:
return self.pointer.method1[ModifierA1, ModifierB1](<str>someParameter)
elif ModifierB == "ModifierB2":
if useInt:
return self.pointer.method1[ModifierA1, ModifierB2](<int>someParameter)
else:
return self.pointer.method1[ModifierA1, ModifierB2](<str>someParameter)
else:
if useInt:
return self.pointer.method1[ModifierA1, ModifierB3](<int>someParameter)
else:
return self.pointer.method1[ModifierA1, ModifierB3](<str>someParameter)
elif ModifierA == "ModifierA2":
if ModifierB == "ModifierB1":
if useInt:
return self.pointer.method1[ModifierA2, ModifierB1](<int>someParameter)
else:
return self.pointer.method1[ModifierA2, ModifierB1](<str>someParameter)
elif ModifierB == "ModifierB2":
if useInt:
return self.pointer.method1[ModifierA2, ModifierB2](<int>someParameter)
else:
return self.pointer.method1[ModifierA2, ModifierB2](<str>someParameter)
else:
if useInt:
return self.pointer.method1[ModifierA2, ModifierB3](<int>someParameter)
else:
return self.pointer.method1[ModifierA2, ModifierB3](<str>someParameter)
elif ModifierA == "ModifierA3":
if ModifierB == "ModifierB1":
if useInt:
return self.pointer.method1[ModifierA3, ModifierB1](<int>someParameter)
else:
return self.pointer.method1[ModifierA3, ModifierB1](<str>someParameter)
elif ModifierB == "ModifierB2":
if useInt:
return self.pointer.method1[ModifierA3, ModifierB2](<int>someParameter)
else:
return self.pointer.method1[ModifierA3, ModifierB2](<str>someParameter)
else:
if useInt:
return self.pointer.method1[ModifierA3, ModifierB3](<int>someParameter)
else:
return self.pointer.method1[ModifierA3, ModifierB3](<str>someParameter)
def method2(self, str ModifierA, str ModifierB, someParameter):
useInt = False
if isinstance(someParameter, int):
useInt = True
elif not isinstance(someParameter, str):
raise NotImplementedError("Third argument to method2 must be an int or a string")
if ModifierA not in ["ModifierA1", "ModifierA2", "ModifierA3"]:
raise NotImplementedError("ModifierA '%s' not handled in SimulationManager.method2" % ModifierA)
if ModifierB not in ["ModifierB1", "ModifierB2", "ModifierB3"]:
raise NotImplementedError("ModifierB '%s' not handled in SimulationManager.method2" % ModifierB)
if ModifierA == "ModifierA1":
if ModifierB == "ModifierB1":
if useInt:
return self.pointer.method2[ModifierA1, ModifierB1](<int>someParameter)
else:
return self.pointer.method2[ModifierA1, ModifierB1](<str>someParameter)
elif ModifierB == "ModifierB2":
if useInt:
return self.pointer.method2[ModifierA1, ModifierB2](<int>someParameter)
else:
return self.pointer.method2[ModifierA1, ModifierB2](<str>someParameter)
else:
if useInt:
return self.pointer.method2[ModifierA1, ModifierB3](<int>someParameter)
else:
return self.pointer.method2[ModifierA1, ModifierB3](<str>someParameter)
elif ModifierA == "ModifierA2":
if ModifierB == "ModifierB1":
if useInt:
return self.pointer.method2[ModifierA2, ModifierB1](<int>someParameter)
else:
return self.pointer.method2[ModifierA2, ModifierB1](<str>someParameter)
elif ModifierB == "ModifierB2":
if useInt:
return self.pointer.method2[ModifierA2, ModifierB2](<int>someParameter)
else:
return self.pointer.method2[ModifierA2, ModifierB2](<str>someParameter)
else:
if useInt:
return self.pointer.method2[ModifierA2, ModifierB3](<int>someParameter)
else:
return self.pointer.method2[ModifierA2, ModifierB3](<str>someParameter)
elif ModifierA == "ModifierA3":
if ModifierB == "ModifierB1":
if useInt:
return self.pointer.method2[ModifierA3, ModifierB1](<int>someParameter)
else:
return self.pointer.method2[ModifierA3, ModifierB1](<str>someParameter)
elif ModifierB == "ModifierB2":
if useInt:
return self.pointer.method2[ModifierA3, ModifierB2](<int>someParameter)
else:
return self.pointer.method2[ModifierA3, ModifierB2](<str>someParameter)
else:
if useInt:
return self.pointer.method2[ModifierA3, ModifierB3](<int>someParameter)
else:
return self.pointer.method2[ModifierA3, ModifierB3](<str>someParameter)
def method3(self, str ModifierA, str ModifierB, someParameter):
useInt = False
if isinstance(someParameter, int):
useInt = True
elif not isinstance(someParameter, str):
raise NotImplementedError("Third argument to method3 must be an int or a string")
if ModifierA not in ["ModifierA1", "ModifierA2", "ModifierA3"]:
raise NotImplementedError("ModifierA '%s' not handled in SimulationManager.method3" % ModifierA)
if ModifierB not in ["ModifierB1", "ModifierB2", "ModifierB3"]:
raise NotImplementedError("ModifierB '%s' not handled in SimulationManager.method3" % ModifierB)
if ModifierA == "ModifierA1":
if ModifierB == "ModifierB1":
if useInt:
return self.pointer.method3[ModifierA1, ModifierB1](<int>someParameter)
else:
return self.pointer.method3[ModifierA1, ModifierB1](<str>someParameter)
elif ModifierB == "ModifierB2":
if useInt:
return self.pointer.method3[ModifierA1, ModifierB2](<int>someParameter)
else:
return self.pointer.method3[ModifierA1, ModifierB2](<str>someParameter)
else:
if useInt:
return self.pointer.method3[ModifierA1, ModifierB3](<int>someParameter)
else:
return self.pointer.method3[ModifierA1, ModifierB3](<str>someParameter)
elif ModifierA == "ModifierA2":
if ModifierB == "ModifierB1":
if useInt:
return self.pointer.method3[ModifierA2, ModifierB1](<int>someParameter)
else:
return self.pointer.method3[ModifierA2, ModifierB1](<str>someParameter)
elif ModifierB == "ModifierB2":
if useInt:
return self.pointer.method3[ModifierA2, ModifierB2](<int>someParameter)
else:
return self.pointer.method3[ModifierA2, ModifierB2](<str>someParameter)
else:
if useInt:
return self.pointer.method3[ModifierA2, ModifierB3](<int>someParameter)
else:
return self.pointer.method3[ModifierA2, ModifierB3](<str>someParameter)
elif ModifierA == "ModifierA3":
if ModifierB == "ModifierB1":
if useInt:
return self.pointer.method3[ModifierA3, ModifierB1](<int>someParameter)
else:
return self.pointer.method3[ModifierA3, ModifierB1](<str>someParameter)
elif ModifierB == "ModifierB2":
if useInt:
return self.pointer.method3[ModifierA3, ModifierB2](<int>someParameter)
else:
return self.pointer.method3[ModifierA3, ModifierB2](<str>someParameter)
else:
if useInt:
return self.pointer.method3[ModifierA3, ModifierB3](<int>someParameter)
else:
return self.pointer.method3[ModifierA3, ModifierB3](<str>someParameter)
Этот объем кода не только смехотворен для функциональности, но это означает, что мне теперь нужно отредактировать файлы .h, файлы .cpp,.pxd файлы и файл .pyx, чтобы добавить один новый тип модификатор. Учитывая, что у программистов есть встроенная одержимость эффективностью, этот процесс просто неприемлем для меня.
Опять же, я признаю, что это своего рода необходимый процесс с использованием cython (хотя я могу думать о многих способах улучшения этого процесса. Возможно, когда у меня будет больше свободного времени, я присомудуюсь к усилиям сообщества). Я прошу только на стороне С++ (если не существует обходного пути в цитоне, о котором я и Google не знают).
Одна вещь, которую я раньше не рассматривал, - это factory, состояние которой указывает тип создаваемых объектов и передает это. Это кажется немного расточительным, хотя, и снова просто подметает проблему под ковриком. Во всяком случае, я действительно прошу идеи (или шаблоны дизайна), и я не возражаю, насколько они сумасшедшие или неполные; Я просто хочу, чтобы поток творчества шел.