Abstract Base Class

Documentation

  1. Abstract base class in the Python Glossary
  2. abc module.
  3. contextlib.AbstractContextManager

Test if an object defines an interface

It’s easy to test if an object (in this case, li) is iterable: simply test if the object has an __iter__ method.

li = [10, 20, 30]

if hasattr(li, "__iter__") and callable(getattr(li, "__iter__")):
    print("li is iterable.")
else:
    print("li is not iterable.")
li is iterable.

It’s a bit harder to test if an object (in this case, it) is an iterator. We have to test if the object has two methods.

li = [10, 20, 30]
it = iter(li)

if hasattr(it, "__next__") and callable(getattr(it, "__next__")) and \
    hasattr(it, "__iter__") and callable(getattr(it, "__iter__")):
    print("it is an iterator.")
else:
    print("it is not an iterator.")
it is an iterator.

Similarly, to test if an object is a context manager, we have to test if the object has the methods __enter__ and __exit__. To test for other interfaces, we might have to test if the object has three or more methods.

An easier way to test an object

Duck-typing means checking whether an object suits your purposes by checking whether the object has the attributes (including the methods) you need. Instead of the above duck-typing, take advantage of the facts that class list is derived from the base class collections.abc.Iterable, and class list_iterator is derived from the base class collections.abc.Iterator,

import collections.abc   #abstract base classes for data types that are collections

li = [10, 20, 30]
it = iter(li)

if isinstance(li, collections.abc.Iterable):
    print("li is iterable.")
else:
    print("li is not iterable.")

if isinstance(it, collections.abc.Iterator):
    print("it is an iterator.")
else:
    print("it is not iterator.")
li is iterable.
it is an iterator.
import contextlib

infile = open("file.txt")

if isinstance(infile, contextlib.AbstractContextManager):
    print("infile is a context manager.")
else:
    print("infile is not a context manager.")
infile is a context manager.

Why you would want to test an object

import collections.abc

def myprint(x):
    "Print a variable of any data type."

    if isinstance(x, str) or not isinstance(x, collections.abc.Iterable):
        print(x)
    else:
        for item in x:   #Arrive here if x is any type of iterable except str
            print(item)

Derive your own classes from these abstract base classes.

We saw this module in Iterator. The advantages of inheriting from an abstract base class are:

  1. It’s easy to recognize whether an object is iterable or an iterator by using the above if isinstance.
  2. You would get an error message if you forgot to give an __iter__ method to your class range, or if you forgot to give a __next__ method to your class iterator. In the latter case, the error message would be
    TypeError: Can't instantiate abstract class iterator with abstract methods __next__”.
    (Of course, you would also get an error message if you misspelled the names of these methods.)
  3. You no longer have to write an __iter__ method for your class iterator. Your class iterator now inherits the __iter__ method from class collections.abc.Iterator.

"""
This module is float.py.
"""

import collections.abc

class range(collections.abc.Iterable):
    "A range of n+1 equally spaced floats, from start to end inclusive."

    def __init__(self, start, end, n):
        if not isinstance(start, int) and not isinstance(start, float):
            raise TypeError(f"start must be int or float, not {type(start)}")
        if not isinstance(end, int) and not isinstance(end, float):
            raise TypeError(f"end must be int or float, not {type(end)}")
        if end <= start:
            raise ValueError("start must be > end")
        if not isinstance(n, int):
            raise TypeError(f"n must be int, not {type(n)}")
        if n <= 0:
            raise ValueError(f"n must be posiive, not {n}")
        self.start = start
        self.end = end
        self.n = n

    def __iter__(self):
        return iterator(self.start, self.end, self.n)


class iterator(collections.abc.Iterator):
    def __init__(self, start, end, n):
        self.start = start
        self.end = end
        self.n = n
        self.i = 0

    def __next__(self):
        if self.i >= self.n + 1:
            raise StopIteration
        result = self.start + (self.end - self.start) * self.i / self.n
        self.i += 1
        return result


if __name__ == "__main__":
    import sys
    for f in range(0.0, 1.0, 10):   #the range we just defined here in float.py
        print(f)
    sys.exit(0)

Test out the module before you try to import it.

python3 -m float
import sys
import float

for i in range(10):                 #the range in the Python Standard Library
    print(i)

print()

for f in float.range(0.0, 1.0, 10): #the range we defined in float.py
    print(f)

sys.exit(0)
0
1
2
3
4
5
6
7
8
9

0.0
0.1
0.2
0.3
0.4
0.5
0.6
0.7
0.8
0.9
1.0