Cython 的主要问题之一是缺乏 Python 文件中的模板支持。我有一个用 C++ 编写的模拟系统,我用 Cython 包装了各种类并使用 python 运行它们。
当 C++ 方法被模板化时,不能从 python 中将模板类发送到包装器方法 - 相反,我最终将字符串发送到 Cython,然后它必须根据已知值检查字符串,手动将 C++ 类传递给底层的 C++ 方法。这绝对有道理,因为 Cython 确实需要知道可能的模板参数才能编译 C++,但这仍然是一个问题。
随着这些模板化方法的候选列表越来越多,这变得非常令人烦恼——尤其是在一个 c++ 方法的两个或三个模板的情况下,我必须在 cython 中执行两到三层 if 语句。
幸运的是,我现在很幸运是这个代码库的唯一作者和用户。我很高兴重构,并希望借此机会这样做,以节省 future 的头痛。我特别在寻求有关可以避免在 C++ 端使用模板的一些方法(作为设计模式问题)的建议,而不是在 cython 端依赖某种hacky 方法。如果 cython 对模板有这些限制。
我编写了一个最小的工作示例来突出显示在我的程序中发生的过程类型。但实际上,它是一种凝聚态模拟,它从并行处理(使用 OMP)中受益匪浅,这就是我的模板在我看来是必要的地方。在尽量保持最小化的同时,它会编译并生成输出,以便您可以看到发生了什么。它是用 g++ 编译的,我使用 -lgomp(或删除编译指示和包含)链接到 OMP,并使用 std=c++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 (lattice) 对象占用大量 RAM,因此无法创建拷贝。
对于给定的 ToModify 对象,每个修饰符类都有不同的值范围。这是由修改的性质决定的,而不是由实例本身决定的,因此我不能(从语义上)证明将它们设置为对象的非静态方法是合理的。将 Modifier 类的实例发送到扫描方法似乎太麻烦了,因为它的状态没有意义。
我曾考虑使用工厂模式 - 但同样,因为它没有理由保持任何类型的状态,它将是静态的 - 并且将静态类传递给方法仍然需要模板化,这让我回到了模板转换问题赛通。我可以创建一个工厂类,它接受类名字符串并选择要使用的正确类,但这似乎只是将我的问题转换为 C++ 方面。
因为我一直致力于编写有意义的代码,所以我有点进退两难。解决这个问题的最简单方法似乎是为不需要它的对象提供状态,但我根本不喜欢这种方法。围绕此类问题还存在哪些其他方法?我应该改变扫描方法的实际工作方式,还是将其移动到自己的类中?为此,我被困住了。
编辑
我认为提供一个 cython 方面的例子是个好主意,以展示这如何成为一场噩梦。
想象一下,我有一个像上面那样的方法,但有两个模板参数。比如说,一个是 Modifier 的 child ,另一个是一个 SecondaryModifier,它进一步修改结果(供任何感兴趣的人使用:在实际程序的情况下,一个 'Modifier' 是一个 EdgeManager,它修改一个边缘权重来模拟应变或外部磁场的影响;另一个可以是 SimulationType - 例如,用于寻找能量/状态的紧束缚模型方法,或更多涉及的东西)。
并说我的修饰符是
ModifierA1
, ModifierA2
, ModifierA3
,我的次要修饰符是 ModifierB1
, ModifierB2
, ModifierB3
.而且,为了变得非常丑陋,让我们使用三个使用两个模板参数的方法,method1
, method2
, method3
,并给他们两个签名(一个是双数,一个是整数)。这在正常的 C++ 设置中非常常见,并且不需要后面的可怕代码。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 文件只是为了添加一种新类型的修饰符。考虑到我们程序员对效率有一种与生俱来的痴迷,这种过程对我来说是 Not Acceptable 。
再次,我承认这是 cython 的一种必要过程(虽然我可以想到很多方法可以改进这个过程。也许当我有更多空闲时间时,我会加入社区的努力)。我要问的纯粹是 C++ 方面的(除非在 cython 中有一个我和谷歌都不知道的解决方法)。
我以前没有考虑过的一件事是一个工厂,它的状态指示要创建的对象的类型,并将其传递。不过,这似乎有点浪费,而且只是将问题扫到了地毯下。如果有的话,我真的是在寻求想法(或设计模式),我不介意它们有多疯狂或不完整;我只是想让一些创造力流动。
最佳答案
好的,所以我一直在玩弄工厂的想法。我仍然不相信它“有意义”,但在这种情况下,也许我对“合理”状态的痴迷不值得麻烦。
为此,我提出以下建议。返回通用修饰符的通用工厂类,具有处理一些常见(但特定于类)方法的子模板化工厂,以及覆盖参数的特定子工厂。这确实意味着依赖指针(从通用工厂返回抽象类指针),但我在原始代码库中使用了这些指针(老实说,不仅仅是为了它)。
我不相信这是最好的方法(并且不会“接受”它作为答案)。但是,这意味着我可以避免嵌套 if 语句。我以为我会发表评论。您在评论中的一些建议非常出色,我要感谢大家。
#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. They each have a range of valid
* parameters.
* These parameters have a minimum and maximum value, which is
* to be determined by the _factory_.
*/
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;
};
/*
* A generalised modifier factory, acting as the parent class
* for the specialised ChildModifierFactories below. This will
* be the type that the scanning method accepts as an argument.
*/
class GeneralModifierFactory{
public:
virtual Modifier* get(double parameter) = 0;
virtual double getMinParameter(ToModify const toModify) = 0;
virtual double getMaxParameter(ToModify const toModify) = 0;
};
/*
* This takes the type of modifier as a template argument. It
* is designed to be a parent to the ModifierFactories that
* follow. Other common methods that involve the modifier
* can be placed here to save code.
*/
template<class ChildModifier>
class ChildModifierFactory : public GeneralModifierFactory{
public:
ChildModifier* get(double parameter){
return new ChildModifier(parameter);
}
virtual double getMinParameter(ToModify const toModify) = 0;
virtual double getMaxParameter(ToModify const toModify) = 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;
}
};
/*
* This class handles the ranges of parameters which are valid in
* the RatioModifier. The parent class handles the constructions.
*/
class RatioModifierFactory : public ChildModifierFactory<RatioModifier>{
public:
double getMaxParameter(ToModify const toModify){
double maxComponent = *std::max_element(toModify.Components.begin(), toModify.Components.end());
return 1.0 / maxComponent;
}
double getMinParameter(ToModify const 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;
}
};
/*
* This class handles the ranges of parameters which are valid in
* the MultipleModifier. The parent class handles the constructions.
*/
class MultipleModifierFactory : public ChildModifierFactory<MultipleModifier>{
public:
double getMaxParameter(ToModify const toModify){
return *std::max_element(toModify.Components.begin(), toModify.Components.end());
}
double getMinParameter(ToModify const 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.
*/
class ModifyManager{
protected:
ToModify const Modify;
public:
ModifyManager(ToModify modify) : Modify(modify){}
std::map<double, std::vector<double>> scanModifiers(GeneralModifierFactory& factory, unsigned steps){
double min = factory.getMinParameter(Modify);
double max = factory.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;
Modifier* modifier = factory.get(parameter);
std::vector<double> currentResult;
for(double m : Modify.Components){
currentResult.push_back(modifier->getResult(m));
}
delete modifier;
#pragma omp critical
result[parameter] = currentResult;
}
return result;
}
void outputScan(GeneralModifierFactory& factory, 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(factory, 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);
RatioModifierFactory ratio;
MultipleModifierFactory multiple;
manager.outputScan(ratio, 10);
std::cout << " --------------- " << std::endl;
manager.outputScan(multiple, 10);
return 0;
}
现在我可以传递一个包装的工厂类,或者为每个这样的参数传递一个可以转换为这样的类(通过辅助函数)的字符串。不完全理想,因为工厂有一个状态——它不需要,除非它有一个 ToModify 对象作为成员(这似乎毫无意义)。但是,唉,它有效。
关于python - C++ 和 cython - 寻求一种避免模板限制的设计模式,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/32551607/