conditionals in lambdas?

Alex Martelli aleaxit at yahoo.com
Thu Nov 9 14:49:25 CET 2000


"Steve Horne" <sh at ttsoftware.co.uk> wrote in message
news:sr6l0t0ofsorqhojv77deecu2gjusgc8o9 at 4ax.com...
> On 3 Nov 2000 19:39:07 GMT, msoulier at nortelnetworks.com (Michael P.
> Soulier) wrote:
>
> >    Hey people. If I want to put conditions in a lambda function, how
would I
    [snip]
> is a simple trick to doing this for conditions in general...
>
>   ([no_result, yes_result] [(condition) + 1])
>
> ie - put the possible answers in a list and then use the condition to
> derive the subscript.

This is (roughly) ok *IF AND ONLY IF* it is OK to evaluate both results
independently of the condition's value.

Why "roughly": this will work if condition is either 0 or 1, but will
fail in different ways for most other values of 'condition'.  Python
accepts many more kinds of values as 'true-or-false conditions'; any
non-zero number is 'true', an empty sequence is 'false' and other
sequences are 'true', etc.

It's easy to do the same by switching your idiom to:

    (yes_result, no_result)[not (condition)]

as the "not" will obey exactly Python's general rules for what is
true and what is false, and it WILL return exactly 0 if condition
is true, exactly 1 if condition is false.


The 'deeper' problem remains: this will fail in some cases in
which if/else would work just fine, e.g....:

    if foo != 0:
        return 1/foo
    else:
        return 23

is NOT equivalent to:

    return (1/foo,23)[not (foo!=0)]

because *both entries in the tuple will be evaluated, whatever
foo is worth*.  So, when foo==0, the 'indexing' approach will
fail with a division-by-zero exception, while if/else would
give no problem.

This is because Python's general approach to evaluation is
"eager" (all arguments to a function are fully evaluated before
the function is called) and not "lazy" (only evaluate args
if and when their values are actually needed).


For cases in which this may be a problem, a closer approach
to 'conditionals' (if/else behavior) is supplied by Python's
operators "and" and "or".  THOSE (and those only) avoid "too
much zeal" (eagerness) in evaluating their operands; their
semantics specifies "short-circuit" evaluation -- the right
side operand gets evaluated at all _ONLY_ if its value is
actually needed to give the operator's result.


For "and": if the left-side operand's value is "false", then
the whole "and" operation returns "false", and the right-side
operand *is NOT evaluated at all*.  If the left-side operand's
value is "true", then the whole "and" operation returns as
its value the value of the right-side operand.

In other words, there IS semantic equivalence between:

    return a and b

and

    temp = a
    if temp:
        return b
    else:
        return temp

for ANY expressions a and b (I've used an explicit 'temp'
in the 'equivalent-semantics' construct to underline that
a is not multiply evaluated...).


"or" works similarly, i.e. it also "short-circuits", but
"the other way around": the equivalence becomes, then:

    return a or b

and

    temp = a
    if temp:
        return temp
    else:
        return b


So, the full semantic equivalence to:
    if condition:
        return yes_result
    else:
        return no_result

can *almost* be built up as an expression by:

    return (condition and yes_result) or no_result

where the "almost" comes from the risk that "yes_result"
may evaluate to false... in which case the "no_result"
would unwontedly be evaluated and returned!


One trick to bypass even this issue may be...:

    return ((condition and (yes_result,)) or (no_result,))[0]

By making the right-side of the "and" into a one-item
tuple, we know Python will _never_ evaluate it as "false"!
(We then also need to make the no_result into a similar
one-item tuple, so we can end by getting the 0-th [only]
element in either case...!).


So much subtlety is hardly ever needed.  Most of the
time, either the indexing-trick or the simpler and/or
trick will be provably OK (if we know it's OK to
evaluate both results, and/or we know the yes_result
cannot be false).  If any cases subsist where the
"full trick" seems necessary, I think I would always,
as an issue of style, prefer to eschew the lambda in
favour of a local named-function, where I will need
no dirty tricks whatsoever to express my wishes...
lambda is supposed to be there *as a convenience*
(it's basically just saving you the trouble of thinking
up a name for some inconsequential thingy) -- why keep
using it in cases where it turns out to be so massively
*IN*-convenient, as to force such complexities...?!-)

Say, for example, that I need to write a function
with the signature:
    def invseq(numseq, onzero)
and the semantics: return a sequence with the numeric
inverse 1/x of each number x in numseq, except that,
if x==0, 'onzero' needs to be used in lieu of 1/x in
that spot in the result-sequence.

I think the cleanest, most readable solution is:

def invseq(numseq, onzero):
    def inv(x, default=onzero):
        if x: return 1/x
        else: return default
    return [inv(x) for x in numseq]

or, in older Python versions (or for list-comprehension-
haters:-):

def invseq(numseq, onzero):
    def inv(x, default=onzero):
        if x: return 1/x
        else: return default
    return map(inv, numseq)

even though the lambda-equivalent is more compact, and,
in this case, can be coded with the simple-and/or-trick:

def invseq(numseq, onzero):
    return map(lambda x,default=onzero: ((x!=0) and (1/x)) or default,
numseq)

It seems to me that _naming_ the "invert" subfunction,
in this case, increases clarity, even though the line
count becomes 4 (moderate-length) lines versus one (a
bit too-long) line.  An issue of style, of course.


Now, say we must handle _two_ sequences (of the same
length), returning a sequence that, at each spot,
has x/y (where x is the corresponding element of
the first sequence, y of the second one) _except_
that, for those spots in which y is 0, x+1 is to be
returned instead.  The simple-trick...:

def divseqs(seqx, seqy):
    return map(lambda x,y: ((y!=0) and (x/y)) or x+1, seqx, seqy)

doesn't work any more -- at those spots where x==0,
we'd be wrongly returning 1 instead!  We need the
full-trick in this case...:

def divseqs(seqx, seqy):
    return map(lambda x,y: (((y!=0) and (x/y,)) or (x+1,))[0], seqx, seqy)

and it seems to me that its readability is truly abysmal.
Wouldn't you rather write...:

def divseqs(seqx, seqy):
    def div(x, y):
        if y: return x/y
        else: return x+1
    return map(div, seqx, seqy)

or equivalently (Python 2):

def divseqs(seqx, seqy):
    def div(x, y):
        if y: return x/y
        else: return x+1
    return [div(x,y) for x, y in zip(seqx,seqy)]

...?


Alex






More information about the Python-list mailing list