Abstract Base Classes (abc), And Duck Typing

  • Python is not picky about types

  • Very late binding

  • By name

  • Method call on object ⟶ lookup in class dict

Duck Typing

  • If it walks and quacks like a duck, it can be used as a duck

  • Concrete example: Sensor-like classes (mockups)

  • Sensor-like: if you can call get_temperature() on it, it is a sensor

    class ConstantSensor:
        def __init__(self, value):
            self.value = value
    
        def get_temperature(self):
            return self.value
    
    import random
    class RandomSensor:
        def __init__(self, lo, hi):
            self.lo = lo
            self.hi = hi
    
        def get_temperature(self):
            return random.uniform(self.lo, self.hi)
    
  • Any of those can be used as-a sensor

    sensors = {
        'my-const': ConstantSensor(36.5),
        'my-random': RandomSensor(34.3, 41.7),
    }
    
    for i in range(5):
        for name, s in sensors.items():
            temperature = s.get_temperature()         # <--- using a duck
            print(f'#{i} {name}: {temperature}')
    
    #0 my-const: 36.5
    #0 my-random: 36.94746223399823
    #1 my-const: 36.5
    #1 my-random: 41.34267386833059
    #2 my-const: 36.5
    #2 my-random: 34.834327667802604
    #3 my-const: 36.5
    #3 my-random: 38.29030843094829
    #4 my-const: 36.5
    #4 my-random: 35.8712316652917
    

Duck Typing: Examples

  • csv.reader (here):

    csvfile can be any object which supports the iterator protocol
    and returns a string each time its __next__() method is called —
    file objects and list objects are both suitable.

Duck Typing Problem: Late Errors

  • A broken duck

    class BrokenSensor:
        def getTemperature(self):               # <--- broken, should be get_temperature()
            return -273.15
    
  • Program setup instantiates object

    sensors = {
        'my-const': ConstantSensor(36.5),
        'my-broken': BrokenSensor(),            # <--- instantiate
    }
    
  • Much later, during regular operation

    for i in range(5):
        for name, s in sensors.items():
            temperature = s.get_temperature()   # <--- non-duck breaks program
            print(f'#{i} {name}: {temperature}')
    
    #0 my-const: 36.5
    
    ---------------------------------------------------------------------------
    AttributeError                            Traceback (most recent call last)
    Cell In[5], line 3
          1 for i in range(5):
          2     for name, s in sensors.items():
    ----> 3         temperature = s.get_temperature()   # <--- non-duck breaks program
          4         print(f'#{i} {name}: {temperature}')
    
    AttributeError: 'BrokenSensor' object has no attribute 'get_temperature'
    

Intermediate Step: Common Base Class (“Interface”)

  • Other languages have interfaces

  • Java: implements expresses is-a

  • Python: only inheritance, and a bunch of metaprogramming possibilities

  • ⟶ create base class Sensor

    class Sensor:
        def get_temperature(self):
            assert False, "implement this in a derived class!"
            return -273.5   # implementations should return float
    
  • And derive concrete sensors from it

    class ConstantSensor(Sensor):
        def __init__(self, value):
            self.value = value
    
        def get_temperature(self):
            return self.value
    
    class BrokenSensor(Sensor):
        def getTemperature(self):               # <--- still broken
            return -273.15
    
  • Instantiation still possible

    sensors = {
        'my-const': ConstantSensor(36.5),
        'my-broken': BrokenSensor(),            # <--- still passes
    }
    
  • Different runtime error, but still during regular operation

    for i in range(5):
        for name, s in sensors.items():
            temperature = s.get_temperature()   # <--- still not a duck
            print(f'#{i} {name}: {temperature}')
    
    #0 my-const: 36.5
    
    ---------------------------------------------------------------------------
    AssertionError                            Traceback (most recent call last)
    Cell In[9], line 3
          1 for i in range(5):
          2     for name, s in sensors.items():
    ----> 3         temperature = s.get_temperature()   # <--- still not a duck
          4         print(f'#{i} {name}: {temperature}')
    
    Cell In[6], line 3, in Sensor.get_temperature(self)
          2 def get_temperature(self):
    ----> 3     assert False, "implement this in a derived class!"
          4     return -273.5
    
    AssertionError: implement this in a derived class!
    

Enter Abstract Base Classes: Wish List

  • True is-a relationship ⟶ inheritance, only stronger

  • No non-compliant objects should be possible

  • ⟶ Error (“not a duck”) should happen as early as possible

  • ⟶ At instantiation!

  • Enter abc

Abtract Base Class

  • abc.ABC: Abstract base class to inherit from

  • @abc.abstractmethod: method decorator

    import abc
    
    class Sensor(abc.ABC):
        @abc.abstractmethod
        def get_temperature(self):
            return -273.5   # implementations should return float
    
  • Derived classes unmodified

    class ConstantSensor(Sensor):
        def __init__(self, value):
            self.value = value
    
        def get_temperature(self):    # <--- good: overriding abstract method
            return self.value
    
    class BrokenSensor(Sensor):
        def getTemperature(self):     # <--- bad: not overriding abstract method
            return -273.15
    
  • Effect: ABC objects cannot be instantiated if they have unimplemented @abc.abstractmethod methods …

    sensors = {
        'my-const': ConstantSensor(36.5),
        'my-broken': BrokenSensor(),  # <--- good: early error
    }
    
    ---------------------------------------------------------------------------
    TypeError                                 Traceback (most recent call last)
    Cell In[12], line 3
          1 sensors = {
          2     'my-const': ConstantSensor(36.5),
    ----> 3     'my-broken': BrokenSensor(),  # <--- good: early error
          4 }
    
    TypeError: Can't instantiate abstract class BrokenSensor without an implementation for abstract method 'get_temperature'