如何在不复制对象的情况下向 Python 公开返回 C++ 对象的函数?

Posted

技术标签:

【中文标题】如何在不复制对象的情况下向 Python 公开返回 C++ 对象的函数?【英文标题】:How to expose a function returning a C++ object to Python without copying the object? 【发布时间】:2015-11-12 17:08:45 【问题描述】:

在another question 中,我学习了如何通过复制对象将返回 C++ 对象的函数公开给 Python。必须执行副本似乎不是最佳选择。如何在不复制的情况下返回对象?即如何直接访问PyPeakDetection.getPeaksself.thisptr.getPeaks(data) 返回的峰值(在peak_detection_.pyx 中定义)?

peak_detection.hpp

#ifndef PEAKDETECTION_H
#define PEAKDETECTION_H

#include <string>
#include <map>
#include <vector>

#include "peak.hpp"


class PeakDetection

    public:
        PeakDetection(std::map<std::string, std::string> config);
        std::vector<Peak> getPeaks(std::vector<float> &data);

    private:
        float _threshold;               
;

#endif

peak_detection.cpp

#include <iostream>
#include <string>

#include "peak.hpp"
#include "peak_detection.hpp"


using namespace std;


PeakDetection::PeakDetection(map<string, string> config)
   
    _threshold = stof(config["_threshold"]);


vector<Peak> PeakDetection::getPeaks(vector<float> &data)

    Peak peak1 = Peak(10,1);
    Peak peak2 = Peak(20,2);

    vector<Peak> test;
    test.push_back(peak1);
    test.push_back(peak2);

    return test;

peak.hpp

#ifndef PEAK_H
#define PEAK_H

class Peak 
    public:
        float freq;
        float mag;

        Peak() : freq(), mag() 
        Peak(float f, float m) : freq(f), mag(m) 
;

#endif

peak_detection_.pyx

# distutils: language = c++
# distutils: sources = peak_detection.cpp

from libcpp.vector cimport vector
from libcpp.map cimport map
from libcpp.string cimport string

cdef extern from "peak.hpp":
    cdef cppclass Peak:
        Peak()
        Peak(Peak &)
        float freq, mag


cdef class PyPeak:
    cdef Peak *thisptr

    def __cinit__(self):
        self.thisptr = new Peak()

    def __dealloc__(self):
        del self.thisptr

    cdef copy(self, Peak &other):
        del self.thisptr
        self.thisptr = new Peak(other)

    def __repr__(self):
        return "<Peak: freq=0, mag=1>".format(self.freq, self.mag)

    property freq:
        def __get__(self): return self.thisptr.freq
        def __set__(self, freq): self.thisptr.freq = freq

    property mag:
        def __get__(self): return self.thisptr.mag
        def __set__(self, mag): self.thisptr.mag = mag


cdef extern from "peak_detection.hpp":
    cdef cppclass PeakDetection:
        PeakDetection(map[string,string])
        vector[Peak] getPeaks(vector[float])

cdef class PyPeakDetection:
    cdef PeakDetection *thisptr

    def __cinit__(self, map[string,string] config):
        self.thisptr = new PeakDetection(config)

    def __dealloc__(self):
        del self.thisptr

    def getPeaks(self, data):
        cdef Peak peak
        cdef PyPeak new_peak
        cdef vector[Peak] peaks = self.thisptr.getPeaks(data)

        retval = []

        for peak in peaks:
            new_peak = PyPeak()
            new_peak.copy(peak) # how can I avoid that copy?
            retval.append(new_peak)

        return retval

【问题讨论】:

您可以将不想复制的对象包装在另一个具有值语义但不复制其持有值的对象中,例如 shared_ptr。不过,不确定这是否是个好主意;你也可以看看 boost::python 做了什么来解决这个问题。 【参考方案1】:

如果你有一个现代 C++ 编译器并且可以使用右值引用,移动构造函数和 std::move 就很简单了。我认为最简单的方法是为向量创建一个 Cython 包装器,然后使用移动构造函数来获取向量的内容。

显示的所有代码都在 peak_detection_.pyx 中。

首先包裹std::move。为简单起见,我只是包装了我们想要的一个案例 (vector&lt;Peak&gt;),而不是乱用模板。

cdef extern from "<utility>":
    vector[Peak]&& move(vector[Peak]&&) # just define for peak rather than anything else

其次,创建一个矢量包装类。这定义了像列表一样访问它所需的 Python 函数。它还定义了一个函数来调用移动赋值运算符

cdef class PyPeakVector:
    cdef vector[Peak] vec

    cdef move_from(self, vector[Peak]&& move_this):
        self.vec = move(move_this)

    def __getitem__(self,idx):
        return PyPeak2(self,idx)

    def __len__(self):
        return self.vec.size()

然后定义包装Peak 的类。这与您的其他类略有不同,因为它不拥有它包装的Peak(向量拥有)。否则,大部分功能保持不变

cdef class PyPeak2:
    cdef int idx
    cdef PyPeakVector vector # keep this alive, since it owns the peak rather that PyPeak2

    def __cinit__(self,PyPeakVector vec,idx):
        self.vector = vec
        self.idx = idx

    cdef Peak* getthisptr(self):
        # lookup the pointer each time - it isn't generally safe
        # to store pointers incase the vector is resized
        return &self.vector.vec[self.idx]

    # rest of functions as is

    # don't define a destructor since we don't own the Peak

最后,实现getPeaks()

cdef class PyPeakDetection:
    # ...    
    def getPeaks(self, data):
        cdef Peak peak
        cdef PyPeak new_peak
        cdef vector[Peak] peaks = self.thisptr.getPeaks(data)

        retval = PyPeakVector()
        retval.move_from(move(peaks))

        return retval

替代方法:

如果Peak 不平凡,您可以采用一种方法,在Peak 上调用move,而不是在向量上调用PyPeaks。对于您在此处移动和复制的情况,将等同于 `Peak。

如果您不能使用 C++11 功能,则需要稍微更改界面。不要让你的 C++ getPeaks 函数返回一个向量,而是将一个空向量引用(由 PyPeakVector 拥有)作为输入参数并写入它。其余大部分包装保持不变。

【讨论】:

再次感谢您的帮助!我不熟悉 move 语义,所以在调查您的解决方案之前,我首先需要阅读它。 非常好。如何在 cython 中确保 C++ 编译器支持 move ?是否可以对其进行条件编译? @StephaneMartin 我不认为 Cython 中的条件编译很容易。您最好创建一个小型 C++ shim,使用 __cplusplus 宏测试版本,然后使用 std::move 或在不支持移动的情况下进行复制。我越来越倾向于假设编译器确实支持 C++11(并因此移动)。【参考方案2】:

有两个项目完成了将 C++ 代码与 Python 的接口,它们经受住了时间Boost.Python 和SWIG 的考验。两者都通过向相关的 C/C++ 代码添加额外的标记并生成动态加载的 python 扩展库(.so 文件)和相关的 python 模块来工作。

但是,根据您的用例,可能仍会有一些看起来像“复制”的附加标记。但是,复制不应该那么广泛,它将全部暴露在 C++ 代码中,而不是在 Cython/Pyrex 中逐字显式复制。

【讨论】:

我相信 Cython 也“经受住了时间的考验”。我确信有办法做到这一点,在我看来这是一个基本功能。我对包装器不熟悉,所以也许我错了......

以上是关于如何在不复制对象的情况下向 Python 公开返回 C++ 对象的函数?的主要内容,如果未能解决你的问题,请参考以下文章

Jackson:如何在不修改 POJO 的情况下向 JSON 添加自定义属性

如何在不阻止 textView 触摸的情况下向所有 UITextView 添加一个 UIGestureRecognizer

如何在不重新绑定 this 的情况下返回一个带有粗箭头函数的对象? [复制]

在不干扰平移的情况下向 MKMapView 添加左滑动手势

如何在不覆盖的情况下向 Firebase Firestore 添加值?

如何在不影响周围文本的情况下向 div 添加文本?