我正在使用 Pybind11/Nanobind 为我的 C++ 库编写 Python 绑定。
我的一个 C++ 函数接受
std::istream &
类型的参数,例如:
std::string cPXGStreamReader::testReadStream(std::istream &stream)
{
std::ostringstream contentStream;
std::string line;
while (std::getline(stream, line)) {
contentStream << line << '\n'; // Append line to the content string
}
return contentStream.str(); // Convert contentStream to string and return
}
我需要在Python中传递什么样的参数来对应这个?
我尝试传递
s
,其中创建 s
:
s = open(r"test_file.pxgf", "rb")
# and
s = io.BytesIO(b"some initial binary data: \x00\x01")
无济于事。我收到错误了
TypeError: test_read_file(): incompatible function arguments. The following argument types are supported:
1. (self: pxgf.PXGStreamReader, arg0: std::basic_istream<char,std::char_traits<char> >) -> str
Invoked with: <pxgf.PXGStreamReader object at 0x000002986CF9C6B0>, <_io.BytesIO object at 0x000002986CF92250>
Did you forget to `#include <pybind11/stl.h>`? Or <pybind11/complex.h>,
<pybind11/functional.h>, <pybind11/chrono.h>, etc. Some automatic
Pybind11 不提供对开箱即用的流参数的支持,因此需要开发自定义实现。我们需要某种适配器来创建一个从 Python 对象读取的
istream
。我想到了两个选择:
std::streambuf
并与std::istream
一起使用(相关问答)。boost::iostreams::stream
一起使用(您可以将其传递给您的函数而不进行任何修改)。我将重点关注后一个选项,并将解决方案限制为仅用于输入的类文件对象(即源自
io.IOBase
)。
我们需要创建一个满足 Boost IOStreams 的“源”模型的类(有关此主题的文档中有一个方便的 tutorial)。基本上具有以下特征:
struct Source {
typedef char char_type;
typedef source_tag category;
std::streamsize read(char* s, std::streamsize n)
{
// Read up to n characters from the input
// sequence into the buffer s, returning
// the number of characters read, or -1
// to indicate end-of-sequence.
}
};
在构造函数中,我们应该存储对用作数据源的 Python 对象 (
pybind11::object
) 的引用。我们还应该验证数据源对象实际上是类似文件的。
py::object io_module = py::module::import("io");
py::object text_io_base = io_module.attr("TextIOBase");
py::object buffered_io_base = io_module.attr("BufferedIOBase");
py::object raw_io_base = io_module.attr("RawIOBase");
if (!(py::isinstance(source_obj, text_io_base)
|| py::isinstance(source_obj, buffered_io_base)
|| py::isinstance(source_obj, raw_io_base))) {
throw std::invalid_argument("source_obj is not file-like");
}
最后,我们可以将类文件对象的
read
属性缓存在另一个 pybind11::object
成员变量中,以避免每次调用时都要查找。
read_source_obj_ = source_obj_.attr("read"); // Cache the function
此函数需要将请求的字节数读取到提供的缓冲区,并在达到时发出文件结束信号。最简单的方法是将 Python 文件类的
read()
方法的结果转换为 std::string
,然后将其内容复制到读取缓冲区。它适用于所有二进制和文本 IO。
py::object result = read_source_obj_(n);
auto const payload = result.cast<std::string>();
if (payload.empty()) {
return -1; // EOF
}
std::copy(payload.begin(), payload.end(), s);
return payload.size();
但是,这涉及到不必要的副本。如果您使用的是足够新的 C++ 标准,则可以转换为
std::string_view
。否则,可以使用较低级别的方法(基于pybind11::string
的实现):
py::object result = read_source_obj_(n);
if (PyUnicode_Check(result.ptr())) {
// Strings need to be converted to UTF8 first, giving us a bytes object
result = py::reinterpret_steal<py::object>(PyUnicode_AsUTF8String(result.ptr()));
if (!result) {
throw py::error_already_set();
}
}
assert(py::isinstance<py::bytes>(result));
// Get the internal buffer of the bytes object, to avoid an extra copy
char_type* buffer;
ssize_t buffer_size;
if (PYBIND11_BYTES_AS_STRING_AND_SIZE(result.ptr(), &buffer, &buffer_size)) {
throw py::error_already_set();
}
if (buffer_size == 0) {
return -1; // EOF
}
std::copy(buffer, buffer + buffer_size, s);
return buffer_size;
让我们使用一个根据您的示例建模的独立函数来测试这一点:
std::string test_read_stream(std::istream& stream)
{
std::ostringstream result;
std::string line;
while (std::getline(stream, line)) {
result << line << '\n';
}
return result.str();
}
为流创建 Pybind11 typecaster 看起来就像打开了另一罐蠕虫,所以让我们跳过它。相反,我们在定义函数绑定时使用相当简单的 lambda。它只需要使用我们的源代码构造一个
boost::iostreams::stream
,并调用包装的函数。
m.def("test_read_stream", [](py::object file_like) -> std::string {
boost::iostreams::stream<python_filelike_source> wrapper_stream(file_like);
return test_read_stream(wrapper_stream);
});
这里全部都在一起了:
#include <iosfwd>
#include <iostream>
#include <sstream>
#include <boost/iostreams/categories.hpp>
#include <boost/iostreams/stream.hpp>
#include <pybind11/pybind11.h>
#include <pybind11/embed.h>
namespace py = pybind11;
class python_filelike_source
{
public:
typedef char char_type;
typedef boost::iostreams::source_tag category;
python_filelike_source(py::object& source_obj)
: source_obj_(source_obj)
{
py::object io_module = py::module::import("io");
py::object text_io_base = io_module.attr("TextIOBase");
py::object buffered_io_base = io_module.attr("BufferedIOBase");
py::object raw_io_base = io_module.attr("RawIOBase");
if (!(py::isinstance(source_obj, text_io_base)
|| py::isinstance(source_obj, buffered_io_base)
|| py::isinstance(source_obj, raw_io_base))) {
throw std::invalid_argument("source_obj is not file-like");
}
read_source_obj_ = source_obj_.attr("read"); // Cache the function
}
std::streamsize read(char_type* s, std::streamsize n)
{
if (n <= 0) {
return 0;
}
py::object result = read_source_obj_(n);
if (PyUnicode_Check(result.ptr())) {
// Strings need to be converted to UTF8 first, giving us a bytes object
result = py::reinterpret_steal<py::object>(PyUnicode_AsUTF8String(result.ptr()));
if (!result) {
throw py::error_already_set();
}
}
assert(py::isinstance<py::bytes>(result));
// Get the internal buffer of the bytes object, to avoid an extra copy
char_type* buffer;
ssize_t buffer_size;
if (PYBIND11_BYTES_AS_STRING_AND_SIZE(result.ptr(), &buffer, &buffer_size)) {
throw py::error_already_set();
}
if (buffer_size == 0) {
return -1; // EOF
}
std::copy(buffer, buffer + buffer_size, s);
return buffer_size;
}
private:
py::object& source_obj_;
py::object read_source_obj_;
};
std::string test_read_stream(std::istream& stream)
{
std::ostringstream result;
std::string line;
while (std::getline(stream, line)) {
result << line << '\n';
}
return result.str();
}
PYBIND11_EMBEDDED_MODULE(testmodule, m)
{
m.def("test_read_stream", [](py::object file_like) -> std::string {
boost::iostreams::stream<python_filelike_source> wrapper_stream(file_like);
return test_read_stream(wrapper_stream);
});
}
int main()
{
py::scoped_interpreter guard{};
try {
py::exec(R"(\
import testmodule
import io
print("BytesIO:")
s = io.BytesIO(b"One\nTwo\nThree")
print(testmodule.test_read_stream(s))
print("StringIO:")
s = io.StringIO("Foo\nBar\nBaz")
print(testmodule.test_read_stream(s))
print("String:")
try:
print(testmodule.test_read_stream("abcd"))
except Exception as exc:
print(exc)
print("File:")
with open("example_file.txt", "w") as f:
f.write("TEST1\nTEST2\nTEST3")
with open("example_file.txt", "r") as f:
print(testmodule.test_read_stream(f))
)");
} catch (py::error_already_set& e) {
std::cerr << e.what() << "\n";
}
}
运行它会产生以下输出:
BytesIO:
One
Two
Three
StringIO:
Foo
Bar
Baz
String:
source_obj is not file-like
File:
TEST1
TEST2
TEST3
注意:由于
istream
固有的缓冲,它通常会以块的形式从Python对象中读取(例如,这里每次都要求4096字节)。如果您对流进行部分读取(例如,仅获取一行),您将需要显式地重新调整类文件的读取位置以反映实际消耗的字节数。
Pybind11 源代码包含一个写入 Python 流的输出
streambuf
实现。这可能是一个不错的入门灵感。
我设法在 GitHub 上的 BlueBrain/nmodl 存储库中找到了现有的
streambuf
输入实现。
您可以按以下方式使用它(可能作为用于分派 C++ 函数的包装器 lambda 的一部分):
py::object text_io_base = py::module::import("io").attr("TextIOBase");
if (py::isinstance(object, text_io_base)) {
py::detail::pythonibuf<py::str> buf(object);
std::istream istr(&buf);
return testReadStream(istr);
} else {
py::detail::pythonibuf<py::bytes> buf(object);
std::istream istr(&buf);
return testReadStream(istr);
}