¿Cómo manejar el tipo de retorno de C++ std::vector<int> en ctypes de Python?

Resuelto PascalVKooten asked hace 11 años • 3 respuestas

No puedo encontrar cómo ctypes cerrará la brecha entre std::vectorPython y Python; En ningún lugar de Internet se menciona la combinación. ¿Es esta una mala práctica, no existe o me falta algo?

C++ : xxx.cpp

#include <fstream>
#include <string>
using namespace std;
extern "C" std::vector<int> foo(const char* FILE_NAME)
{
    string line;
    std::vector<int> result;
    ifstream myfile(FILE_NAME);
    while (getline(myfile, line)) {
      result.push_back(1);
    }

    return(result);
}

Python: xxx.py

import ctypes
xxx = ctypes.CDLL("./libxxx.so")
xxx.foo.argtypes = ??????
xxx.foo.restype = ??????
PascalVKooten avatar Jun 03 '13 00:06 PascalVKooten
Aceptado

Ya sea que este enfoque proporcione o no un tiempo de ejecución más rápido, explicaré un poco cómo podría hacerlo. Básicamente, cree un puntero a C++ vectorque pueda interactuar con Python a través de funciones de C. Luego puede empaquetar el código C++ en una clase de Python, ocultando los detalles de implementación de ctypes.

Incluí lo que pensé que serían métodos mágicos útiles para incluir en la clase de Python. Puede optar por eliminarlos o agregar más según sus necesidades. Sin embargo, es importante conservar el destructor.

C++

// vector_python.cpp
#include <vector>
#include <iostream>
#include <fstream>
#include <string>

using namespace std;

extern "C" void foo(vector<int>* v, const char* FILE_NAME){
    string line;
    ifstream myfile(FILE_NAME);
    while (getline(myfile, line)) v->push_back(1);
}

extern "C" {
    vector<int>* new_vector(){
        return new vector<int>;
    }
    void delete_vector(vector<int>* v){
        cout << "destructor called in C++ for " << v << endl;
        delete v;
    }
    int vector_size(vector<int>* v){
        return v->size();
    }
    int vector_get(vector<int>* v, int i){
        return v->at(i);
    }
    void vector_push_back(vector<int>* v, int i){
        v->push_back(i);
    }
}

Compílelo como una biblioteca compartida. En Mac OS X esto podría verse así:

g++ -c -fPIC vector_python.cpp -o vector_python.o
g++ -shared -Wl,-install_name,vector_python_lib.so -o vector_python_lib.so vector_python.o

Pitón

from ctypes import *

class Vector(object):
    lib = cdll.LoadLibrary('vector_python_lib.so') # class level loading lib
    lib.new_vector.restype = c_void_p
    lib.new_vector.argtypes = []
    lib.delete_vector.restype = None
    lib.delete_vector.argtypes = [c_void_p]
    lib.vector_size.restype = c_int
    lib.vector_size.argtypes = [c_void_p]
    lib.vector_get.restype = c_int
    lib.vector_get.argtypes = [c_void_p, c_int]
    lib.vector_push_back.restype = None
    lib.vector_push_back.argtypes = [c_void_p, c_int]
    lib.foo.restype = None
    lib.foo.argtypes = [c_void_p]

    def __init__(self):
        self.vector = Vector.lib.new_vector()  # pointer to new vector

    def __del__(self):  # when reference count hits 0 in Python,
        Vector.lib.delete_vector(self.vector)  # call C++ vector destructor

    def __len__(self):
        return Vector.lib.vector_size(self.vector)

    def __getitem__(self, i):  # access elements in vector at index
        if 0 <= i < len(self):
            return Vector.lib.vector_get(self.vector, c_int(i))
        raise IndexError('Vector index out of range')

    def __repr__(self):
        return '[{}]'.format(', '.join(str(self[i]) for i in range(len(self))))

    def push(self, i):  # push calls vector's push_back
        Vector.lib.vector_push_back(self.vector, c_int(i))

    def foo(self, filename):  # foo in Python calls foo in C++
        Vector.lib.foo(self.vector, c_char_p(filename))

Luego puedes probarlo en el intérprete ( file.txtsolo consta de tres líneas de galimatías).

>>> from vector import Vector
>>> a = Vector()
>>> a.push(22)
>>> a.push(88)
>>> a
[22, 88]
>>> a[1]
88
>>> a[2]
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "vector.py", line 30, in __getitem__
    raise IndexError('Vector index out of range')
IndexError: Vector index out of range
>>> a.foo('file.txt')
>>> a
[22, 88, 1, 1, 1]
>>> b = Vector()
>>> ^D
destructor called in C++ for 0x1003884d0
destructor called in C++ for 0x10039df10
Jared avatar Jun 02 '2013 21:06 Jared

La razón particular es que la velocidad es importante. Estoy creando una aplicación que debería poder manejar big data. En 200.000 filas, los faltantes deben contarse en 300 valores (matriz de 200k por 300). Creo, pero corríjanme si me equivoco, que C++ será significativamente más rápido.

Bueno, si estás leyendo un archivo grande, tu proceso estará principalmente vinculado a IO, por lo que los tiempos entre Python y C probablemente no serán significativamente diferentes.

El siguiente código...

result = []
for line in open('test.txt'):
    result.append(line.count('NA'))

...parece ejecutarse tan rápido como cualquier cosa que pueda hackear en C, aunque utiliza algún algoritmo optimizado con el que no estoy muy familiarizado.

Se necesitan menos de un segundo para procesar 200.000 líneas, aunque me interesaría ver si se puede escribir una función C que sea significativamente más rápida.


Actualizar

Si desea hacerlo en C y terminar con una lista de Python, probablemente sea más eficiente usar la API de Python/C para crear la lista usted mismo, en lugar de crear una lista de Python y std::vectorluego convertirla a una lista de Python.

Un ejemplo que simplemente devuelve una lista de números enteros del 0 al 99...

// hack.c

#include <python2.7/Python.h>

PyObject* foo(const char* filename)
{
    PyObject* result = PyList_New(0);
    int i;

    for (i = 0; i < 100; ++i)
    {
        PyList_Append(result, PyInt_FromLong(i));
    }

    return result;
}

Compilado con...

$ gcc -c hack.c -fPIC
$ ld -o hack.so -shared hack.o -lpython2.7

Ejemplo de uso...

>>> from ctypes import *
>>> dll = CDLL('./hack.so')
>>> dll.foo.restype = py_object
>>> dll.foo('foo')
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, ...]
Aya avatar Jun 02 '2013 20:06 Aya

Básicamente, devolver un objeto C++ desde una biblioteca cargada dinámicamente no es una buena idea. Para usar C++ vectoren código Python, debe enseñarle a Python a manejar objetos C++ (y esto incluye la representación binaria de los objetos que pueden cambiar con la nueva versión de un compilador de C++ o STL).

ctypesle permite interactuar con una biblioteca usando tipos C. No C++.

Tal vez el problema se pueda resolver mediante boost::python, pero parece más confiable usar C simple para la interacción.

nullptr avatar Jun 02 '2013 17:06 nullptr