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