模拟可调用对象

如果类有称作 __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 的代码时要格外慎重