Search code examples
pythonpython-3.xconst-correctnessinformation-hiding

What is the Pythonic equivalent of const-references?


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:

  1. Writing a wrapper method in Car for every method of FuelTank that the client of Car is allowed to use (not scalable and hard to maintain).
  2. 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 set tank to a completely different object. But, otherwise is as good as making tank public.
  3. Making 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?


Solution

  • 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.