Custom setters and getters
In a previous section, we learned how to attach the default getter and setter to a field in a #[pyclass]:
#![allow(unused)] fn main() { use pyo3::prelude::*; #[pyclass] struct Wallet { #[pyo3(get, set)] balance: i32, } }
This is convenient, but it's not always desirable!
Let's introduce an additional constraint to our Wallet struct: the balance should never go below a pre-determined
overdraft threshold.
We'd start by enforcing this constraint in the constructor method:
#![allow(unused)] fn main() { use pyo3::prelude::*; use pyo3::exceptions::PyValueError; #[pyclass] struct Wallet { #[pyo3(get, set)] balance: i32, } const OVERDRAFT_LIMIT: i32 = -100; #[pymethods] impl Wallet { #[new] fn new(balance: i32) -> PyResult<Self> { if balance < OVERDRAFT_LIMIT { return Err(PyValueError::new_err("Balance cannot be below overdraft limit")); } Ok(Wallet { balance }) } } }
Wallet::new ensures that a newly-created Wallet upholds the overdraft constraint. But the default setter
can be easily used to circumvent the limit:
wallet = Wallet(0)
wallet.balance = -200 # This should not be allowed, but it is!
#[setter] and #[getter]
We can override the default getter and setter by defining custom methods for them.
Here's how we can implement a custom setter for the balance field via the #[setter] attribute:
#![allow(unused)] fn main() { use pyo3::prelude::*; #[pyclass] struct Wallet { // We keep using the default getter, no issues there #[pyo3(get)] balance: i32, } const OVERDRAFT_LIMIT: i32 = -100; #[pymethods] impl Wallet { #[new] fn new(balance: i32) -> PyResult<Self> { Wallet::check_balance(balance)?; Ok(Wallet { balance }) } #[setter] fn set_balance(&mut self, value: i32) { Wallet::check_balance(value)?; self.balance = value; } } impl Wallet { // We put this method in a separate `impl` block to avoid exposing it to Python fn check_balance(balance: i32) -> PyResult<()> { if balance < OVERDRAFT_LIMIT { return Err(PyValueError::new_err("Balance cannot be below overdraft limit")); } Ok(()) } } }
Every time the balance field is set in Python, Wallet::set_balance will be called:
wallet = Wallet(0)
wallet.balance = -200 # Now raises a `ValueError`
The field is associated with its setter using a conventional naming strategy for the setter method: set_<field_name>.
You can also explicitly specify the field name in the #[setter] attribute, like this: #[setter(balance)].
Custom getters are defined in a similar way using the #[getter] attribute. The naming convention for
getter methods is <field_name>, but you can also specify the field name explicitly in the attribute—e.g.
#[getter(balance)].
Exercise
The exercise for this section is located in 02_classes/03_setters