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:
def is_bigger(x, y): """ Assumes x and y are ints Returns True if x is less than y, else False """What are some natural partitions to check for? I can think of three partitions: one where x is less than y, second where x is equal to y, and third where x is greater than y.
Blackbox testing
def sqrt(x, eps): """ Assumes x and eps are non-negative floats Returns res such that x - eps <= res*res <= x+eps"""
Case | x | eps |
boundary | 0 | 0.00001 |
perfect square | 25 | 0.00001 |
less than 1 | 0.05 | 0.00001 |
irrational square root | 2 | 0.00001 |
extremes | 2 | 1.0/2.0**64.0 |
extremes | 1.0/2.0**64.0 | 1.0/2.0**64.0 |
extremes | 2.0**64.0 | 1.0/2.0**64.0 |
extremes | 1/2.0**64.0 | 2.0**64.0 |
extremes | 2.0**64.0 | 2.0**64.0 |
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
abs(-1)
incorrectly returns -1
Debugging
Print statements
Debugging steps
Error messages - easy Consider the following program
test = [1, 2, 3]
test[4] # IndexError
int(test) #TypeError
a #NameError
'3'/4 #TypeError
a = len([1, 2, 3] print(a) #Syntax Error
Logic Errors - Hard
Exceptions and Assertions
test = [1, 7, 4] test[4] # IndexError
int(test) # TypeError
a # NameError
'a'/4 # TypeError
Other examples of exceptions: already seen common datatypes
SyntaxError
: Python can't parse programNameError
: Local or global name not foundAttributeError
: Attribute reference failsTypeError
: Operand does not have correct typeValueError
: Operand type okay, but value is illegalIOError
: IO system reports malfunction (e.g., file not found)Dealing with exceptions
try: a = int(input("Tell me one number:")) b = int(input("Tell me another number:")) print(a/b) except: print("Bug in user input.")
except
statement.
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?
raise Exception("descriptive string")
Exceptions as control flow
raise
keyword can be used to indicate an exception (with its name and
description) at runtime.except
block at the very end.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 blockNotice 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
:
None
value.
def avg(marklist): try: return sum(marklist)/len(marklist) except: print('warning: no marks data')This results in the following output:
warning: no marks data marks = [ [ ['rahul', 'tendulkar'], [80.0, 70.0, 85.0], 78.33333 ], [ [ 'sachin', 'dravid'], [100.0, 80.0, 74.0], 84.66667 ], [ ['test'], [], None ] ]Notice that the
None
value is output because avg
did not return anything in the except
block, which is equivalent to returning the None
value.
0.0
.
def avg(marklist): try: return sum(marklist)/len(marklist) except: print('warning: no marks data') return 0.0This results in the following output:
warning: no marks data marks = [ [ ['rahul', 'tendulkar'], [80.0, 70.0, 85.0], 78.33333 ], [ [ 'sachin', 'dravid'], [100.0, 80.0, 74.0], 84.66667 ], [ ['test'], [], 0.0] ]
def avg(marklist): try: return sum(marklist)/len(marklist) except: print('warning: no marks data') raise ValueError("marklist found empty")This results in the following output:
warning: no marks data Traceback: ... .... ValueError: "marklist found empty"
Assertions
assert
statement to raise an AssertionError
exception if assumptions not met.def avg(marklist): assert len(marklist) != 0, 'no grades data' return sum(marklist)/len(marklist)
AssertionError
if it is given an empty marklist
Assertions as defensive programming
z0_power_i == z0**i
at the beginning the body of a for-loop.Where to use assertions?
n=len(L)
, or z0_power_i == z0**i
at certain program points.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