Tags: #python #interfaces #protocols #design #collections #abc #iterator #sized #metaclasses #abstract
Python is a strongly typed dynamic language, and so it has no support for the interface
keyword.
Note: some languages are weakly typed (JavaScript), some are strongly typed (Python) and some are statically typed (Go, Rust). Being strongly typed means you can’t perform operations inappropriate to the type, so for example: in Python you can’t add a number typed variable with a string typed variable.
Instead, Python provides ‘protocols’ which are a bit like the interface support in Go. They’re not strictly enforced, but if you implement specific magic methods you’ll find a selection of builtin Python functions become available to use on objects they otherwise wouldn’t necessarily support.
This code doesn’t work:
class Team:
def __init__(self, members):
self.members = members
t = Team(['foo', 'bar', 'baz'])
t.members # ['foo', 'bar', 'baz']
len(t) # TypeError: object of type 'Team' has no len()
But if we implement the __len__
magic method, we are now telling Python that we support the Sized
protocol:
class Team:
def __init__(self, members):
self.members = members
def __len__(self):
return len(self.members)
t = Team(['foo', 'bar', 'baz'])
t.members # ['foo', 'bar', 'baz']
len(t) # 3
There are many different protocols, such as: collections.abc.Iterator
which if we were to implement the __iter__
and __next__
magic methods, then we’d be able to use a for
loop construct on our object:
class Team:
def __init__(self, members):
self.members = members
def __iter__(self, max=0):
self.n = 0
return self
def __next__(self):
if self.n < len(self.members):
index = self.n
self.n += 1
return self.members[index]
else:
raise StopIteration
t = Team(['foo', 'bar', 'baz'])
for member in t:
print(f't member: {member}')
t member: foo
t member: bar
t member: baz
Protocols are useful but sometimes you require something that does indeed behave more like a traditional ‘interface’, and that’s where ABCs can help us.
They are best explained by way of an example:
import abc
class Foo(abc.ABC):
@abc.abstractmethod
def bar(self):
pass
class Thing(Foo):
pass
t = Thing() # TypeError: Can't instantiate abstract class Thing with abstract methods bar
Note: we’re subclassing directly from
abc.ABC
where that parent class is settingabc.ABCMeta
as a metaclass on itself (e.g. something likeclass ABC(metaclass=ABCMeta)
) and so we could do that directly with ourFoo
class like so:class Foo(metaclass=abc.ABCMeta)
. Read below for more information on metaclasses.
So we can see from the above example code that we’ve enforced the Foo
‘interface’ onto the Thing
object. If we want Thing
to truly be a type of Foo
, then it’ll need to provide a concrete implementation of the bar
method that the Foo
‘interface’ has defined, like so:
import abc
class Foo(abc.ABC):
@abc.abstractmethod
def bar(self):
pass
class Thing(Foo):
def bar(self):
print('this is bar, i am a foo type')
t = Thing() # no exception raised
t.bar() # this is bar, i am a foo type
We can now also compare types using: isinstance(t, Foo)
. Meaning if you have two classes and both of them implement the bar
method, it doesn’t automatically mean they are compatible types:
import abc
class Foo(abc.ABC):
@abc.abstractmethod
def bar(self):
pass
class Bar(abc.ABC):
@abc.abstractmethod
def bar(self):
pass
class ThingA(Foo):
def bar(self):
print('this is bar, i am a foo type')
class ThingB(Bar):
def bar(self):
print('this is bar, i am a bar type')
ta = ThingA()
tb = ThingB()
isinstance(ta, Foo) # True
isinstance(ta, Bar) # False
isinstance(tb, Foo) # False
isinstance(tb, Bar) # True
Do ABCs give us the full benefit of traditional interfaces? No. But they do help us move closer in that direction.
Metaclasses define default behaviours for an instance of a class.
Doing this manually would look something like this:
def new(cls):
x = object.__new__(cls)
x.attr = 100
return x
class Foo:
pass
Foo.__new__ = new
f = Foo()
f.attr # 100
But this can also be done using a new ‘meta’ class and the metaclass
keyword argument:
class Meta(type):
def __new__(cls, name, bases, dct):
x = super().__new__(cls, name, bases, dct)
x.attr = 100
return x
class Foo(metaclass=Meta):
pass
f = Foo()
f.attr # 100
In the same way that a class functions as a template for the creation of objects, a metaclass functions as a template for the creation of classes. Metaclasses are sometimes referred to as class factories.
Do you need a metaclass for this silly example of ensuring the attr
attribute is assigned to a unique class type? No. You could just use a class level variable:
class Base:
attr = 100
class X(Base):
pass
class Y(Base):
pass
class Z(Base):
pass
X.attr # 100
Y.attr # 100
Z.attr # 100
But it’s good to know about metaclasses as they underpin the collections.abc
implementation of abc.ABCMeta
.