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 sensorclass 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: 35.716083298965245 #1 my-const: 36.5 #1 my-random: 34.458679593732505 #2 my-const: 36.5 #2 my-random: 38.21630333284501 #3 my-const: 36.5 #3 my-random: 35.186829992546556 #4 my-const: 36.5 #4 my-random: 34.88238733612109
Duck Typing: Examples¶
csv.reader
(here):csvfile can be any object which supports the iterator protocoland 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
expressesis-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 decoratorimport 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'