In some other languages, one can not recover from an error, or it is difficult to recover from an error, so one tests input before doing something that could provoke the error. This technique is called Look Before You Leap (LBYL)
For example, one must avoid dividing by zero.
Below is code that divides by numbers. When it gets the zero, it crashes.
In [1]:
numbers = (3, 1, 0, -1, -2)
In [2]:
def foo(x):
return 10 // x
for x in numbers:
y = foo(x)
print(f'foo({x}) --> {y}')
So one checks before dividing as shown below.
Checking before doing something is called "Look Before You Leap" (LBYL).
In [3]:
def foo(x):
if x == 0:
y = 0
else:
y = 10 // x
return y
for x in numbers:
y = foo(x)
print(f'foo({x}) --> {y}')
Another technique is to just try stuff, and if it blows up, do something else.
This technique is called Easier to Ask Forgiveness than Permission (EAFP).
Python makes it very easy to do something else when something blows up.
In [4]:
def foo(x):
try:
y = 10 // x
except ZeroDivisionError:
y = 0
return y
for x in numbers:
y = foo(x)
print(f'foo({x}) --> {y}')
For that simple example, EAFP does not have much if any benefit over LBYL. For that simple example, there is not much benefit in the size or readability of the code. However, for more complicated problems, EAFP lets one write much simpler and readable code.
We will use the example of determining if a string is a valid float for Python.
See 2.4.6. Floating point literals for what constitutes a valid float.
floatnumber ::= pointfloat | exponentfloat
pointfloat ::= [digitpart] fraction | digitpart "."
exponentfloat ::= (digitpart | pointfloat) exponent
digitpart ::= digit (["_"] digit)*
fraction ::= "." digitpart
exponent ::= ("e" | "E") ["+" | "-"] digitpart
Some code for that follows.
In [5]:
import re
def is_float(s, debug=False):
digit = f'([0-9])'
digitpart = f'({digit}(_?{digit})*)' # digit (["_"] digit)*
fraction = f'([.]{digitpart})' # "." digitpart
pointfloat = f'(({digitpart}?{fraction}) | ({digitpart}[.]))' # [digitpart] fraction | digitpart "."
exponent = f'([eE][-+]?{digitpart})' # ("e" | "E") ["+" | "-"] digitpart
exponentfloat = f'(({digitpart} | {pointfloat}) {exponent})' # (digitpart | pointfloat) exponent
floatnumber = f'^({pointfloat} | {exponentfloat})$' # pointfloat | exponentfloat
floatnumber = f'^[-+]?({pointfloat} | {exponentfloat} | {digitpart})$' # allow signs and ints
if debug:
regular_expressions = (
digit,
digitpart,
fraction,
pointfloat,
exponent,
exponentfloat,
floatnumber,
)
for s in regular_expressions:
print(repr(s))
# print(str(s))
float_pattern = re.compile(floatnumber, re.VERBOSE)
return re.match(float_pattern, s)
In [6]:
floats = '''
2
0
-1
+17.
.
-.17
17e-3
-19.e-3
hello
'''.split()
floats
Out[6]:
In [7]:
is_float('', debug=True)
In [8]:
for s in floats:
print(f'{s!r} -> {bool(is_float(s))}')
In [9]:
import re
def is_float(s):
digit = f'([0-9])'
digitpart = f'({digit}(_?{digit})*)' # digit (["_"] digit)*
fraction = f'([.]{digitpart})' # "." digitpart
pointfloat = f'(({digitpart}?{fraction}) | ({digitpart}[.]))' # [digitpart] fraction | digitpart "."
exponent = f'([eE][-+]?{digitpart})' # ("e" | "E") ["+" | "-"] digitpart
exponentfloat = f'(({digitpart} | {pointfloat}) {exponent})' # (digitpart | pointfloat) exponent
floatnumber = f'^({pointfloat} | {exponentfloat})$' # pointfloat | exponentfloat
floatnumber = f'^[-+]?({pointfloat} | {exponentfloat} | {digitpart})$' # allow signs and ints
float_pattern = re.compile(floatnumber, re.VERBOSE)
return re.match(float_pattern, s)
def safe_float(s, default=0.):
if is_float(s):
x = float(s)
else:
x = default
return x
def main(lines):
total = sum(safe_float(line) for line in lines)
print(f'total is {total}')
main(floats)
Now we try EAFP technique below.
In [10]:
def safe_float(s, default=0.):
try:
x = float(s)
except ValueError:
x = default
return x
def main(lines):
total = sum(safe_float(line) for line in lines)
print(f'total is {total}')
main(floats)
The EAFP code is much much simpler.
The LBYL version was very complicated. If there was a bug in the LBYL version, how would you find it? If you fixed it, how much confidence would you have that your fix is correct? How hard would it be to have test cases that covered all the edge cases? How much confidence would you have that the test cases were comprehensive?
In [11]:
from glob import glob
import os
for filename in sorted(glob('20170424-cohpy-except-*.py')):
print(79 * '#')
print(filename)
print()
with open(filename) as f:
print(f.read())
print()