为具有通用 Rust 类型的 Rust 函数实现 Python 接口

Implementing a Python interface for a Rust function with generic Rust type

提问人:DrStrangeLove 提问时间:11/15/2023 最后编辑:DrStrangeLove 更新时间:11/15/2023 访问量:61

问:

这个函数在 Rust 上完美运行:

fn jaccard_similarity<T>(s1: Vec<T>, s2: Vec<T>) -> f32
where
    T: Hash + Eq + Clone,
{
    let s1 = vec_to_set(&s1);
    let s2 = vec_to_set(&s2);
    let i = s1.intersection(&s2).count() as f32;
    let u = s1.union(&s2).count() as f32;
    return i / u;
}

fn vec_to_set<T>(vec: &Vec<T>) -> HashSet<T>
where
    T: Hash + Eq + Clone,{
    HashSet::from_iter(vec.iter().cloned())
}

在以下测试用例上:

#[test]
fn test_jaccard_similarity() {
    let left = vec!["kitten", "sitting", "saturday", "sunday"];
    let right = vec!["kitten", "sitting", "saturday", "sunday"];
    assert_eq!(jaccard_similarity(left, right), 1.0);
    let left = vec![1,2,3,4];
    let right = vec![1,2,3,4];
    assert_eq!(jaccard_similarity(left, right), 1.0);
    let left = vec![1,2,3,4];
    let right = vec![2,2,3,4];
    assert_eq!(jaccard_similarity(left, right), 0.75);

}

但是,一旦我将其包装为 pyo3 crate [版本:0.13.2] 的 #[pyfunction](并且我还更新了我的 lib.rs 和 mod.rs 文件)。对于上下文,我正在使用 Maturin 库。

#[pyfunction]
fn jaccard_similarity<T>(s1: Vec<T>, s2: Vec<T>) -> f32
where
    T: Hash + Eq + Clone,
{
    let s1 = vec_to_set(&s1);
    let s2 = vec_to_set(&s2);
    let i = s1.intersection(&s2).count() as f32;
    let u = s1.union(&s2).count() as f32;
    return i / u;
}

我收到以下错误:

--> src\distance_functions\jaccard_similarity.rs:6:4
  |
6 | fn jaccard_similarity<T>(s1: Vec<T>, s2: Vec<T>) -> f32
  |    ^^^^^^^^^^^^^^^^^^ cannot infer type of the type parameter `T` declared on the function `jaccard_similarity`
  |
  = note: cannot satisfy `_: Hash`
note: required by a bound in `jaccard_similarity`
 --> src\distance_functions\jaccard_similarity.rs:8:8
  |
6 | fn jaccard_similarity<T>(s1: Vec<T>, s2: Vec<T>) -> f32
  |    ------------------ required by a bound in this function
7 | where
8 |     T: Hash + Eq + Clone,
  |        ^^^^ required by this bound in `jaccard_similarity`
help: consider specifying the generic argument
  |
6 | fn jaccard_similarity::<T><T>(s1: Vec<T>, s2: Vec<T>) -> f32
  |                      +++++

泛型参数已在函数上声明。我无法理解编译器要求我做什么。

当 Rust 代码被包装在 Python 接口中时,在 Rust 中工作的内容也应该如此。

编辑:我将pyo3版本更新为0.20.0,现在我收到更有意义的错误消息:

error: Python functions cannot have generic type parameters
 --> src\distance_functions\jaccard_similarity.rs:6:23
  |
6 | fn jaccard_similarity<T>(s1: Vec<T>, s2: Vec<T>) -> f32

有没有办法对 Python 函数使用泛型类型参数?

PYO3型 马图林

评论

5赞 Sven Marnach 11/15/2023
如果我编译您的确切代码,我会收到错误“Python 函数不能具有泛型类型参数”,而不是您在问题中显示的错误消息。(Rust 1.73.0,pyo3 0.20.0)
0赞 DrStrangeLove 11/15/2023
感谢您指出这一点。我使用的是 pyo3 版本 0.13.2。我更新了它,现在我收到一条有意义的错误消息。还更新了问题

答:

7赞 Masklinn 11/15/2023 #1

当 Rust 代码被包装在 Python 接口中时,在 Rust 中工作的内容也应该如此。

不。Python 不是静态类型的,python 接口不支持泛型,因此 pyo3 无法创建桥接函数和 Rust 实现的绑定。

事实上,这与 Rust 本身的行为相匹配:它本身实际上不会生成任何代码。相反,编译器将查看调用站点,并为调用函数的每个实例生成一个实例,这些实例是最终进入二进制文件的代码。实例化步骤是 pyo3 无法实现的步骤,因此它无法工作。jaccard_similarityT

我还想说这段代码从根本上没有用,python 等价物是用 C 实现的 5 次调用(创建两个集合,将它们相交,将它们联合起来并除以)。将数据复制到向量,然后设置这些向量的开销可能与让 python 做这件事一样大。尤其是 Rust 的默认哈希函数。

为了获得任何实际收益的机会,我认为您可能需要完全避免这两个 vec(从 PyList 动态执行转换)并避免重新化其中一个集合(可能是更大的集合)。