8 Robustness and Performance
Last edited
65 try/except/else/finally
try/finally
try/finallyare basicallydeferin go,finallywill always run, used for cleanup.
else
try- minimal code that can raise the caught exceptionelse- the follow-on work that assumes the try succeeded
Full example
UNDEFINED = object()
def divide_json(path):
handle = open(path, 'r+') # May raise OSError
try:
data = handle.read() # May raise UnicodeDecodeError
op = json.loads(data) # May raise ValueError
value = (op['numerator'] / op['denominator']) # May raise ZeroDivisionError
except ZeroDivisionError as e:
return UNDEFINED
else:
op['result'] = value
result = json.dumps(op)
handle.seek(0) # May raise OSError
handle.write(result) # May raise OSError
return value
finally:
handle.close() # Always runsIf the JSON is invalid, json.loads raises ValueError inside try. It isn’t caught by except ZeroDivisionError, so else is skipped, finally runs handle.close(), and then the exception is propagated up to the caller.
The most important part about this is any error caught or uncaught causes else be skipped and finally be run.
Question
Why not just dedent after the
try/except?
Answer
It depends on your code. If the following code isn’t dependent on the result of the
tryblock, then go ahead.
But the example above has two blockers:
elseruns beforefinally, andfinallydoeshandle.close()— so dedentedseek/writewould hit a closed file- it relies on
op/valuefrom thetryAnother option: if the
exceptreturns/raises, the error paths are handled, so dedenting is safe. (Shown below still usingelsefor clarity.)try: data = handle.read() op = json.loads(data) value = (op['numerator'] / op['denominator']) except: ... else: # else never runs if try raised, so op is safe to use here. # if you DEDENTED this instead and the except didn't return/raise, # you'd fall through with a possibly undefined op and crash. op['result'] = value
66 contextlib instead of try/finally
contextlib just a shortcut to write defer-style cleanups (try/accept) and encapsulate them.
Behind the scenes @contextmanager decortator defines the __enter__ and __exit__ dunders for you.
You can also use except/else in these functions. Or you could leave it to the caller
try:
with open_file() as f:
f.write('data')
except ValueError:
...from contextlib import contextmanager
@contextmanager
def open_file(path, mode):
f = open(path, mode)
try:
yield f
finally:
f.close()
with open_file('out.txt', 'w') as f:
f.write('data')69 Use decimal for precision
Broken using built-in float
rate = 1.45
cost = rate * (3*60+42) / 60 # 5.364999999999999
round(cost, 2) # 5.36 should be 5.37Fixed using decimal
Pass strings into Decimal not floats.
from decimal import Decimal, ROUND_UP
rate = Decimal('1.45')
seconds = Decimal(3*60 + 42)
cost = rate * seconds / Decimal(60) # 5.365 exactly
rounded = cost.quantize(Decimal('0.01'), rounding=ROUND_UP)
print(rounded) # 5.37Question
How does
Decimalget this right and why doesn’t python’sfloat?
Answer
Speed - decimal gives up speed. Decimal math (base-10) is not done at the cpu level like base-2 (typical floats), it’s done at the software level. Higher cpu cost and memory cost.