8 Robustness and Performance

Last edited

65 try/except/else/finally

try/finally

  • try/finally are basically defer in go, finally will always run, used for cleanup.

else

  • try - minimal code that can raise the caught exception
  • else - 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 runs

If 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 try block, then go ahead.
But the example above has two blockers:

  1. else runs before finally, and finally does handle.close() — so dedented seek/write would hit a closed file
  2. it relies on op/value from the try

Another option: if the except returns/raises, the error paths are handled, so dedenting is safe. (Shown below still using else for 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.37

Fixed 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.37

Question

How does Decimal get this right and why doesn’t python’s float?

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.