Variable inheritance

Alex Martelli aleaxit at yahoo.com
Tue May 22 11:48:32 CEST 2001


"Roman Suzi" <rnd at onego.ru> wrote in message
news:mailman.990513152.3215.python-list at python.org...
> On 22 May 2001, Paul Foley wrote:
>
> > On Tue, 22 May 2001 07:45:41 +0400 (MSD), Roman Suzi wrote:
> >
> > > Multiple inheritance says it bad design.
> >
> > It *is* a bad design, but multiple inheritance certainly isn't a sign
> > of a bad design.  You seem to be saying MI is inherently bad.
> > [MI without multiple dispatch is broken, though...]
>
> MI is in most cases bad. Why not aggregation instead?

In Python, inheritance is often a handier way to reuse
code than the alternative of aggregation-plus-delegation.
The latter requires more boilerplate-coding, the former
uses the fast and solid infrastructure Python supplies!

And the inheritance needs to be multiple, because reusable
snippets are best kept separate -- don't jumble them all
up into one big undifferentiated soup, remember instead
the key "interface segregation principle" (Martin's name
for it, but a universally acknowledged idea!).  Which
leads us to the 'mixin' style of programming.

Say I have, in a naive first-cut design, classes such as:

class Intseq:
    def __init__(self):
        self.state = 0
    def nextOne(self):
        result = self.state
        self.state += 1
        return result
    def nextN(self, N):
        return [self.nextOne() for i in range(N)]
    def howMany(self):
        return self.state
    def finished(self):
        return 0

class Readseq:
    def __init__(self, fileobj):
        self.file = fileobj
        self.ncals = 0
        self.done = 0
    def nextOne(self):
        self.ncals += 1
        if self.done: return ''
        result = self.file.read(1)
        self.done = result==''
        return result
    def nextN(self, N):
        return [self.nextOne() for i in range(N)]
    def howMany(self):
        return self.ncals

class Callseq:
    def __init__(self, callable):
        self.funz = callable
        self.ncals = 0
    def nextOne(self):
        self.ncals += 1
        return self.funz()
    def nextN(self, N):
        return [self.nextOne() for i in range(N)]
    def howMany(self):
        return self.ncals
    def finished(self):
        return 0

How would you best capture the commonalities and
differences among these classes, and other similar
ones yet?  I propose mix-in (multiple) inheritance
to minimize complexity and boilerplate.  Some of
our "sequences" need to count calls -- this can
be done by having them expose their actual
'nextOne' as an internal method '_nextOne', and
mixing in a CallCounter:

class CallCounter:
    def __init__(self):
        self.ncals = 0
    def nextOne(self):
        self.ncals += 1
        return self._nextOne()
    def howMany(self):
        return self.ncals

There -- now we can refactor:

class Readseq(CallCounter):
    def __init__(self, fileobj):
        CallCounter.__init__(self)
        self.file = fileobj
        self.done = 0
    def _nextOne(self):
        if self.done: return ''
        result = self.file.read(1)
        self.done = result==''
        return result
    def nextN(self, N):
        return [self.nextOne() for i in range(N)]

class Callseq(CallCounter):
    def __init__(self, callable):
        CallCounter.__init__(self)
        self.funz = callable
        self.ncals = 0
    def _nextOne(self):
        return self.funz()
    def nextN(self, N):
        return [self.nextOne() for i in range(N)]
    def finished(self):
        return 0

Call-counting is now coded in ONE place, just as
it should be.  Copy-and-paste reuse is a killer.

Some classes need a 'never-finished' functionality,
so let's factor out that one too:

class NeverFinished:
    def finished(self): return 0

class Callseq(CallCounter, NeverFinished):
    def __init__(self, callable):
        CallCounter.__init__(self)
        self.funz = callable
        self.ncals = 0
    def _nextOne(self):
        return self.funz()
    def nextN(self, N):
        return [self.nextOne() for i in range(N)]

&c.  And all need "iterable" functionality, so:

class Iterable:
    def nextN(self, N):
        return [self.nextOne() for i in range(N)]

and

class Callseq(CallCounter, NeverFinished, Iterable):
    def __init__(self, callable):
        CallCounter.__init__(self)
        self.funz = callable
        self.ncals = 0
    def _nextOne(self):
        return self.funz()

and so on.  Doing it with aggregation and delegation
would fill our classes with boilerplate methods that
do nothing but delegate to the aggregated object that
does the real work, *AND* saddle us with serious
callback organization problems which sharing identity
and behavior (thanks to MI) makes vanish into thin air.


Alex






More information about the Python-list mailing list