Accessors#
We do our best to expose coherent C++ and Python interfaces. Yet, the languages have some peculiarities that may present some pitfalls, one of which are accessors to mutable fields.
C++#
Given a C++ class A
with two private attributes (one mutable, one immutable) and public accessors to them
#include <string>
class MutableInt {
public:
MutableInt(int value) : _value(value) {}
void set(int value) {_value = value};
int get() const {return _value};
std::string repr() const {
return "MutableInt(" + std::to_string(_value) + ")";
}
private:
int _value;
}
class A {
A() : _i(0), _mi(0) {}
// returns a copy;
int get_i() const { return _i; }
void set_i(int value) { _i = value; }
// returns a copy;
MutableInt get_mi() const { return _mi; }
// returns a reference;
MutableInt & get_mi_ref() { return _mi; }
void set_mi(const MutableInt & value) { _mi = value; }
private:
// immutable type;
int _i;
// mutable type;
MutableInt _mi;
};
A a;
there are two ways to modify the attributes:
setting new values
a.set_i(2); a.set_mi(MutableInt{2});
call a modifier on the mutable attribute,
a.get_mi_ref().set(2);
provided that the class does expose an accessors that returns a reference.
Note that accessors that returns a reference have the further effect to track changes:
auto mi = a.get_mi();
auto & mi_ref = a.get_mi_ref();
a.set_mi(MutableInt{3});
now mi_ref
is equal to Mutable(3)
, while mi
is still equal to Mutable(2)
as it has been copied.
Pybind11#
When these accessors are exposed as Python properties (or as Python methods), e.g. through
#include <pybind11/pybind11.h>
#include <string>
namespace py = pybind11;
PYBIND11_MODULE(my_module, m) {
py::class_<MutableInt>(m, "MutableInt")
.def(py::init<int>())
.def("set", &MutableInt::get)
.def("get", &MutableInt::set)
.def("__repr__", &MutableInt::repr);
py::class_<A>(m, "A")
.def(py::init<int>())
.def_property("i", &A::get_i, &A::set_i)
.def_property("mi", &A::get_mi, &A::set_mi)
.def_property("mi_ref", &A::get_mi_ref, nullptr);
some methods do not modify A
as a Python user may instead expect (see below)
from my_module import A, MutableInt
a = A(1)
do modify
a
:>>> a.i = 2 >>> a.mi = MutableInt(2) >>> a.i, a.mi 2, MutableInt(2) >>> a.mi_ref.set(3) >>> a.mi MutableInt(3)
does not modify
a
asa.mi
returns a copy:>>> a.mi.set(4) >>> a.mi MutableInt(3)
Python#
The equivalent Python class, implemented like
class PyA:
def __init__(self):
self.i = 0
self.mi = MutableInt(0)
py_a = PyA()
uses references to holds/pass objects, copying only if asked explicitly, for instances using a property like
@property
def mi_copy(self):
return MutableInt(self.mi.get())
Therefore both methods to modify the (mutable) attribute are available:
setting a value
>>> py_a.i = 2 >>> py_a.mi = MutableInt(2) >>> i, py_a.mi 2, MutableInt(2)
modifying the existing value
>>> py_a.mi.set(3) MutableInt(3)
This difference may confuse users, as they may consider the classes A
and PyA
to be equivalent, as they expose the same interface (ignoring A.mi_ref
).
py_a = PyA()
# this has an effect
py_a.mi.set(2)
a = A()
# while this does not
a.mi.set(2)
Note that this difference does not apply to immutable attributes.