As we inch towards more Pythonic interfaces, we’re going to repeatedly encounter problems in which a property needs to expose an immutable view to a mutable object.
For instance, let’s imagine that we have a Point
class with x
and y
get/set properties, and a Box
class with a get-only max
property that returns a new Point
(perhaps because it’s computed from the minimum point and the dimensions, rather than held by the box). This situation leads to the following surprising behavior:
box = Box(Point(2, 3), Point(5, 6))
box.max.x = 1 # silently does nothing
This is a problem intrinsic to Python, and I think it’s one of the reasons many of Python’s built-in types (str
, complex
) are immutable, and part of why there are immutable versions of other types (set
and frozenset
).
We already have this same problem with our getters, of course, but getters at least somewhat imply that the returned object is a copy; to my eyes, the following doesn’t read like something that should be automatically expected to work, even though it has the same behavior (and the same syntax would work for other getters):
box.getMax().setX(1) # also silently does nothing, but maybe that's not as bad?
I think we have a few options here:
-
Ignore this problem, aside from documenting it everywhere we can. This would be a horrible sin in C++, but maybe it’s just one part of Python’s “you can’t expect the language to stop you from shooting yourself in the foot” philosophy?
-
Make all of our small, frequently-used classes (like both
Point
andBox
) immutable in Python, turning those silent failures into helpful exceptions. Of course, this comes at the expense of never being able to modify aPoint
orBox
in-place in Python, which might be a significant inconvenience in other code. -
Make immutable and mutable versions of all of our small, frequently-used classes. That’s a bit more work in the wrappers, and it still leaves one edge case that’s slightly confusing:
point = box.max # this returns FrozenPoint, but we probably wanted a copy ... # lots of other code, in which we forget where "point" came from point.x = 1 # this now throws an exception
I think I have a slight preference for (3), at least for classes as ubiquitous as Point
and Box
, but my mind is really not made up. For complex classes like Psf
and Wcs
that are nevertheless held frequently by other objects, I’d lean towards (2).
Does anyone else have an opinion on this, or some wisdom from other Python projects?