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