Try, except...retry?

Alex Martelli aleax at aleax.it
Thu Nov 13 17:33:01 CET 2003


Robert Brewer wrote:
   ...
> into cases like:
> 
> try:
>     aPiranha = allPiranhas['Doug']
> except KeyError:
>     aPiranha = Pirhana()
>     allPiranhas['Doug'] = aPiranha
> aPiranha.weapon = u'satire'

Yeah, it IS frequent enough that Python has two well-known idioms to
deal with it.  If a call to Piranha() has very low cost,

    allPiranhas.setdefault('Doug', Piranha()).weapon = u'satire'

is very compact.  If calling Piranha() is potentially costly, this
is unfortunately no good (due to Python's "strict" execution order,
all arguments are evaluated before running the method), so:

    if 'Doug' not in allPiranhas:
        allPiranhas['Doug'] = Piranha()
    allPiranhas['Doug'].weapon = u'satire'

or, to avoid indexing twice:

    aPiranha = allPiranhas.get('Doug')
    if aPiranha is None:
        aPiranha = allPiranhas['Doug'] = Piranha()
    aPiranha.weapon = u'satire'

In retrospect, it WOULD perhaps be better if setdefault was designed
to take a callable (and optional args for it) and only call it if and
when needed -- that would add a little speed and clarity in the two most
typical use cases, currently:
    dictOfDicts.setdefault(mainKey, {})[secondaryKey] = value
and
    dictOfLists.setdefault(key, []).append(value)
which would just become:
    dictOfDicts.setdefault(mainKey, dict)[secondaryKey] = value
and
    dictOfLists.setdefault(key, list).append(value)
respectively; and widen the applicability of .setdefault to other cases,
while costing very little actual use cases (I've never seen setdefault
correctly called with a 2nd argument that wasn't (), {}, or the like).
Ah well, too late, just musing aloud.

Still, it seems to me that the existing idioms are nevertheless
superior to your desideratum:

> which would, in my opinion, be better written (i.e. *clearer*) as:
> 
> try:
>     allPiranhas['Doug'].weapon = u'satire'
> except KeyError:
>     allPiranhas['Doug'] = Pirhana()
>     retry

which does index twice anyway.

> Of course, there are other ways of doing it currently, most notably with
> 1) a while loop and a retry flag, or 2) just repeating the assignment:
> 
> try:
>     allPiranhas['Doug'].weapon = u'satire'
> except KeyError:
>     allPiranhas['Doug'] = Pirhana()
>     allPiranhas['Doug'].weapon = u'satire'
> 
> Yuck to both.

Yes, flags (hiding control flow in data!) and repeated code do suck, but
you need neither to get exactly the same semantics as your desideratum:

    while True:
        try: allPiranhas['Doug'].weapon = u'satire'
        except KeyError: allPiranhas['Doug'] = Pirhana()
        else: break

> Current docs, 4.2 Exceptions says, "Python uses the ``termination''
> model of error handling: an exception handler can find out what happened
> and continue execution at an outer level, but it cannot repair the cause
> of the error and retry the failing operation (except by re-entering the
> offending piece of code from the top)." I'm proposing that 'retry' does
> exactly that: reenter the offending piece of code from the top. Given
> the aforementioned pressure to reduce try: blocks to one line, this
> could become a more viable/common technique.

It does not appear to me that, even assuming that this looping is in
fact the best general approach, there is enough advantage to your
proposed "try/except ... retry" technique, with respect to the
"while/try/except/else: break" one that is already possible today.

I could be wrong, of course: there is nothing that appears to me
to be "outrageously nonPythonic" in your proposal -- it just seems
that new statements need to be more of a win than this in order to
stand a chance.  But a PEP on this is surely warranted, if you want
to try one.

> Apparently Ruby has this option? Gotta keep up with the Joneses. :) I'm

Yes, Ruby does allow retry in a begin/rescue/else/end construct (on
the rescue leg only).  I don't think the use case in the "Programming
Ruby" book shows it in a good light _at all_, though -- and I quote:

"""
@esmtp = true

begin
  # First try an extended login. If it fails because the
  # server doesn't support it, fall back to a normal login

  if @esmtp then
    @command.ehlo(helodom)
  else
    @command.helo(helodom)
  end

rescue ProtocolError
  if @esmtp then
    @esmtp = false
    retry
  else
    raise
  end
end
"""

Eep!-)  The dreaded "flag", AND a rather intricated structure
too...!!!  It seems to me that a much clearer way is (Python-ish
syntax, same semantics):

try: 
    self.command.ehlo(helodom)
except ProtocolError:
    self.command.helo(helodom)
    self.smpt = False
else:
    self.smpt = True

(this leaves the self.smpt flag unset if the ProtocolError
exception propagates, rather than ensuring it's false in that
case, but if [unlikely...] this gave any problem, then just
moving the "self.smpt = False" to the top would fix that:-).

Admittedly the use of else IS "a bit precious" here, since
assigning to self.smpt is VERY unlikely to raise a ProtocolError,
so, maybe,

try: 
    self.command.ehlo(helodom)
    self.smpt = True
except ProtocolError:
    self.command.helo(helodom)
    self.smpt = False

would also be OK.  I just try to foster the HABIT of using
try/except/else to avoid "protecting", with an except, ANY
more code than strictly necessary, on general principles.  But
in this case the else-less form has a very pleasing symmetry
so I would nod it through during a code-inspection or the like:-).

But the point is, the existence of retry has tempted those
_excellent_ (and pragmatic:-) authors & programmers, Thomas and 
Hunt, into perverting a clean, simple structure into a little 
but definite mess.  This sure ain't good recommendation for
adding 'retry' to Python...:-).  Given that the "key missing
in a dict" case is also dealt with quite decently without
looping, I would suggest you look for other use cases.
Perhaps simplest...:

try:
    spin = raw_input("Please enter your PIN: ")
    pin = int(spin)
except (EOFError, KeyboardInterrupt):
    print "Bye bye!"
    return 0
except ValueError:
    print "PIN must be an integer [just digits!!!], please re-enter"
    retry
else:
    return validatePIN(pin)


> not enough of a Pythonista yet to understand all the implications of
> such a scheme (which is why this is not a PEP), so I offer it to the
> community to discuss.

No special "implications", as the semantics are just about the
same as the above-indicated (flags-less, duplication-less)
"while True:" loop that is so easy to code explicitly today.

It's just that, partly because of this (and attendant benefits
that writing out "while" DOES clearly indicate to the reader
that the following code may repeat, etc etc), it does not seem
to me that 'retry' is worth adding.  But unless somebody does
write a PEP, you'll just have my opinion about this...


Alex





More information about the Python-list mailing list