join battle redux (was Re: For review: PEP 308 - If-then-else expression)

Alex Martelli aleax at aleax.it
Tue Feb 11 03:48:40 EST 2003


On Tuesday 11 February 2003 01:11 am, Carlos Ribeiro wrote:
   ...
> > > Then I wonder why do people think that the following snippet is
> > > pythonic in any (reasonable) way:
> > >
> > >   ','.join(lines)
> > >
> > > In what direction does it reads?
> >
> > Left to right:
> >    From the literal string object ',',
> >    fetch the attribute named 'join',
> >    prepare the value of the variable named 'lines' as the argument,
> >    and execute the attribute (must be a method) with the argument.
>
> Alex,
>
> You're _literally_ right :-). But that doesn't mean that it 'reads right';
> my intention was to say,
>
>   'I want my lines joined using commas as delimiters',
>
> not:
>
>   'Comma, please join these lines for me.'

Just as when you write:
    somelist.append(anitem)
your intention was to say "I want to append this item to this list" and
NOT "o dear list, please append this item to yourself for me".  And so?

The order of words in the most natural English way to express the
operation is not the same as in, say, German (where the verb would
come at the end) or Python (where the list comes first).  But your
argument doesn't show comma.join(somelist) as any less Pythonic
than somelist.append(anitem), and that's Pythonic enough for me.

It's just silly to dwell on the order of words in one natural language
or another -- it's exactly the wrong criterion to use in order to
determine a programming language's syntax &c.  Rather, in a
language that does single dispatching, what you need to focus on
is: the object to the left of the comma is the one on which full
dispatching is done -- those in parentheses can be accessed via
suitable protocols, but NOT sensibly dispatched on.  Given this,
what's the best one object to have on the left?

And that's *CLEARLY* the joiner (for joiner.join(anything)).  Only
in this way can you get the operation "FOR FREE" on ANY
iterable -- sequences, files, generators, ANYTHING!!! -- *AND*
full polymorphism on the joiner object, whose usefulness I have
shown many times in the past.

Anybody who doesn't see this at once is quite forgivable -- errare
humanum est! -- but I hold that people who HAVE been shown
this and STILL keep whining on how they'd much rather express
their programs in Swahili word-order, no matter what horrible loss
of functionality and huge burdens this implies, should go program
in Swahili (or go debate PEP 308 for the next year or two) and
give the rest of us a breather.


> btw, this particular discussion is already a few years old, and there's

Yes, but not all readers of the newsgroup can be expected to know
the ins and outs of it already; therefore, when the whining comes up
again, somebody sensible must keep repeating the obvious reasons
why the whining is simply wrong, and Python's design is right on
this specific aspect.

> nothing that can be done about it (and I found myself using it lately, so
> it may be the right way to say it after all...)

Educating people who CAN be educated is not "nothing": by helping
them understand why this design IS exactly right, one may help them
become better programmers, and designers.  And I'm glad to see that
a few years' experience may be starting to help you see how this
design IS right -- maybe in a few more you'll even stop whining
against it.

But for the benefit of people who can learn from reading examples,
rather than needing years of experience, let me give an example.

The current iterator protocol requires me to code a class that
exposes a method named 'next' and one named __iter__ which
returns self -- that's all, and then ALL kinds of iterations, fully
including inside library functions and methods that take iterator
arguments, can be smoothly performed.

So, say I need strings of the form '0', '0-1', '0-1-2', and so on;
or in some cases '0', '0/1', '0/1/2', and so on; and the like.

The current way:

class nums:
    def __init__(self, N):
        self.N = N
        self.i = 0
    def __iter__(self, N):
        return self
    def next(self):
        if self.i >= self.N: raise StopIteration
        result = str(self.i)
        self.i += 1
        return result

and then '-'.join(nums(3)), '/'.join(nums(5)), and so on.  Very
simple and elementary.  ANY iterable can be iterated on for
all sort of interesting purposes, including joining.

And if I need something slightly different, such as alternating
dashes and slashes as in '0-1/2-3/4-5', that's easy too:

class dashandslash:
    def join(self, sequence):
       result = []
       sep = '-'
       for x in sequence:
           result.append(x)
           result.append(sep)
           if sep=='-': sep = '/'
           else: sep = '-'
       return ''.join(result[:-1])

and dashandslash.join(nums(6)) is now what we want.


If the paladins of horrible designs based on having join as
a method of SEQUENCES had been at the helm in Python's
design, Python would be a SUBSTANTIALLY worse language
as a result -- we'd have to code a join method to EVERY
sequence (shudder) AND other iterator, or forcibly inherit
"helpers", i.e., crutches to partially compensate for the crippled
design.  This is just the sort of thing that makes me happy
the BDFL takes the decisions...:-).


Alex






More information about the Python-list mailing list