[英]PyBind11 destructor not invoked?
我有一个c++
包裹的PyBind11
。 问题是:当Python
脚本结束时,不会自动调用c++
destructor
。 这会导致不整洁的退出,因为网络资源需要由析构函数释放。
作为解决方法,有必要明确删除Python
object,但我不明白为什么!
请有人解释这里出了什么问题,以及如何在Python
object 被垃圾收集时自动调用destructor
?
Pybind11绑定代码:
py::class_<pcs::Listener>(m, "listener")
.def(py::init<const py::object &, const std::string &, const std::string &, const std::string &, const std::string &, const std::set<std::string> &, const std::string & , const bool & , const bool & >(), R"pbdoc(
Monitors network traffic.
When a desired data source is detected a client instance is connected to consume the data stream.
Reconstructs data on receipt, like a jigsaw. Makes requests to fill any gaps. Verifies the data as sequential.
Data is output by callback to Python. Using the method specified in the constructor, which must accept a string argument.
)pbdoc");
在 Python:
#Function to callback
def print_string(str):
print("Python; " + str)
lstnr = listener(print_string, 'tcp://127.0.0.1:9001', clientCertPath, serverCertPath, proxyCertPath, desiredSources, 'time_series_data', enableCurve, enableVerbose)
#Run for a minute
cnt = 0
while cnt < 60:
cnt += 1
time.sleep(1)
#Need to call the destructor explicity for some reason
del lstnr
正如评论中提到的,这种行为的直接原因是 Python 垃圾收集器:当对象的引用计数器变为零时,垃圾收集器可能会销毁该对象(从而调用 c++ 析构函数),但它不会必须在那一刻去做。
这个想法在这里的答案中得到了更全面的阐述:
https://stackoverflow.com/a/38238013/790979
正如在上面的链接中也提到的,如果您在 Python 中的对象生命周期结束时进行清理,一个不错的解决方案是上下文管理,您可以在对象的包装器中定义__enter__
和__exit__
(在 pybind11 中)或在 Python 本身中),让__exit__
释放网络资源,然后在 Python 客户端代码中,类似于:
with listener(print_string, 'tcp://127.0.0.1:9001', clientCertPath, serverCertPath, proxyCertPath, desiredSources, 'time_series_data', enableCurve, enableVerbose) as lstnr:
# Run for a minute
cnt = 0
while cnt < 60:
cnt += 1
time.sleep(1)
所以几年后,我通过在我的PyBind11代码中添加__enter__
和__exit__
方法处理来启用Python 上下文管理器with
解决这个问题:
py::class_<pcs::Listener>(m, "listener")
.def(py::init<const py::object &, const std::string &, const std::string &, const std::string &, const std::string &, const std::set<std::string> &, const std::string & , const bool & , const bool & >(), R"pbdoc(
Monitors network traffic.
When a desired data source is detected a client instance is connected to consume the data stream.
Specify 'type' as 'string' or 'market_data' to facilitate appropriate handling of BarData or string messages.
Reconstructs data on receipt, like a jigsaw. Makes requests to fill any gaps. Verifies the data as sequential.
Data is output by callback to Python. Using the method specified in the constructor, which must accept a string argument.
)pbdoc")
.def("__enter__", &pcs::Listener::enter, R"pbdoc(
Python 'with' context manager support.
)pbdoc")
.def("__exit__", &pcs::Listener::exit, R"pbdoc(
Python 'with' context manager support.
)pbdoc");
给C++ class增加了相应的函数,如下:
//For Python 'with' context manager
auto enter(){std::cout << "Context Manager: Enter" << std::endl; return py::cast(this); }//returns a pointer to this object for 'with'....'as' python functionality
auto exit(py::handle type, py::handle value, py::handle traceback){ std::cout << "Context Manager: Exit: " << type << " " << value << " " << traceback << std::endl; }
注意事项
从enter()
返回的指针值对于with
.... as
语句中的as
功能很重要。
传递给exit(py::handle type, py::handle value, py::handle traceback)
的参数是有用的调试信息。
Python 用法:
with listener(cb, endpoint, clientCertPath, serverCertPath, proxyCertPath, desiredSources, type, enableCurve, enableVerbose):
cnt = 0
while cnt < 10:
cnt += 1
time.sleep(1)
Python 上下文管理器现在调用 C++ object 上的析构函数,从而顺利释放网络资源。
GoFaster 的上述解决方案很有帮助且是正确的方法,但我只是想澄清并纠正他们的断言
Python 上下文管理器现在调用 C++ object 上的析构函数,从而顺利释放.networking 资源
这是不正确的。 上下文管理器只保证调用__exit__
,而不保证调用任何析构函数。 让我演示一下——这是在 C++ 中实现的托管资源:
class ManagedResource
{
public:
ManagedResource(int i) : pi(std::make_unique<int>(i))
{
py::print("ManagedResource ctor");
}
~ManagedResource()
{
py::print("ManagedResource dtor");
}
int get() const { return *pi; }
py::object enter()
{
py::print("entered context manager");
return py::cast(this);
}
void exit(py::handle type, py::handle value, py::handle traceback)
{
// release resources
// pi.reset();
py::print("exited context manager");
}
private:
std::unique_ptr<int> pi;
};
python 绑定:
py::class_<ManagedResource>(m, "ManagedResource")
.def(py::init<int>())
.def("get", &ManagedResource::get)
.def("__enter__", &ManagedResource::enter, R"""(
Enter context manager.
)""")
.def("__exit__", &ManagedResource::exit, R"""(
Leave context manager.
)""");
和一些 python 测试代码(请注意,上面的代码尚未(尚未)释放__exit__
中的资源):
def f():
with ManagedResource(42) as resource1:
print(f"get = {resource1.get()}")
print(f"hey look I'm still here {resource1.get()}") # not destroyed
if __name__ == "__main__":
f()
print("end")
产生:
ManagedResource ctor
entered context manager
get = 42
exited context manager
hey look I'm still here 42
ManagedResource dtor
end
因此构建资源,获取 memory,并在上下文管理器中访问。 到目前为止一切都很好。 然而, memory 仍然可以在上下文管理器之外访问(并且在调用析构函数之前,这由 python 运行时决定并且在我们的控制之外,除非我们用del
强制它,这完全违背了上下文管理器的要点。
但是我们实际上并没有释放__exit__
中的资源。 如果您在 function 中取消pi.reset()
的注释,您将得到:
ManagedResource ctor
entered context manager
get = 42
exited context manager
Segmentation fault (core dumped)
这一次,当你在上下文管理器之外调用get()
时, ManagedResource
object 本身仍然非常没有被破坏,但是它里面的资源已经被释放了,
还有更多的危险:如果你在with
块之外创建一个ManagedResource
,你将泄漏资源,因为__exit__
永远不会被调用。 要解决此问题,您需要推迟从构造函数获取资源到__enter__
方法,并检查资源是否存在于get
中。
简而言之,这个故事的寓意是:
__enter__
方法中获取,而不是在构造函数中__exit__
方法中释放,而不是析构函数上下文管理的 object 本身不是 RAII 资源,而是 RAII 资源的包装器。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.