Defensive Programming

When we program, we often miss certain important aspects, and introduce potential errors in the programs, that may manifest only for certain inputs. For example, it is estimated that even in mature software, it is common to find at least one bug in every thousand lines of code. Defensive programming is a term used to describe a collection of techniques that reduce the chances of errors (also called bugs) escaping into the program.

Two very common methods for defensive programming are:

Set yourself up for easy testing and debugging

When are you ready to test?

Classes of tests:

Testing approaches:

Blackbox testing

def sqrt(x, eps):
  """ Assumes x and eps are non-negative floats
      Returns res such that x - eps <= res*res <= x+eps"""

Glassbox testing

Path-complete testing is not necessarily enough

def abs(x):
  """ Assumes x is an int
      Returns x if x>=0 and –x otherwise """
  if x < -1:
    return –x
  else:
    return x

Debugging

Print statements

Debugging steps

Error messages - easy Consider the following program

test = [1, 2, 3]

Logic Errors - Hard

Exceptions and Assertions

Other examples of exceptions: already seen common datatypes

Dealing with exceptions

You can have separate except clauses to deal with a particular type of exception

try:
  a = int(input("Tell me one number:"))
  b = int(input("Tell me another number:"))
  print(a/b)
except ValueError:
  print("Could not convert to a number.")  # only executes if ValueError comes up
except ZeroDivisionError:
  print("Can't divide by zero.")  # only executes if ZeroDivisionError comes up
except:  # for all other errors
  print("Something else went wrong.")

What to do when you encounter an exception?

Exceptions as control flow

Syntax:
raise <exceptionName>(<arguments>)
Example:
raise ValueError("something is wrong")  #The string description is optional

Example: raising an exception

def get_ratios(L1, L2):
""" Assumes: L1 and L2 are lists of equal length of numbers
    Returns: a list containing L1[i]/L2[i] """
  ratios = []
  for index in range(len(L1)):
    try:
      ratios.append(L1[index]/L2[index])
    except ZeroDivisionError:
      ratios.append(float('nan')) #nan = not a number
    except:
      raise ValueError('get_ratios called with bad arg') #manage flow of program by raising own error
    return ratios

get_ratios("") #will raise ValueError.  Can enclose this within an except block
Notice that an exception searches for the innermost try/except block. If it does not exist in the current function, it exits the function (destroying its scope) and going to the caller's scope, caller's caller's scope, and so on, until a try/except block is found. If no try/except block is found, the program terminates and the user is shown the details of the exception raised.

Example: We are given a class list for a subject, such that each list entry is a list of two parts:

marks = [ [ ['rahul', 'tendulkar'], [80.0, 70.0, 85.0] ],
          [ ['sachin', 'dravid'], [100.0, 80.0, 74.0] ] ]
Create a new class list, with name, marks, and an average
[ [ ['rahul', 'tendulkar'], [80.0, 70.0, 85.0], 78.33333 ],
  [ [ 'sachin', 'dravid'], [100.0, 80.0, 74.0], 84.66667 ] ]

Example code

def get_stats(class_list):
  new_stats = []
  for elt in class_list:
    new_stats.append([elt[0], elt[1], avg(elt[1])])
  return new_stats

def avg(marklist):
  return sum(marklist)/len(marklist)

We will get an error on the following input:

marks = [ [ ['rahul', 'tendulkar'], [80.0, 70.0, 85.0] ],
          [ [ 'sachin', 'dravid'], [100.0, 80.0, 74.0] ],
          [ ['test'], [] ] ]
Get ZeroDivisionError: float division by zero because try to divide by len(marklist) which is 0 for test.

Multiple choices for handling these errors using except:

Assertions

Assertions as defensive programming

Where to use assertions?

Example:

cube = int(input("Enter a number: ")
for guess in range(abs(cube)+1):
  assert(guess**3 <= abs(cube))
  if guess**3 >= abs(cube):
    break
  assert(guess**3 < abs(cube))
if guess**3 != abs(cube):
  print(cube, 'is not a perfect cube')
else:
  if cube < 0:
    guess = -guess
  print('Cube root of ' + str(cube) + ' is ' + str(guess))
Another example:
cube = int(input("Enter a number: ")
epsilon = 0.01
low = 0
high = cube
guess = (low+high)/2.0
num_guesses = 0
while abs(guess**3 - cube) >= epsilon:
  assert(low <= guess)
  assert(guess <= high)
  if guess**3 < cube:
    low = guess
  else:
    high = guess
  guess = (high + low)/2.0
  num_guesses += 1
print 'num_guesses =', num_guesses
print(guess, 'is close to the cube root of', cube)

Example on longest common subsequence (LCS):

def isCommonSubsequence(X, Y, R):
  xi = 0
  yi = 0
  for c in R:
    while X[xi] != c:
      xi+=1
      if xi == len(X):
        return False
    while Y[yi] != c:
      yi+=1
      if yi == len(Y):
        return False
  return True

def lcs(X, Y):
  ret = ""
  if len(X) == 0 or len(Y) == 0:
    ret = ""
  elif X[-1] == Y[-1]:
    lcsXY = lcs(X[0:-1], Y[0:-1])
    ret = lcsXY + X[-1]
  else:
    lcsY = lcs(X[0:-1], Y)
    lcsX = lcs(X, Y[0:-1])
    if len(lcsY) < len(lcsX):
      ret = lcsX
    else:
      ret = lcsY
  assert(isCommonSubsequence(X, Y, ret))
  return ret

Example on placing N queens in an NxN chessboard

global N
N = 4

def printSolution(board):
    for i in range(N):
        for j in range(N):
            if board[i][j] == 1:
                print("Q",end=" ")
            else:
                print(".",end=" ")
        print()

def isSafe(board, row, col):

    # Check this row on left side
    for i in range(col):
        if board[row][i] == 1:
            return False

    # Check upper diagonal on left side
    for i, j in zip(range(row, -1, -1),
                    range(col, -1, -1)):
        if board[i][j] == 1:
            return False

    # Check lower diagonal on left side
    for i, j in zip(range(row, N, 1),
                    range(col, -1, -1)):
        if board[i][j] == 1:
            return False

    return True


def solveNQUtil(board, col):
    # Base case: If all queens are placed
    # then return true
    if col >= N:
        return True

    # Consider this column and try placing
    # this queen in all rows one by one
    for i in range(N):

        if isSafe(board, i, col):

            # Place the queen at board[i][col]
            board[i][col] = 1

            # Recurse to place rest of the queens
            if solveNQUtil(board, col + 1) == True:
                return True

            board[i][col] = 0

    return False

def solveNQ():
    board = [[0, 0, 0, 0],
             [0, 0, 0, 0],
             [0, 0, 0, 0],
             [0, 0, 0, 0]]

    if solveNQUtil(board, 0) == False:
        print("Solution does not exist")
        return False

    for i in range(0, N, 1):
      for j in range(0, N, 1):
        assert (!board[i][j] || isSafe(board, i, j))
    printSolution(board)
    return True