Learning OOP...

Alex Martelli aleaxit at yahoo.com
Mon Jun 11 04:40:46 EDT 2001


"Glyph Lefkowitz" <glyph at twistedmatrix.com> writes:
    ...
> > > Furthermore, I'd say that creating reusable code is *wrong*.
> >
> > Interesting strawman, but you haven't come anywhere close
> > to 'proving' it in the following.
>
> I'm sorry, I should have been more clear.  "reusable" code is wrong;
> "usable" code is right.  Re-use implies recycling, dragging something out

Maybe it has this implication to you -- it definitely doesn't
have it to me, nor to most authors on the subject of reuse and
reusability and therefore to their readers.

> of the garbage to use it again.  For example, when you put on your new
> pair of shoes for a second time, you usually don't think of it as re-use
> (although you are technically "Using them again"), but it *is* re-use when
> those shoes are a hand-me-down from an older relative, and have thus
> exceeded their intended usefulness.

But in a context where "custom design" is the NORM, as it has
long been in the software world for MOST code, ANY "second use
in another project" IS "exceeding [expected] usefulness".  The
point of the whole software-reuse movement is exactly that of
changing such expectations of customization.


> > ... "The unit of reuse is the unit of release" ... Robert Martin ...
>
> Excellent!  Where can I find some writings by this fellow? :)

www.objectmentor.com, of course.


> > > projects.  Keep your systems small and have them communicate clearly
and
> > > sparsely over encapsulation boundaries, and your code will be useful
for
> > > years; and you'll never have to re-cycle it.
> >
> > "Interface segregation principle", "dependency inversion principle",
> > etc.  Again, www.objectmentor.com has good papers on this:-).
>
> I hardly think that object orientation has a monopoly on this approach :).

Yeah, yeah, right.  As is well known, practitioners' response to a
paradigm shift goes through various typical stages, including an
early "this stuff is nonsense" and a later "but that's what we've
been doing all along".  One would think that a quarter of a century
after the OO shift most practitioners would have gotten over it,
but apparently Kuhn had it right back when he wrote "The Structure
of Scientific Revolutions" -- in the end, it takes a generational
turnover to make a new paradigm fully accepted, ideologically as
well as pragmatically.


> The reason that I don't think that these are really OO principles is that
> the most successful component architecture and reuse framework that I've
> ever seen is UNIX, which says nothing about objects; only executables,
> files, and streams.

Unfortunately, this "most successful component architecture and
reuse framework" today lags badly behind a component architecture
and reuse framework that outsells it by at least an order of
magnitude and DOES actually provide for component reuse.  When
a programmer has gotten used to COM and Automation, for all their
warts, moving back to a world bereft of it is anything but a
nice prospect.  It should be a GIVEN that I can reuse the
functionality of an existing application by driving it with
mine -- in the world of COM, it basically is.  Connecting
separate executables along a couple of file-like text streams
just can't compare -- and people who pretend to compare, if
they know what they're talking about, are talking through their
hats.  Why has Unix evolved .so's, and now at long last XPCOM
and other REAL component frameworks, if "executables and streams"
were sufficient as a component-reuse framework?-)  Alas, too
many Unix fans apparently live mired in the '70s -- Unix may
not get a *really GOOD *AND* WIDESPREAD* component framework
until it's too late (because generational turnover is just too
slow compared with technology's pace).  I grieve for this prospect,
since COM is perched on top of an OS architecture which can be
thought of as a collection of mutually-sustaining warts -- I'd
MUCH rather be doing components on top of a GOOD kernel & other
OS-level components.  But "network effects" (in the economic
sense) are likely to ensure this won't be practicable, most
particularly when coupled with this reverence you're displaying
for "executables, files and streams" as component-technology.
Oh well -- taking Niven and Pournelle's advice, I think of it
as evolution in action:-).


> The most revolutionary development in software engineering was the
> subroutine, closely followed by the data structure.  The notion of
> "objects" has been a useful tool to think with, but much more of a small
> incremental improvement than the revolution of the subroutine :)

I'm not sure what the interest in this historical debate might be.
What about alphabets, positional number systems, and algebraic
notation -- weren't *those* even bigger "revolutionary developments"
in information-processing technologies?  And -- ***SO WHAT***?!

I'm second to nobody in my fascination for useless tidbits of
historical framing and debate, but I don't kid myself that this
is a productive way to employ my time.  Whether Arabic numerals
actually reached European practice thanks to Pope Sylvester (born
as Gerbert, and probably the only great mathematician to ever be
Pope), and thus through Arabic Spain, or rather through Italian
merchants' business dealings with the East, is an absolutely
fascinating issue, but it makes no difference whatsoever to the
way I, or anybody else, actually practice arithmetic today.  It
is a similarly-fascinating and equally useless "waste of time"
to drag the conversation away from today's best practices and to
attempts to evaluate relative importance of very different IT
developments.


> > Except that it's not, when applied well.  Python shows well how
> > suitably designed inheritance can make for smooth framework-code
> > reuse, e.g. in sgmllib/htmllib and the whole server hierarchy.  Code
> > generators should generate base-classes that are specialized by
> > inheritance, *NOT* by editing generated sources (a round-trip
> > nightmare).  Mixins allow seamless reuse.  Etc, etc...
>
> I haven't had good experiences either with inheritance in either of those
> examples you mentioned. "Aggregate, not inherit".  Especially in python,

Wrong, especially in Python.  When needed semantics are exactly
those of Python's inheritance (which happens very often, because
Python is very well designed), it is an absurd obfuscation
exercise to rebuild the same mechanism, slowly and unreadably,
on top of _other_ Python building-blocks, in the name of some
abstract and inapplicable principle.

> when one can easily forget the oh-so-subtle method chain;
>
>  def __init__(self, *args, **kw):
>   apply(self.__class__.__bases__[0].__init__, (self,)+args, kw)

Do try to forget this, PLEASE.  It's an absurd obfuscation all
over again -- an attempt to ensure recursion when this class
is inherited from.  MyBase.__init__(self, *args, **kw) is the
obvious way to do it.  Complication for complication's sake is
not Pythonic, not sensible, and not pretty.


> > It's true that implementation inheritance CAN too easily create
> > unintended strong coupling.  Scott Meyer's rule of thumb to
> > "never inherit from a concrete class" (in a C++ setting) shows
> > to what extremes one could go to ward off that danger...
>
> A number of programmers I've met adhere to this rule (Moshe Zadka in
> particular, I believe ^_^), and I aspire to now. Twisted Python (PLUG:
> http://twistedmatrix.com ) began life with a rather entangled inheritance
> structure and I've almost completely deconstructed it... and it's much
> easier to use for it.

If your structure was entangled, you did well to refactor it.  But
if you've replaced an entangle structure of inheritance by an
entangled structure of mutual object references, particularly
with circular aspects, then your "eschewing inheritance in favor
of aggregation" actually made your architecture *WORSE*.  The
problem is the entanglement, NOT inheritance, which, in Python,
is just a neat and powerful reuse-mechanism -- no more, no less.

A typical case (exemplified by sgmllib, etc) is a design pattern
with a general-controller and a specializer.  The controller
implements higher-abstraction processing by sequencing calls
to lower-level methods that MAY be implemented in the
specializer -- but need not be (the controller itself provides
default lower-level methods that will often suffice).  This
is basically what the GoF call the "Template" DP, and is a very
handy one.  Implementing it without inheritance requires a
tangled structure: the controller must hold a reference to
the specializer to invoke methods on it, the specializer must
refer back to the controller to re-delegate some processing
to it.  What a mess!  Not to mention the need to use weak
references or otherwise break the loop when you're done (if
a __del__ interferes with GC...:-).  Inheritance on the other
hand makes this extremely simple and effective -- we share
*object identity* between controller and specializer, AND
method-overriding just works right -- no hassles, no trouble.

In the general case, and particularly in Python, rewriting a
structure that so obviously calls for Python's inheritance
as "the obviously right way", such as the Template DP, in
terms of aggregation and explicit delegation, is just to go
around looking for trouble for trouble's sake.  Practicality
beat purity, and Python's inheritance is VERY practical.


> > > things).  Also, some systems which claim to be "OO" also have static
> > > typing, which partially negates the value of sending messages.
>
> > ...and partially enhances it.
>
> Inheriting across language boundaries (where this communication benefit is
> felt the most) is almost *never* a good idea.

Wrong, if the cross-language framework is designed for it.  With
the Boost Python Library, for example, it's a snap.

> If there were a neater way
> to say "I implement this" than inheriting a base class in Jython, I'd use

Why, sure -- to take 'extends' as meaning 'implements' *IS* to
go begging for trouble.  Confusing the two concepts is a sad
C++ (and Eiffel) design mistake, which Java got right instead.
There is still hope for Python -- see PEP 245 and 246:-).

> trying to do cross-language metaclass hacks? :)  It's a semantic accident
> if any of that stuff works, which I think indicates the fragility of
> inheritance.

All it indicates is that inheritance is great for its job (code
reuse) and NOT for jobs that it's not suited to ('implements'
being one good example:-).  If you think that "the fragility of
inheritance" is indicated by the fact that it doesn't do well
those jobs that it shouldn't be used for, why stop to 'implements':
inheritance is a LOUSY way to squeeze oranges, it can't hold a
candle to a shaped-charge for building-demolition purposes, AND
it's really useless for fireproofing (although it doesn't suffer
from the same side effects as asbestos in the latter case).

If your distaste for inheritance and other OOP tools is an
over-reaction to the over-use of such tools and concepts by
too-eager practitioners, it's psychologically understandable.
But just about ANY tool can be mis-applied to jobs it's not
really well-suited for -- and that tells you nothing about
the tool itself *when properly used*.


> > Again, it demands very solid design (I wouldn't be Pythoning if I
> > didn't think the flexibility of dynamic typing was more often useful
> > than the rigidity of static typing, but I do have to play devil's
> > advocate on this thread, I think:-) but when you have it you're well
>
> I can see advantages to static typing and efficiency as well, but I see
> the need to do so.  What would that make me?  "God's advocate?"  We can

"The need to do" WHAT?  I don't follow you here.


> all see that Python has the divine will on its side now... ;-)
>
> > placed.  Templates (or other mechanisms for compile-time genericity)
> > recover a lot of that flexibility at no runtime cost -- Coplien's
> > "pattern without a name" is IMHO mostly aboyt that (template <class X>
> > class Y: public X, etc).  Basically, templates give you
> > signature-based polymorphism, like Python's, at compile-time only
> > (thus less flexible, but easier to generate speedy code for, and
> > sometimes allowing somewhat earlier error diagnosis).
>
> The "error diagnosis" argument I can't really argue with (C++ compilers
> *do* describe more errors at compile-time), but it smells funny to me.
> I'd guess that C++ just introduces more errors so it can diagnose them.
> After all, which do you find is usually more error-ridden on the first run
> -- C++ code or Python code you've written? :)

It's roughly even, but that's because I'm MUCH more careful,
slow and deliberate when I code in C++.  For a given function-
point set, which may take about 100 lines in Python or 400
in C++ (with liberal use of Boost &c:-), I may take about
an hour to an hour and a half to code it in C++ versus about
10 to 15 minutes in Python -- a ratio of about 6 to 10 for
coding time vs one of about 4 for code size.  Number of
errors left and time to fix them is thus back to roughly
code size (4:1 in favour of Python).


> Mr. Meyer's thoughts notwithstanding; static typing is a way of making OO
> more linearly isomorphic to structured programming.  For example;
>
> ---
>  class Foo:
>   def x(self):
>    blah()
> ...
> f = Foo()
> f.x()
> ---
>
> means that we have to figure out what kind of thing 'f' is before we go to
> send the message 'x' to it.  However,

Absolutely not!  As long as f implements an interface including
a method x that can be called without parameters, we can call
that method without caring in the least "what kind of thing f *IS*".


> difference between thinking of data as "behaviorally" instead of
> "structurally" composed, then non-virtual functions (and the static type
> information that makes them possible) are against the spirit of OO.

Now why are you dragging "non virtual functions" into the mix?!  Eiffel,
the "king" of static-type-checking OO languages and Meyer's cherished
brainchild, has nothing to do with such strange beasts.  The compiler
(if it has all relevant information) MAY be able to optimize away the
virtual-lookup by type-deduction, but that's a compiler-optimization
issue (and, in theory, it might just as well be done in a dynamic
language -- the stalin compiler for Scheme being one good example
that shows such theory working in practice as well:-).


> > > have anything to do with object orientation.  Certainly, provable
> > > correctness and static typing are at odds with reuse, since they make
your
> > > software more resistant to change.
> >
> > Making software resistant to change is neither an intrinsically
> > bad thing, nor one at odds with software reuse.  I DO want the
> > software that I'm reusing to be unchangeable from the point
> > of view of behavior that I, the reuser, observe.
>
> IMHO making software resistant to change *is* an intrinsically bad thing
> Embrace change! :).  Although I appreciate the desire for the behavior to
> remain constant across project boundaries, I don't see that static typing
> helps with this.

You can't really argue both ways: either static typing DOES make
software more resistant to change, and therefore promotes "behavior
remaining constant", or it doesn't.  You seem to be saying that
static typing DOES facilitate constant behavior EXCEPT where such
constance is desirable, yet you bring no arguments at all for
such a peculiar contention of "antiselectivity"!-)

Static typing really needs to be accompanied by "programming as
contracting" constructs (preconditions, postconditions, and
class invariants) to let you specify WHAT behavior will "remain
constant" (and, by difference, what behavior will be free to
change).  A good set of unit-tests for a component does do
SOME of that job (and other important jobs that PbyC by itself
can't really do:-), but not all of it and not as well as PbyC,
because what we want is a *weakest* precondition (in Djikstra's
terms), not just ANY precondition -- just like, in abstract
algebra and elsewhere, we need to identify the minimum set of
axioms that let us prove certain theorems, not just ANY set.

> Python's "file" interface convention has done a lot
> towards aiding re-use; I believe that's partially because it's so easy to
> implement.

But it's badly under-specified.  Ever tried to use your own
C-implemented "file-like object" and wrestled for hours until
you found that the "file-like object" needs to have a
settable 'softspace' attribute for SOME uses (print) but
not for others?  Having to read "client-code" sources to
find out if you need to implement .read() without args,
.read(N), .readline(), .readlines(), or those WITH args,
or whatever else, is also a recurring issue.  A protocol
(or rather set of protocols in this case) specified in
executable, checkable terms rather than just by handwaving
would do wonders to help here!  Cfr PEPs 245 and 246, again.


> > > The easiest way to have a responsive, efficient program is to
prototype
> > > and prototype and prototype in a flexible, "slow" language, until
you've
> > > got the fastest possible high-level algorythm, then optimize for
constant
> >
> > Nolo contendere here, but it's SO important it's worth quoting
> > just to increase the likelihood that it gets read:-).
>
> Yes, let's just leave it here :)

One more time can't hurt:-).


> critique is that high level languages are more important to
> programming than object-orientation. [...]

Maybe -- IF I can do something at a high-enough level, I MAY
not miss O-O too badly.  (E.g., for what little I've done in
Haskell, its typeclasses have AMPLY filled my needs -- I've
felt no need yet to experiment with O'Haskell to get OO as
well).  OTOH, *having* to go at things "the high level way"
when you really and truly need low-level tools is no less
of a bummer.  A single language trying to stretch all the
way (C++, probably also Eiffel, Lisp, Dylan) in my humble
opinion gets too rich and complex for really-handy use --
give me mixed-language, mixed-paradigm programming any time.


Alex







More information about the Python-list mailing list