Emulating callable objects
Classes can be callable if they have a #[pymethod]
named __call__
.
This allows instances of a class to behave similar to functions.
This method's signature must look like __call__(<self>, ...) -> object
- here,
any argument list can be defined as for normal pymethods
Example: Implementing a call counter
The following pyclass is a basic decorator - its constructor takes a Python object as argument and calls that object when called. An equivalent Python implementation is linked at the end.
An example crate containing this pyclass can be found here
#![allow(unused)] fn main() { use pyo3::prelude::*; use pyo3::types::{PyDict, PyTuple}; /// A function decorator that keeps track how often it is called. /// /// It otherwise doesn't do anything special. #[pyclass(name = "Counter")] pub struct PyCounter { // We use `#[pyo3(get)]` so that python can read the count but not mutate it. #[pyo3(get)] count: u64, // This is the actual function being wrapped. wraps: Py<PyAny>, } #[pymethods] impl PyCounter { // Note that we don't validate whether `wraps` is actually callable. // // While we could use `PyAny::is_callable` for that, it has some flaws: // 1. It doesn't guarantee the object can actually be called successfully // 2. We still need to handle any exceptions that the function might raise #[new] fn __new__(wraps: Py<PyAny>) -> Self { PyCounter { count: 0, wraps } } #[args(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) } } #[pymodule] pub fn decorator(_py: Python, module: &PyModule) -> PyResult<()> { module.add_class::<PyCounter>()?; Ok(()) } }
Python code:
@Counter
def say_hello():
print("hello")
say_hello()
say_hello()
say_hello()
say_hello()
assert say_hello.count == 4
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
Pure Python implementation
A Python implementation of this looks similar to the Rust version:
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)
Note that it can also be implemented as a higher order function:
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