模拟可调用对象

如果类有称作 __call__#[pymethod],那么它是可调用的。这允许类的实例表现得像函数一样。

这个方法的签名必须是__call__(<self>, ...) -> object

例子:实现一个可调用的计数器

下面的 pyclass 是一个基本的装饰器(构造器将一个Python对象作为参数,当被调用时调用该对象),本节最后有一个等价的Python实现的链接。

这里可以看到一个包含这个 pyclass 的示范包。

#![allow(unused)]
fn main() {
include ../../../examples/decorator/src/lib.rs
}

Python code:

#include ../../../examples/decorator/tests/example.py

Output:

say_hello has been called 1 time(s).
hello
say_hello has been called 2 time(s).
hello
say_hello has been called 3 time(s).
hello
say_hello has been called 4 time(s).
hello

纯Python实现

一个和Rust版本类似的Python实现

class Counter:
    def __init__(self, wraps):
        self.count = 0
        self.wraps = wraps

    def __call__(self, *args, **kwargs):
        self.count += 1
        print(f"{self.wraps.__name__} has been called {self.count} time(s)")
        self.wraps(*args, **kwargs)

注意到它也可以用高阶函数实现

def Counter(wraps):
    count = 0
    def call(*args, **kwargs):
        nonlocal count
        count += 1
        print(f"{wraps.__name__} has been called {count} time(s)")
        return wraps(*args, **kwargs)
    return call

Cell 什么?

previous implementation使用了u64,这表示它需要一个&mut self接收者(receiver)来更新计数

#![allow(unused)]
fn main() {
#[pyo3(signature = (*args, **kwargs))]
fn __call__(&mut self, py: Python<'_>, args: &PyTuple, kwargs: Option<&PyDict>) -> PyResult<Py<PyAny>> {
    self.count += 1;
    let name = self.wraps.getattr(py, "__name__")?;

    println!("{} has been called {} time(s).", name, self.count);

    // After doing something, we finally forward the call to the wrapped function
    let ret = self.wraps.call(py, args, kwargs)?;

    // We could do something with the return value of
    // the function before returning it
    Ok(ret)
}
}

这里的问题是&mut self这样的接收者意味着PyO3必须唯一地借用它,并在self.wraps.call(py, args, kwargs)调用中保持该借用。这个调用会将控制权返回给一个可以任意调用的Python代码,包括装饰器函数。如果这个发生了,PyO3就不能创建第二个唯一的借用并会抛出异常。

@Counter
def say_hello():
    if say_hello.count < 2:
        print(f"hello from decorator")

say_hello()
# RuntimeError: Already borrowed

本章给出的实现通过永远不做唯一的借用避免了这个错误,所有方法将&self作为接受者,all the methods take &self as receivers, of which multiple may exist simultaneously,这需要一个共享的计数器,而最简单的方式就是这里所使用的Cell

这显示了运行任意Python代码的危险,

  • Python 的异步执行器(asynchronous executor)可能在Python代码的中途挂起当前线程,即使是你在控制的Python代码,而运行其他Python代码
  • 丢弃任意的Python对象可能会引起(invoke)在Python中通过__del__ methods定义的自毁器(destructor)
  • 调用Python的 C-API(绝大多数PyO3 api在内部调用了 C-API)可能会抛出异常,可能让信号处理器(signal handler)中的Python代码运行

所以在写 unsafe 的代码时要格外慎重