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:
- Writing a wrapper method in
Carfor every method ofFuelTankthat the client ofCaris allowed to use (not scalable and hard to maintain). - Keeping
_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 settankto a completely different object. But, otherwise is as good as makingtankpublic. - Making
tankpublic, and asking client "please do not callCar.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:
Note that
Foodoes not inherit fromReadFoo; another distinction between Python and C++ is that Python cannot expressBase &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:
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.