There has been a lot of debate (at least on SO) about lack of const
-correctness and lack of true private
members in Python. I am trying to get used to the Pythonic way of thinking.
Suppose I want to implement a fuel tank. It has a capacity, it can be refilled, or fuel can be consumed out of it. So I would implement it as follows:
class FuelTank:
def __init__(self, capacity):
if capacity < 0:
raise ValueError("Negative capacity")
self._capacity = capacity
self._level = 0.0
@property
def capacity(self):
return self._capacity
@property
def level(self):
return self._level
def consume(self, amount):
if amount > self.level:
raise ValueError("amount higher than tank level")
self._level -= amount
def refill(self, amount):
if amount + self.level > self.capacity:
raise ValueError("overfilling the tank")
self._level += amount
So far I've put some level of const
-correctness in my code: by not implementing a property setter for capacity
I inform the client that capacity
cannot be changed after the object is constructed. (Though technically this is always possible by accessing _capacity
directly.) Similarly, I tell the client that you can read the level
but please use consume
or refill
methods to change it.
Now, I implement a Car
that has a FuelTank
:
class Car:
def __init__(self, consumption):
self._consumption = consumption
self._tank = FuelTank(60.0)
@property
def consumption(self):
return self._consumption
def run(self, kms):
required_fuel = kms * self._consumption / 100
if required_fuel > self._tank.level:
raise ValueError("Insufficient fuel to drive %f kilometers" %
kms)
self._tank.consume(required_fuel)
def refill_tank(self, amount):
self._tank.refill(amount)
Again I'm implying that client is not supposed to access _tank
directly. The only think (s)he can do is to refill_tank
.
After some time, my client complains that (s)he needs a way to know how much fuel is left in the tank. So, I decide to add a second method called tank_level
def tank_level(self):
return self._tank.level
Fearing that that a tank_capacity
will become necessary soon, I start to add wrapper methods in Car
to access all methods of FuelTank
except for consume
. This is obviously not a scalable solution. So, I can alternatively add the following @property
to Car
@property
def tank(self):
return self._tank
But now there is no way for the client to understand consume
method should not be called. In fact this implementation is only slightly safer than just making tank
a public attribute:
def __init__(self, consumption):
self._consumption = consumption
self.tank = FuelTank(60.0)
and saving extra lines of code.
So, in summary, I've got three options:
Car
for every method of FuelTank
that the client of Car
is allowed to use (not scalable and hard to maintain)._tank
(nominally) private and allowing client to access it as a getter-only property. This only protects me against an excessively 'idiot' client that may try to set tank
to a completely different object. But, otherwise is as good as making tank
public.tank
public, and asking client "please do not call Car.tank.consume
" I was wondering which option is considered as the best practice in a Pythonic world?
Note in C++ I would've made level
and capacity
methods const
in Tank
class and declared tank
as private member of Car
with a get_tank()
method that returns a const
-reference to tank
. This way, I would only need one wrapper method for refill
, and I give the client full access to any const
members of Tank
(with zero future maintenance cost). As a matter of taste, I find this an important feature that Python lacks.
Clarification.
I understand that what can be achieved in C++ is almost certainly impossible to achieve in Python (due to their fundamental differences). What I am mainly trying to figure out is which one of the three alternatives is the most Pythonic one? Does option (2) have any particular advantage over option (3)? Is there a way to make option (1) scalable?
Since Python doesn’t have any standard way of marking a method const
, there can’t be a built-in way of providing a value (i.e., an object) that restricts access to them. There are, however, two idioms that can be used to provide something similar, both made easier by Python’s dynamic attributes and reflection facilities.
If a class is to be designed to support this use case, you can split its interface: provide only the read interface on the “real” type, then provide a wrapper that provides the write interface and forwards any unknown calls to the reader:
class ReadFoo:
def __init__(self): self._bar=1
@property
def bar(self): return self._bar
class Foo:
def __init__(self): self._foo=ReadFoo()
def read(self): return self._foo
def more(self): self._foo._bar+=1
def __getattr__(self,n): return getattr(self._foo,n)
class Big:
def __init__(self): self._foo=Foo()
@property
def foo(self): return self._foo.read()
Note that Foo
does not inherit from ReadFoo
; another distinction between Python and C++ is that Python cannot express Base &b=derived;
, so we have to use a separate object. Neither can it be constructed from one: clients cannot then think they’re supposed to do so to obtain write access.
If the class isn’t designed for this, you can reverse the wrapping:
class ReadList:
def __init__(self,l): self._list=l
def __getattr__(self,n):
if n in ("append","extend","pop",…):
raise AttributeError("method would mutate: "+n)
return getattr(self._list,n)
This is obviously more work since you must make a complete blacklist (or whitelist, though then it’s a bit harder to make helpful error messages). If the class is cooperative, though, you could use this approach with tags (e.g., a function attribute) to avoid the explicit list and having two classes.