【问题标题】:Writing a pyo3 function equivalent to a Python function that returns its input object编写一个等效于 Python 函数的 pyo3 函数,该函数返回其输入对象
【发布时间】:2019-03-03 10:55:44
【问题描述】:

我正在为我的库编写一个 Rust 后端,我需要在 pyo3 中实现以下功能的等效项:

def f(x):
    return x

这应该返回与输入相同的对象,并且获得返回值的函数应该持有对输入的新引用。如果我在 C API 中写这个,我会写成:

PyObject * f(PyObject * x) {
    Py_XINCREF(x);
    return x;
}

PyO3 中,我发现在PyObjectPyObjectRef&PyObjectPy<PyObject>Py<&PyObject> 之间的差异非常令人困惑。

这个函数最幼稚的版本是:

extern crate pyo3;

use pyo3::prelude::*;

#[pyfunction]
pub fn f(_py: Python, x: &PyObject) -> PyResult<&PyObject> {
    Ok(x)
}

除此之外,x 的生命周期和返回值不一样,而且我认为 pyo3 没有机会增加 x 的引用计数,实际上编译器似乎同意我:

error[E0106]: missing lifetime specifier
 --> src/lib.rs:4:49
  |
4 | pub fn f(_py: Python, x: &PyObject) -> PyResult<&PyObject> {
  |                                                 ^ expected lifetime parameter
  |
  = help: this function's return type contains a borrowed value, but the signature does not say whether it is borrowed from `_py` or `x`

我可能有办法手动使用_py参数增加引用计数并使用生命周期注解让编译器高兴,但我的印象是pyo3打算管理使用对象生命周期的引用计数自身

编写此函数的正确方法是什么?我应该尝试将其包装在 Py 容器中吗?

【问题讨论】:

  • @Shepmaster 我认为您的编辑使标题不太准确。引用计数增加的事实不是我想在 Rust 版本中管理或考虑的事情。如果 pyo3 做了其他相当于增加引用计数的事情,那很好。
  • @Shepmaster 这并不是一个真正的硬性要求。如果pyo3 保留对创建的任何 Python 对象的单个引用,然后使用 Rust 对象生命周期或其他方式管理是否释放该引用,那很好。我真的不知道也不关心,只要我从 pyo3 的角度使用正确的生命周期管理。
  • x 的生命周期和返回值不一样——你为什么这么说? Lifetime elision 使它们相同。您可以明确地注释它们以“证明”它:fn f&lt;'a&gt;(_py: Python, x: &amp;'a PyObject) -&gt; PyResult&lt;&amp;'a PyObject&gt;.
  • @Shepmaster 这样做似乎无论如何都会导致编译器错误,因为pyfunc 没有保留生命周期参数。
  • 至于生命周期问题,这是编译器错误的要点——这可能是@E_net4 所说的问题,但我认为 Python 引用计数的重点是输入的生命周期并且输出应该是断开的(因此引用计数),所以即使 Rust 语义说它具有正确的生命周期声明,这不是函数的预期使用方式。

标签: python rust pyo3


【解决方案1】:

PyObjecta simple wrapper around a raw pointer

pub struct PyObject(*mut ffi::PyObject);

它有多个创建函数,每个函数对应于我们可能从 Python 获得的不同类型的指针。其中一些,例如from_borrowed_ptr,在传入的指针上调用Py_INCREF

因此,我们似乎可以接受PyObject,只要它是以“正确”的方式创建的。

如果我们expand这个代码:

#[pyfunction]
pub fn example(_py: Python, x: PyObject) -> PyObject {
    x
}

我们可以看到调用我们函数的这段代码:

let mut _iter = _output.iter();
::pyo3::ObjectProtocol::extract(_iter.next().unwrap().unwrap()).and_then(
    |arg1| {
        ::pyo3::ReturnTypeIntoPyResult::return_type_into_py_result(example(
            _py, arg1,
        ))
    },
)

我们的参数是通过调用ObjectProtocol::extract 创建的,而后者又调用FromPyObject::extract。这是implemented for PyObject,请致电from_borrowed_ptr

因此,使用裸 PyObject 作为参数类型将正确地增加引用计数。

同样,当 PyObject 在 Rust 中被删除时,它会自动 decrease the reference count。当它返回给 Python 时,ownership is transferred 并由 Python 代码来适当地更新引用计数。


从 master 分支对 commit ed273982 进行的所有调查,对应于 v0.5.0-alpha.1。

【讨论】:

    【解决方案2】:

    根据to the other answerpyo3 负责围绕我们的函数构建额外的样板,以跟踪 Python 引用计数。特别是,当将对象作为参数传递给函数时,计数器已经递增。不过,clone_ref 方法可用于显式创建对同一对象的新引用,这也会增加其引用计数器。

    函数的输出必须仍然是一个实际的 Python 对象,而不是对它的引用(这似乎是合理的,因为 Python 不理解 Rust 引用;pyo3 似乎忽略了这些函数中的生命周期参数)。

    #[pyfunction]
    fn f(py: Python, x: PyObject) -> PyResult<PyObject> {
        Ok(x.clone_ref(py))
    }
    

    在 Python 领域(AKA 不是一个严肃的测试平台)中使用该函数,它至少似乎按预期工作。

    from dummypy import f
    
    def get_object():
        return f("OK")
    
    a = [1, 2, 3]
    
    if True:
        b = f(a)
        assert b is a
        b[0] = 9001
    
    print(a)
    
    x = get_object()
    print(x)
    

    【讨论】:

    • 最简单的测试是调用a = object(); assert f(a) is a,因为重要的部分是输入应该是与输出相同的对象。
    • @Paul 我想也可以添加。但我主要是尝试检查引用计数是否与指向对象的变量数一致。
    猜你喜欢
    • 1970-01-01
    • 2019-10-18
    • 2019-10-27
    • 2021-12-18
    • 2012-12-25
    • 2019-11-04
    • 2012-07-17
    • 2016-12-13
    • 1970-01-01
    相关资源
    最近更新 更多