从numpy.uint8数组中提取unsigned char

7 投票
2 回答
5877 浏览
提问于 2025-04-29 23:57

我有一段代码可以从Python的序列中提取数字值,这段代码在大多数情况下都能正常工作,但在处理numpy数组时就不行了。

当我尝试提取一个无符号字符时,我是这样做的:

unsigned char val = boost::python::extract<unsigned char>(sequence[n]);

这里的sequence是任何Python序列,n是索引。结果我遇到了以下错误:

TypeError: No registered converter was able to produce a C++ rvalue of type 
unsigned char from this Python object of type numpy.uint8

那么,我该如何在C++中成功提取一个无符号字符呢?我是否需要为numpy类型编写或注册特殊的转换器?我更希望能使用我在其他Python序列中用的相同代码,而不想为PyArrayObject*写特殊代码。

暂无标签

2 个回答

1

这里有一个稍微通用一点的版本,来自被接受的答案:
https://github.com/stuarteberg/printnum

(这个转换器是从VIGRA的C++/Python绑定中复制过来的。)

被接受的答案提到,它不支持不同标量类型之间的转换。而这个转换器可以在任何两个标量类型之间进行隐式转换(比如说,可以从int32转换到int8,或者从float32转换到uint8)。我觉得这样更好,但在方便性和安全性之间有一点权衡。

#include <iostream>
#include <boost/python.hpp>

#define NPY_NO_DEPRECATED_API NPY_1_7_API_VERSION

// http://docs.scipy.org/doc/numpy/reference/c-api.array.html#importing-the-api
#define PY_ARRAY_UNIQUE_SYMBOL printnum_cpp_module_PyArray_API
#include <numpy/arrayobject.h>
#include <numpy/arrayscalars.h>


/*
 * Boost python converter for numpy scalars, e.g. numpy.uint32(123).
 * Enables automatic conversion from numpy.intXX, floatXX
 * in python to C++ char, short, int, float, etc.
 * When casting from float to int (or wide int to narrow int),
 * normal C++ casting rules apply.
 *
 * Like all boost::python converters, this enables automatic conversion for function args
 * exposed via boost::python::def(), as well as values converted via boost::python::extract<>().
 *
 * Copied from the VIGRA C++ library source code (MIT license).
 * http://ukoethe.github.io/vigra
 * https://github.com/ukoethe/vigra
 */
template <typename ScalarType>
struct NumpyScalarConverter
{
    NumpyScalarConverter()
    {
        using namespace boost::python;
        converter::registry::push_back( &convertible, &construct, type_id<ScalarType>());
    }

    // Determine if obj_ptr is a supported numpy.number
    static void* convertible(PyObject* obj_ptr)
    {
        if (PyArray_IsScalar(obj_ptr, Float32) ||
            PyArray_IsScalar(obj_ptr, Float64) ||
            PyArray_IsScalar(obj_ptr, Int8)    ||
            PyArray_IsScalar(obj_ptr, Int16)   ||
            PyArray_IsScalar(obj_ptr, Int32)   ||
            PyArray_IsScalar(obj_ptr, Int64)   ||
            PyArray_IsScalar(obj_ptr, UInt8)   ||
            PyArray_IsScalar(obj_ptr, UInt16)  ||
            PyArray_IsScalar(obj_ptr, UInt32)  ||
            PyArray_IsScalar(obj_ptr, UInt64))
        {
            return obj_ptr;
        }
        return 0;
    }

    static void construct( PyObject* obj_ptr, boost::python::converter::rvalue_from_python_stage1_data* data)
    {
        using namespace boost::python;

        // Grab pointer to memory into which to construct the C++ scalar
        void* storage = ((converter::rvalue_from_python_storage<ScalarType>*) data)->storage.bytes;

        // in-place construct the new scalar value
        ScalarType * scalar = new (storage) ScalarType;

        if (PyArray_IsScalar(obj_ptr, Float32))
            (*scalar) = PyArrayScalar_VAL(obj_ptr, Float32);
        else if (PyArray_IsScalar(obj_ptr, Float64))
            (*scalar) = PyArrayScalar_VAL(obj_ptr, Float64);
        else if (PyArray_IsScalar(obj_ptr, Int8))
            (*scalar) = PyArrayScalar_VAL(obj_ptr, Int8);
        else if (PyArray_IsScalar(obj_ptr, Int16))
            (*scalar) = PyArrayScalar_VAL(obj_ptr, Int16);
        else if (PyArray_IsScalar(obj_ptr, Int32))
            (*scalar) = PyArrayScalar_VAL(obj_ptr, Int32);
        else if (PyArray_IsScalar(obj_ptr, Int64))
            (*scalar) = PyArrayScalar_VAL(obj_ptr, Int64);
        else if (PyArray_IsScalar(obj_ptr, UInt8))
            (*scalar) = PyArrayScalar_VAL(obj_ptr, UInt8);
        else if (PyArray_IsScalar(obj_ptr, UInt16))
            (*scalar) = PyArrayScalar_VAL(obj_ptr, UInt16);
        else if (PyArray_IsScalar(obj_ptr, UInt32))
            (*scalar) = PyArrayScalar_VAL(obj_ptr, UInt32);
        else if (PyArray_IsScalar(obj_ptr, UInt64))
            (*scalar) = PyArrayScalar_VAL(obj_ptr, UInt64);

        // Stash the memory chunk pointer for later use by boost.python
        data->convertible = storage;
    }
};

/*
 * A silly function to test scalar conversion.
 * The first arg tests automatic function argument conversion.
 * The second arg is used to demonstrate explicit conversion via boost::python::extract<>()
 */
void print_number( uint32_t number, boost::python::object other_number )
{
    using namespace boost::python;
    std::cout << "The number is: " << number << std::endl;
    std::cout << "The other number is: " << extract<int16_t>(other_number) << std::endl;
}

/*
 * Instantiate the python extension module 'printnum'.
 *
 * Example Python usage:
 *
 *     import numpy as np
 *     from printnum import print_number
 *     print_number( np.uint8(123), np.int64(-456) )
 *
 *     ## That prints the following:
 *     # The number is: 123
 *     # The other number is: -456
 */
BOOST_PYTHON_MODULE(printnum)
{
    using namespace boost::python;

    // http://docs.scipy.org/doc/numpy/reference/c-api.array.html#importing-the-api
    import_array();

    // Register conversion for all scalar types.
    NumpyScalarConverter<signed char>();
    NumpyScalarConverter<short>();
    NumpyScalarConverter<int>();
    NumpyScalarConverter<long>();
    NumpyScalarConverter<long long>();
    NumpyScalarConverter<unsigned char>();
    NumpyScalarConverter<unsigned short>();
    NumpyScalarConverter<unsigned int>();
    NumpyScalarConverter<unsigned long>();
    NumpyScalarConverter<unsigned long long>();
    NumpyScalarConverter<float>();
    NumpyScalarConverter<double>();

    // Expose our C++ function as a python function.
    def("print_number", &print_number, (arg("number"), arg("other_number")));
}
5

我们可以使用Boost.Python注册一个自定义的从Python转换器,这个转换器可以处理从NumPy数组标量(比如 numpy.uint8)到C++标量(比如 unsigned char)的转换。注册一个自定义的从Python转换器主要有三个部分:

  • 一个函数,用来检查一个 PyObject 是否可以转换。如果返回 NULL,说明这个 PyObject 不能使用注册的转换器。
  • 一个构造函数,用于从 PyObject 创建C++类型。这个函数只有在 converter(PyObject) 不返回 NULL 的情况下才会被调用。
  • 要构造的C++类型。

从NumPy数组标量中提取值需要调用一些NumPy的C API:

  • import_array() 必须在初始化将要使用NumPy C API的扩展模块时调用。根据扩展如何使用NumPy C API,可能还需要其他导入要求。
  • PyArray_CheckScalar() 用来检查一个 PyObject 是否是NumPy数组标量。
  • PyArray_DescrFromScalar() 获取数组标量的数据类型描述对象。这个描述对象包含了如何解释底层字节的信息。例如,它的 type_num 数据成员包含一个对应C类型的枚举值。
  • PyArray_ScalarAsCtype() 可以用来从NumPy数组标量中提取C类型的值。

下面是一个完整的示例,演示如何使用一个辅助类 enable_numpy_scalar_converter 来注册特定的NumPy数组标量与它们对应的C++类型。

#include <boost/cstdint.hpp>
#include <boost/python.hpp>
#define NPY_NO_DEPRECATED_API NPY_1_7_API_VERSION
#include <numpy/arrayobject.h>

// Mockup functions.

/// @brief Mockup function that will explicitly extract a uint8_t
///        from the Boost.Python object.
boost::uint8_t test_generic_uint8(boost::python::object object)
{
  return boost::python::extract<boost::uint8_t>(object)();
}

/// @brief Mockup function that uses automatic conversions for uint8_t.
boost::uint8_t test_specific_uint8(boost::uint8_t value) { return value; }

/// @brief Mokcup function that uses automatic conversions for int32_t.
boost::int32_t test_specific_int32(boost::int32_t value) { return value; }


/// @brief Converter type that enables automatic conversions between NumPy
///        scalars and C++ types.
template <typename T, NPY_TYPES NumPyScalarType>
struct enable_numpy_scalar_converter
{
  enable_numpy_scalar_converter()
  {
    // Required NumPy call in order to use the NumPy C API within another
    // extension module.
    import_array();

    boost::python::converter::registry::push_back(
      &convertible,
      &construct,
      boost::python::type_id<T>());
  }

  static void* convertible(PyObject* object)
  {
    // The object is convertible if all of the following are true:
    // - is a valid object.
    // - is a numpy array scalar.
    // - its descriptor type matches the type for this converter.
    return (
      object &&                                                    // Valid
      PyArray_CheckScalar(object) &&                               // Scalar
      PyArray_DescrFromScalar(object)->type_num == NumPyScalarType // Match
    )
      ? object // The Python object can be converted.
      : NULL;
  }

  static void construct(
    PyObject* object,
    boost::python::converter::rvalue_from_python_stage1_data* data)
  {
    // Obtain a handle to the memory block that the converter has allocated
    // for the C++ type.
    namespace python = boost::python;
    typedef python::converter::rvalue_from_python_storage<T> storage_type;
    void* storage = reinterpret_cast<storage_type*>(data)->storage.bytes;

    // Extract the array scalar type directly into the storage.
    PyArray_ScalarAsCtype(object, storage);

    // Set convertible to indicate success. 
    data->convertible = storage;
  }
};

BOOST_PYTHON_MODULE(example)
{
  namespace python = boost::python;

  // Enable numpy scalar conversions.
  enable_numpy_scalar_converter<boost::uint8_t, NPY_UBYTE>();
  enable_numpy_scalar_converter<boost::int32_t, NPY_INT>();

  // Expose test functions.
  python::def("test_generic_uint8",  &test_generic_uint8);
  python::def("test_specific_uint8", &test_specific_uint8);
  python::def("test_specific_int32", &test_specific_int32);
}

交互使用示例:

>>> import numpy
>>> import example
>>> assert(42 == example.test_generic_uint8(42))
>>> assert(42 == example.test_generic_uint8(numpy.uint8(42)))
>>> assert(42 == example.test_specific_uint8(42))
>>> assert(42 == example.test_specific_uint8(numpy.uint8(42)))
>>> assert(42 == example.test_specific_int32(numpy.int32(42)))
>>> example.test_specific_int32(numpy.int8(42))
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
Boost.Python.ArgumentError: Python argument types in
    example.test_specific_int32(numpy.int8)
did not match C++ signature:
    test_specific_int32(int)
>>> example.test_generic_uint8(numpy.int8(42))
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: No registered converter was able to produce a C++ rvalue of type
  unsigned char from this Python object of type numpy.int8

从交互使用中有几点需要注意:

  • Boost.Python能够从 numpy.uint8int Python对象中提取 boost::uint8_t
  • enable_numpy_scalar_converter 不支持类型提升。例如,test_specific_int32() 应该可以安全地接受一个被提升到更大标量类型(如 int)的 numpy.int8 对象。如果想要进行类型提升:
    • convertible() 需要检查兼容的 NPY_TYPES
    • construct() 应该使用 PyArray_CastScalarToCtype() 将提取的数组标量值转换为所需的C++类型。

撰写回答