Proposal: default __init__

Alex Martelli aleaxit at yahoo.com
Mon Nov 13 16:40:55 CET 2000


"Andrew Dalke" <dalke at acm.org> wrote in message
news:8uotmc$uo4$1 at slb7.atl.mindspring.net...
> Hello,
>
>   I propose that objects in some Python 2.x or in 3K have a default
> __init__.  As it is now, base classes must explicitly include an
> __init__ to be future-proof, which is not obvious.  Let me explain.
>
>   I updated one of my libraries and ended up having to make a lot
> of changes to client code.  What happened was the base class didn't
> have a constructor
>
>   class Spam:
>     pass

OK so far.

> so my client code was declared like:
>
>   class Eggs(Spam):
>     def __init__(self):
>        self.scrambled = 0

This relies on there NOT being a Spam.__init__ to call, of course.
Nothing terrible, mind you, but it IS a "dog that didn't bark in
the night" kind of scenario:-).


> It could not call Spam.__init__(self) because that method does not exist.

No, but it _could_ perfectly well use a "call-if-exists" idiom, IF
you decided to do so.  E.g.,

class Eggs(Spam):
    def __init__(self):
        self.scrambled = 0
        superinit = getattr(Spam,'__init__',None)
        if callable(superinit): superinit(self)

Note that I'm not saying it *should* do so.  Generality that
goes unused is a _cost_... it is, in general, quite reasonable
to "do the simplest thing that could possibly work" -- just
refactor later, if and when it turns out that a little bit
more generality IS needed in a given, specific case.

Similarly, if you wanted to take into account possible future
needs, nothing stopped you from including a do-nothing-yet
__init__ in Spam itself (similar 'simplicity' considerations
apply).

One hardly expects derived-classes to stay unchanged when the
base-class's structure changes, after all.


> But then the base class changed so it had a constructor:
>
>   class Spam:
>     def __init__(self):
>       self.__cache = {}

Good.

> That meant I needed to change all of my Egg's __init__s to call
> Spam.__init__.

Yes, of course.

> However, in some sense the API for Spam did not change.  Suppose

In *what* sense?  Only that of a call to Spam(), I guess --
which internally does the call-if-exists on __init__.  But,
clearly, not for derived-classes of Spam -- for them, Spam's
API did change, and how!

> all classes have a built-in method called __init__ which takes no
> parameters and does nothing but is called if there is no user defined
> call.  Then I could have written

You mean, something equivalent to:
    def __init__(self): pass
?

That would break currently-working code if an __init__ existed
in any _other_ base-class:

class A:
    pass

class B:
    def __init__(self): print "B.__init__"

class C(A,B):
    pass

class D(C):
    def __init__(self): return C.__init__(self)

In current Python, D() runs B.__init__; but, if a
do-nothing __init__ was added to either C _or A_,
then B.__init__ wouldn't get called any more.

So, if nothing else, the "if there is no user
defined call" _has_ to be specified *much* more
precisely -- the "phantom do-nothing __init__"
should "appear" under exactly _what_ cases...?

"not hasattr(X, '__init__')" does not seem to
be a sufficient condition for a do-nothing
__init__ to be synthesized for class X -- note
that class A, above, satisfies this, yet doing
the synthesis on it *would* break currently
working code for class D.

Maybe the determination would have to be context
dependent -- and unless we're willing to shatter
a lot of current implication relationships among
hasattr, getattr, ..., that seems a biggie.  I.e.,
hasattr(A,'__init__'), for *one and the same*
class object A, would have to be true in certain
cases, false in others... seems pretty thorny to
sort out.

>   class Eggs(Spam):
>     def __init__(self):
>       Spam.__init__(self)
>       self.scrambled = 0

Why not always code like this, if you want to 'never'
have to (so modestly!) refactor derived-classes?  Or
check as above, if you insist on coding base-classes
as:

>   class Spam:
>     pass

rather than:

> having a different interface than the functionally identical
>   class Spam:
>     def __init__(self):
>       pass

These are FAR from "functionally identical", BTW.  See
above...!  The latter stops __init__ from being looked
for in other, parallel bases-classes; the former, of
course, doesn't.

Even apart from such lookup differences, claiming these
classes are functionally identical is like making the
same claim for

class Foo1:
    def bar(self): pass

vs

class Foo2:
    pass

They simply *aren't*.  You can, if you wish, call .bar
on instances of class Foo1 (without effect); you can't
do it on instances of class Foo2.

Why is __init__ any different?  Because, in certain
cases, it's called 'on your behalf' by the class itself,
when it's used as a callable to instantiate it?  But
that is no different from, say, __add__ -- *that*, too,
is called 'indirectly' (by using + as an operator on
an instance of the class)...

Python's rules for inheritance are crystal-simple: their
utter, refreshing simplicity is exactly what makes it all
so awesomely *powerful*.


> Comments?  Any reason why this would be a bad thing?

Convenience, Simplicity, Power: pick any two.

Python is one of the few software environments I know
that has _mostly_ managed to eschew the misleading lure
of "Convenience", in favour of the far-preferable
beacons of Simplicity and Power.  Thus, for example,
no automatisms (which would be VERY convenient indeed,
far more than your more limited proposal...) in the
calls of such methods as __init__ and __del__ on base
classes -- you must do it yourself, explicitly, if and
when you want; Python itself only deals with the 'first'
call to such methods, found with the same getattr rules
that always apply to _all_ method lookups.  A trifle
less convenience, a ton more simplicity & the resulting
power...


Alex






More information about the Python-list mailing list