LBYL vs EAFP

  • Thread starter Steven D'Aprano
  • Start date
S

Steven D'Aprano

The eternal conflict between "Look Before You Leap" and "Easier to Ask for
Forgiveness than Permission" (LBYL vs EAFP) continues...

I want to check that a value is a number. Let's say I don't care what sort
of number -- float, int, complex, Fraction, Decimal, something else -- just
that it is a number. Should I:

Look Before I Leap:

from numbers import Number
if isinstance(x, Number):
...
else:
raise TypeError


or Ask Forgiveness:

x + 0
...


where in both cases the ellipsis ... is the code I actually care about.

The second version is more compact and easier to write, but is it easier to
read?

Does your answer change if I then go on to check the range of the number,
e.g. to test that x is positive?



A third option is not to check x at all, and hope that it will blow up at
some arbitrary place in the middle of my code rather than silently do the
wrong thing. I don't like this idea because, even if it fails, it is better
to fail earlier than later.

Comments, thoughts and opinions please.
 
C

Chris Angelico

A third option is not to check x at all, and hope that it will blow up at
some arbitrary place in the middle of my code rather than silently do the
wrong thing. I don't like this idea because, even if it fails, it is better
to fail earlier than later.

Comments, thoughts and opinions please.

It depends on what could cause the failure. If only a programming
error could cause x to not be a number, I'd go with your third option
- let it blow up anywhere, and follow the trace. That option requires
zero forethought, which translates directly into programmer
efficiency. But if x came from a user, then I'd be checking inputs at
a much earlier point.

Those are the two obvious cases though, and I'm assuming your
situation is neither of them. Writing library code is half way in
between. With the specific examples given, I wouldn't like to use "x +
0" as a check; it seems dodgy. Firstly because it doesn't look like a
data type check (though a comment can help with that), and secondly
because something might very well support having a number added to it
while definitely not itself being a number - eg something along the
lines of a database cursor. So I would recommend LBYL for this
particular case.

ChrisA
 
I

Ian Kelly

On Feb 4, 2013 4:24 PM, "Steven D&apos;Aprano" <
The eternal conflict between "Look Before You Leap" and "Easier to Ask for
Forgiveness than Permission" (LBYL vs EAFP) continues...

I want to check that a value is a number. Let's say I don't care what sort
of number -- float, int, complex, Fraction, Decimal, something else -- just
that it is a number. Should I:

Look Before I Leap:

from numbers import Number
if isinstance(x, Number):
...
else:
raise TypeError


or Ask Forgiveness:

x + 0
...


where in both cases the ellipsis ... is the code I actually care about.

It seems to me that both of these are LBYL. That the second test checks by
trying an operation and potentially raising an exception is immaterial.
You're still performing a test prior to attempting the actual operation.
A third option is not to check x at all, and hope that it will blow up at
some arbitrary place in the middle of my code rather than silently do the
wrong thing. I don't like this idea because, even if it fails, it is better
to fail earlier than later.

This is what I would consider EAFP. Presumably if the operation requires a
number, then it will at some point perform some kind of numerical
manipulation that will raise a TypeError if one is not passed. If the
operation succeeds, then the object supported all the operations you asked
of it, so in what sense would the program be doing the wrong thing?
 
D

Dave Angel

It depends on what could cause the failure. If only a programming
error could cause x to not be a number, I'd go with your third option
- let it blow up anywhere, and follow the trace. That option requires
zero forethought, which translates directly into programmer
efficiency. But if x came from a user, then I'd be checking inputs at
a much earlier point.

Those are the two obvious cases though, and I'm assuming your
situation is neither of them. Writing library code is half way in
between. With the specific examples given, I wouldn't like to use "x +
0" as a check; it seems dodgy. Firstly because it doesn't look like a
data type check (though a comment can help with that), and secondly
because something might very well support having a number added to it
while definitely not itself being a number - eg something along the
lines of a database cursor. So I would recommend LBYL for this
particular case.

ChrisA

I agree with Chris. It would seem that the main purpose of the abc
numbers.Number is for this. See the page:

http://docs.python.org/2/library/numbers.html
"If you just want to check if an argument x is a number, without
caring what kind, use isinstance(x, Number)."

But the real question is "what is a number?" . You used lowercase in
your description, so you weren't defining it as the particular classes
that were already associated with Number. I expect you're using Python
3, which you should have specified. If so, I'd tend to replace the x+0
with x > 0 which would be an error for any class other than int which
didn't define the corresponding dunder operators.


Coincidentally, that's the next question you asked. So if you're only
interested in values that are positive, that would seem to be clearer.

Unfortunately, that test would raise a TypeError for complex values.
 
E

Ethan Furman

The eternal conflict between "Look Before You Leap" and "Easier to Ask for
Forgiveness than Permission" (LBYL vs EAFP) continues...

I want to check that a value is a number. Let's say I don't care what sort
of number -- float, int, complex, Fraction, Decimal, something else -- just
that it is a number. Should I:

Look Before I Leap:

from numbers import Number
if isinstance(x, Number):
...
else:
raise TypeError


or Ask Forgiveness:

x + 0
...

As Ian mentioned, both cases are LYBL, unless of course your addition
was just an example of some mathematical code you have further down.

Personally, I go with EAFP unless I'm trying to present friendlier error
messages.

~Ethan~
 
C

Chris Angelico

from numbers import Number
if isinstance(x, Number):
...
else:
raise TypeError


or Ask Forgiveness:

x + 0
...


where in both cases the ellipsis ... is the code I actually care about.

Caveat to my previous post: I would NOT indent the function body for
the sake of this check. I'd negate it:

if not isinstance(x, Number): raise TypeError

(hopefully with further information in the TypeError). I don't like
the code style that puts conditions, then more code, then error
handling - I prefer to fail-and-bail.

ChrisA
 
O

Oscar Benjamin

I want to check that a value is a number. Let's say I don't care what sort
of number -- float, int, complex, Fraction, Decimal, something else -- just
that it is a number. Should I:

Look Before I Leap:

from numbers import Number
if isinstance(x, Number):
...
else:
raise TypeError


or Ask Forgiveness:

x + 0
...

One example that passes in this case but would fail in many others is
a numpy array:
from numpy import array
a = array([1, 2, 3])
a + 0 array([1, 2, 3])
range(a)
Traceback (most recent call last):
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
ValueError: The truth value of an array with more than one element is
ambiguous. Use a.any() or a.all()
where in both cases the ellipsis ... is the code I actually care about.

The second version is more compact and easier to write, but is it easier to
read?

Does your answer change if I then go on to check the range of the number,
e.g. to test that x is positive?

A third option is not to check x at all, and hope that it will blow up at
some arbitrary place in the middle of my code rather than silently do the
wrong thing. I don't like this idea because, even if it fails, it is better
to fail earlier than later.

Why would a non-number appear in this code? How serious a problem
would it be if it did happen?

The other question is whether there is likely to be a use-case for
allowing non-standard numeric types (apart from dogmatic support of
duck-typing).

I'm imagining a scenario where you wrote a library and a user (another
programmer) passed in the wrong thing (even though you clearly
documented that you wanted a number).

If you do check then they get this:

# myuserscript.py
import stevesmod
stevesmod.calculate_with_numbers(number=file)

$ python myuserscript.py
Traceback (most recent call last):
File "myuserscript.py", line 5, in <module>
stevesmod.calculate_with_numbers(number=file)
File ".local/pymodules/stevesmod.py", line 5, in calculate_with_numbers
x + 0
TypeError: unsupported operand type(s) for +: 'type' and 'int'

If you don't check then they get:

$ python myuserscript.py
Traceback (most recent call last):
File "myuserscript.py", line 5, in <module>
stevesmod.calculate_with_numbers(number=file)
File "/home/user/.local/pymodules/stevesmod.py", line 7, in
calculate_with_numbers
compute_stuff(number, other_parameters)
File "/home/user/.local/pymodules/stevesmod.py", line 26, in compute_stuff
normalise(nx, ny)
File "/home/user/.local/pymodules/stevesmod.py", line 35, in normalise
nx += 1
TypeError: unsupported operand type(s) for +: 'type' and 'int'

From the users perspective the traceback gets more complicated. They
still see the exact point at which there code calls into yours though.
So if they know how to use a debugger and reread your docs then it's
fine. (If you were doing something callback-based or storing the
values for later use it would get more complicated).

On the other hand if they just end up getting nonsensical return
values then they should be able to trace that back.


Oscar
 
S

Steven D'Aprano

A third option is not to check x at all, and hope that it will blow up
at some arbitrary place in the middle of my code rather than silently
do the wrong thing. I don't like this idea because, even if it fails,
it is better to fail earlier than later.
[...]

With
the specific examples given, I wouldn't like to use "x + 0" as a check;
it seems dodgy. Firstly because it doesn't look like a data type check

Strange. To me, it looks like "oh, you're testing whether x supports
numeric addition". I suppose the hidden assumption I'm making is that if
x supports addition, it is a kind of number. Perhaps that's an unsafe
assumption.

(though a comment can help with that), and secondly because something
might very well support having a number added to it while definitely not
itself being a number - eg something along the lines of a database
cursor.

But surely duck-typing tells us that if database cursors support the same
sort of operations that numbers support, we should therefore treat them
as a kind of number?

My library will not try to prevent the caller taking the average of (say)
a dozen eggs and 10 miles. I'm not sure that it should try to prevent the
caller from trying to take the average between two database cursors.
 
S

Steven D'Aprano

Presumably if the operation requires
a number, then it will at some point perform some kind of numerical
manipulation that will raise a TypeError if one is not passed. If the
operation succeeds, then the object supported all the operations you
asked of it, so in what sense would the program be doing the wrong
thing?

It might not support *all* the operations. Consider a toy function like
this:

def toy(x): # Don't try this at home!
return x*10000000000 + 1

If you pass a non-empty list, Python will:

- try to allocate a chunk of memory of at least 4 GB (estimated), which
may cause quite a bit of thrashing;

- if somehow this succeeds, then it will create a list and populate it
with rather a lot of duplicated references;

- at which point it will then try to add a list to an int, and raise an
exception;

- and then deallocate a huge list, causing more thrashing.


So there are consequences to allowing exceptions to occur in arbitrary
places.

There's also the principle that it is best to raise an exception as early
as possible. It's easier to track down errors at the point they are
introduced than long afterwards.

And finally, even worse than exceptions are silent failures of semantics:
code that does the wrong thing rather than fail loudly and safely. Just
because the code doesn't fail, doesn't mean it has worked, and an
exception is much better than a silent failure. You seem to be making the
classic mistake of thinking that exceptions are something to avoid:

"I find it amusing when novice programmers believe their main job is
preventing programs from crashing. [...] More experienced programmers
realize that correct code is great, code that crashes could use
improvement, but incorrect code that doesn’t crash is a horrible
nightmare." -- Chris Smith

http://cdsmith.wordpress.com/2011/01/09/an-old-article-i-wrote/


Duck-typing is not a panacea, and it too has failure modes. The usual
example is, suppose you have a graphics application that expects an
object with a "draw" method:

pencil.draw()
paintbrush.draw()
crayon.draw()
six_shooter.draw() # bang, you've just shot yourself in the foot


So I lean very strongly to some sort of explicit check ahead of time. I'm
just not sure *what sort* of explicit check.
 
C

Chris Angelico

A third option is not to check x at all, and hope that it will blow up
at some arbitrary place in the middle of my code rather than silently
do the wrong thing. I don't like this idea because, even if it fails,
it is better to fail earlier than later.
[...]

With
the specific examples given, I wouldn't like to use "x + 0" as a check;
it seems dodgy. Firstly because it doesn't look like a data type check

Strange. To me, it looks like "oh, you're testing whether x supports
numeric addition". I suppose the hidden assumption I'm making is that if
x supports addition, it is a kind of number. Perhaps that's an unsafe
assumption.

If your code bombs and the exception traceback shows a line saying "x
+ 0", I'm going to wonder if it's a massive bug... or, worse, if
you're expecting x.__add__() to have stupid side effects ("yeah,
adding 0 to x turns x into a float, but subtracting 0 from it forces
it to be an int").
But surely duck-typing tells us that if database cursors support the same
sort of operations that numbers support, we should therefore treat them
as a kind of number?

Depends what operations you mean. It's not uncommon for a
cursor/iterator object to support "x+=1" to move to the next
row/element/whatever; if your result set is maintained elsewhere, you
could cheaply support "x + 4" to create a new iterator that's four
rows further along. But in all other respects, it's not a number. You
can't multiply an iterator by 2, for instance.
My library will not try to prevent the caller taking the average of (say)
a dozen eggs and 10 miles. I'm not sure that it should try to prevent the
caller from trying to take the average between two database cursors.

Well, sure... if your code is so simple and trivial, and if the
cursors support "x - y" to get the number of rows between them. But in
that case, go for true EAFP and eschew error checking altogether.

ChrisA
 
C

Chris Angelico

There's also the principle that it is best to raise an exception as early
as possible. It's easier to track down errors at the point they are
introduced than long afterwards.

Yes, definitely, especially (as was mentioned) if you're working with
callbacks. But I'd use isinstance then.

ChrisA
 
I

Ian Kelly

You seem to be making the
classic mistake of thinking that exceptions are something to avoid:

Far from it. You've extrapolated a lot more than what I actually
said, and I completely agree with everything you wrote. I was
explaining EAFP as I see it, not advocating it for all circumstances.

Although since you bring it up, I find that the LBYL crowd tends to be
more prone to exception avoidance, e.g. returning None on a failure
rather than raising an exception, whereas the EAFP crowd seems more
likely to just let the original exception propagate up.
 
S

Steven D'Aprano

Yes, definitely, especially (as was mentioned) if you're working with
callbacks. But I'd use isinstance then.


I'm leaning towards an isinstance check

I've been using Python since Python 1.5. Even though 1.5 had an
"isinstance" function, I learned Python from books and code written for
Python 1.4 which did not have that function, so the usual way to do type-
checking was:


if type(obj) is type(1):
# it's an int


and strongly discouraged. So my instincts are still very strongly primed
to *not* do type-checking if I can avoid it. But in this case, I think I
agree with those suggesting the isinstance check is the right approach.
The main downside to this is that objects which delegate to a number will
not work unless they are explicitly registered with the Number ABC.


Thanks to everyone who responded.
 
T

Terry Reedy

The eternal conflict between "Look Before You Leap" and "Easier to Ask for
Forgiveness than Permission" (LBYL vs EAFP) continues...

A somewhat different answer is that it depends on what you want the
function to do, as documented and *tested*. And that partly depends on
whether it is educational code for humans, production app code, or
library code.

The test driven approach would be to write tests and then do what is
needed to get them to pass. Doctests, unittests, and my private function
test functions allow testing for raising a particular exception.
AssertRaises() is used, perhaps increasingly, in stdlib tests. The
absence of any tests for the response to 'bad' input suggests that the
responses are 'undefined'.

That said, I admit that Python's extensible class system makes bad-input
testing harder. I also think that anyone who uses non-builtin classes
outside of their intended use area has to take responsibility.

For instance:

def f(n, a):
if n < 0: raise ValueError('n cannot be negative')
b = 0
while n:
b = process(a, b)
n -= 1

looks like a safe LBYL function. But suppose n is an instance of a class
that perversely implements subtraction as addition (or as doing
nothing). Algorithm termination is based on the presumption that
'decrementing' a 'positive value' moves it 'toward 0' and can only be
done a finite number of times. Verifying that an input is a member of
that abstract class is not trivial ;-).
A third option is not to check x at all, and hope that it will blow
up at some arbitrary place in the middle of my code rather than
silently do the wrong thing.

A silent infinite loop is bad. Infinite recursion stopped with the
recursion limit check is less bad.

If tests pass with no check, then nothing need be done until one moves
from correctness to resource use.
 
P

Pete Forman

Steven D'Aprano said:
I want to check that a value is a number. [...]
I'm leaning towards an isinstance check

Well that is the answer to your question, whether the value *is* a
number. EAFP can answer the question whether the value *behaves* like a
number, where the criterion depends on what your code is aiming to do
with the value.

BTW what if the value is Not-a-Number? ;-)
 
S

Steven D'Aprano

Pete said:
Steven D'Aprano said:
I want to check that a value is a number. [...]
I'm leaning towards an isinstance check
[...]
BTW what if the value is Not-a-Number? ;-)

Nothing different, and hopefully exactly what the caller expects. As far as
Python is concerned, NANs are Numbers.

py> NAN = float('nan')
py> from numbers import Number
py> isinstance(NAN, Number)
True


If it's a float NAN, Python doesn't give you much control over what happens
next, but generally any arithmetic operation on a NAN will return a NAN
rather than raise.

If it's a Decimal NAN, the same applies:

py> NAN = decimal.Decimal('nan')
py> NAN + 0
Decimal('NaN')


If it's a Decimal SNAN (signalling NAN), then arithmetic operations signal
InvalidOperation, which by default will raise an exception:

py> SNAN = decimal.Decimal('snan')
py> SNAN + 0
Traceback (most recent call last):
...
decimal.InvalidOperation: sNaN
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Members online

No members online now.

Forum statistics

Threads
473,981
Messages
2,570,188
Members
46,731
Latest member
MarcyGipso

Latest Threads

Top