statements in control structures (Re: Conditional Expressions don't solve the problem)

Andrew Dalke dalke at dalkescientific.com
Thu Oct 18 19:31:48 EDT 2001


Huaiyu Zhu:
>I picked a few files that have more than a few break statements, and
>searched for break.  Almost all of them are in this style:
>
>while 1:
>    x = something
>    if not x: break
>    ...

which in Python 2.2 can in many cases be rewritten

for x in iter(something, None):
    ...

>Following are a few examples of while-loops.  I don't think I'll spend more
>time on this.

I'm hoping you'll write this up as a PEP because:
  1) I don't channel Guido
  2) even if he says no, it will be a reference for future discussions
      on this topic.

>Anybody can do their own search if they want real life
>examples.

But then you have nothing to counter someone saying "I scanned
the standard library but couldn't find any place where this proposal
is helpful in real life."  :)

>  Also I'm not sure how to search for possible places where nested
>if-else ccould be changed to flat elif.

Look for "elif" followed by an "elif" which is indented by
another level.  The code I posted earlier should be a good base
for this search.

Also, have you looked for the many places that would be *worse* by the
flexibility to make this choice?  I point out one below.

>Doing these examples, I got a clearer view on why the change would be good:
>The new syntax allows one to say
>        " do ... under this condition ... "

There are times when it's shorter.  But you skip the possibility of
using an iterator.

>instead of the more twisted logic of
>        " do ..., but if not this, change ... "
>It also displays the loop condition prominently, instead of displaying a
>dummy "while 1" and bury the substance inside.

"while 1" is considered ugly by quite a few people.  But I argue
that; multiple; statements; also; buries; the; substance; inside.

>Here are 6 examples [Disclaimer: It's about half an hour quick hack
>including the searching.  Almost mechanical change.  No guarantee I really
>understand the original code before changing them. No checking, debugging
or
>whatsoever, etc.]

Understood.  And there are bugs.  :)

>./Demo/pdist/makechangelog.py:
>Change to:
>
>    while file = getnextfile(f); file:
>        revs = []
>        while rev = getnextrev(f, file); rev:
>            revs.append(rev)
>        if revs:
>            allrevs[len(allrevs):] = revs
   ...

>changed to
>    while line = f.readline(); line:
>        if startprog.match(line) >= 0:
>            file = startprog.group(1)
>            while line = f.readline(); line[:10] != '-'*10:
>                if not line: return None
>                if line[:10] == '='*10: return None
>            return file
>    return None

This module is really old.  It even uses regex.  I would rewrite
much of the code, to make it look more iterator based:

for file in getnextfile(f):
    revs = [x for x in getnextrev(f, file)]
    if revs:
        allrevs.extend(revs)


def getnextfile(f):
    while 1:
        line = f.readline()
        if not line: break
        if startprog.match(line) >= 0:
            file = startprog.group(1)
            # Skip until first revision
            while 1:
                line = f.readline()
                if not line: return
                if line[:10] == '='*10: return
                if line[:10] == '-'*10: break
            yield file

I would also change getnextrev so it's an iterator.

>Lib/ftplib.py:
>--------------------------------------------(3)
>    def getmultiline(self):
>        line = self.getline()
>        if line[3:4] == '-':
>            code = line[:3]
>            while 1:
>                nextline = self.getline()
>                line = line + ('\n' + nextline)
>                if nextline[:3] == code and \
>                        nextline[3:4] != '-':
>                    break
>        return line
>
>changed to
>
>    def getmultiline(self):
>        code = None
>        lines = []
>        while line = self.getline(); lines.append(line); line[3:4] == '-':
>            if code != line[:3]: break
>            else: code = line[:3]
>        return '\n'.join(lines)

I don't understand your conversion.  I did a mechanical translation
of your code to existing Python and I get a different result.

class Test:
   def __init__(self):
      self.data = ["012- Some sort of message",
                   "012- Would go here",
                   "012  End of multiline"]
   def getline(self):
      x = self.data[0]
      del self.data[0]
      return x

class TestOld(Test):
   def getmultiline(self):
      line = self.getline()
      if line[3:4] == '-':
         code = line[:3]
         while 1:
            nextline = self.getline()
            line = line + ('\n' + nextline)
            if nextline[:3] == code and \
               nextline[3:4] != '-':
                  break
      return line

class TestNew(Test):
   def getmultiline(self):
      code = None
      lines = []
      while 1:
         line = self.getline()
         lines.append(line)
         if not(line[3:4] == '-'):
            break
         if code != line[:3]:
            break
         else:
            code = line[:3]
      return '\n'.join(lines)

print "old"
print TestOld().getmultiline()
print "new"
print TestNew().getmultiline()


[dalke at pw600a src]$ ./python spam.py
old
012- Some sort of message
012- Would go here
012  End of multiline
new
012- Some sort of message
[dalke at pw600a src]$



>--------------------------------------------(4)
>        while 1:
>            data = conn.recv(blocksize)
>            if not data:
>                break
>            callback(data)
>
>changed to:
>
>        while data = conn.recv(blocksize); data:
>            callback(data)

Yes, this is shorter.  It's hard for me to judge if it's better
because I'm so used to the first form.  I have troubles parsing
the two different statement blocks - the first with semicolons
and the second with newlines.

I still don't like multiple items on a given line so if using
this form would be prone to write it like this.

while data = conn.recv(blocksize); \
      data:
    callback(data)

Which is ugly and little shorter than the existing code.

>./Lib/mimify.py:

Writing the full function in your form

def mime_decode(line):
  newline = ""
  pos = 0
>    while res = mime_code.search(line, pos); res is not None:
>        newline = newline + line[pos:res.start(0)] + \
>                  chr(int(res.group(1), 16))
>        pos = res.end(0)
  return newline

Using the (experimental) scanner in sre yields one fewer line:

>>> import mimify
>>> def my_decode(s):
...   t = ""
...   p = 0
...   for m in iter(mimify.mime_code.scanner(s).search, None):
...     t = t + s[p:m.start(0)] + chr(int(m.group(1), 16))
...     p = m.end(0)
...   return t + s[p:]
...
>>> my_decode("=41ndrew =44alke")
'Andrew Dalke'
>>> mimify.mime_decode("=41ndrew =44alke")
'Andrew Dalke'
>>>

Yes, I'm not playing fair here by using an undocumented feature.

/F?  When's this going to be non-experimental?


>    # read header
>    hfile = HeaderFile(ifile)
>    while 1:
>        line = hfile.readline()
>        if not line:
>            return

>Changed to
>
>    # read header
>    hfile = HeaderFile(ifile)
>    while line = hfile.readline(); line:
>        if prefix and line.startswith(prefix):
>            line = line[len(prefix):]
>            pref = prefix
>        else:
>            pref = ''

Your translation is incorrect.  The original is
    if not line:
      return

while yours translates to
    if not line:
      break

so to use your construct you'll need to stick an

  else:
    return

at the end.  This makes it longer.

>        line = mime_decode_header(line)
>        if qp.match(line):
>            quoted_printable = 1
>        elif decode_base64 and base64_re.match(line):
>            is_base64 = 1
>        else

You skipped converting the first two lines into

        if line = mime_decode_header(line); qp.match(line):

Why?  What's the guidance of when to use one form over another?
I prefer the Python philosophy where there should be one preferred
way to do something.  This makes the code easier to understand
and maintain.  Your proposal would shatter that, because there
would be no prefered way.

My prefered way is no semicolon separated statements.


>        if mp_res = mp.match(line); mp_res:
>            multipart = '--' + mp_res.group(1)
>        if he.match(line):
>            break

Yes, this is shorter.  I still argue it's more confusing because
it looses the guidance of the vertical arrangement of code.

It's also possible to solve this specific case with

import operator
class check:
  def __init__(self, result):
    self.result = result
  def __nonzero__(self):
    return operator.truth(result)
check = check()

if check(mp_match(line)):
  multipart = '--' + check.result.group(1)

Indeed, this is closer to how the regex module works.  But I
don't like this style.

So in a couple cases your proposal reduces the number of lines
of code by a smidgeon, with the tradeoff of doing several things
on one line.  In other cases it doesn't help, and hinders things
by making it harder to figure out where the bug is.

Compare this to my experience with list comprehensions.  I
didn't like it because that added a new way to do things I
had already been doing, and I was worried that it would lead
to increased confusing.  It hasn't, because list comprehensions
ended up replacing a very common phrase

x = []
for a in data:
  x.append(f(a))

I now prefer list comprehensions.  I can't imagine ever
prefering semicolon separated statements on the same line.

                    Andrew
                    dalke at dalkescientific.com






More information about the Python-list mailing list