Data Structures and Algorithms using Python
Copyright © 2023 by Rance Necaise
Table Of Contents
Data Structures and Algorithms using Python
Copyright © 2023
by Rance Necaise

1.9 Preconditions and Postconditions

When defining the operations of an ADT, we must include a specification of required inputs and the resulting output, if any. In addition, we must specify the preconditions and postconditions for each operation. A precondition indicates the condition or state of the ADT instance and inputs before the operation can be performed. A postcondition indicates the result or ending state of the ADT instance after the operation is performed. The precondition is assumed to be true while the postcondition is a guarantee as long as the preconditions are met. Attempting to perform an operation in which the precondition is not satisfied should be flagged as an error.

Consider the use of the pop(i) method for removing a value from a list.

myList = [0, 1, 2, 3, 4, 5]
i = 2
myList.pop(i)

When this method is called, the precondition states the supplied index must be within the legal range. Upon successful completion of the operation, the postcondition guarantees the item has been removed from the list. If an invalid index, one that is out of the legal range, is passed to the pop method, an exception is raised.

Now, consider the following code segment

myList = [0, 1, 2, 3, 4, 5]
myList.pop(8)

which results in an "Index out of range" exception

   Traceback (most recent call last):
     File "<stdin>", line 1, in <module>
   IndexError: pop index out of range

because we are trying to pop an element beyond the end of the list.

All operations have at least one precondition, which is that the ADT instance has to have been previously initialized. In an object-oriented language, this precondition is automatically verified since an object must be created and initialized via the constructor before any operation can be used. Other than the initialization requirement, some operations may not have any other preconditions. It all depends on the type of ADT and the respective operation. Likewise, some operations may not have a postcondition, as is the case for simple access methods, which simply return a value without modifying the ADT instance itself.

Throughout the text, we do not explicitly state the precondition and postcondition as such, but they are easily identified from the description of the ADT operations. For example, the definition of the slope operation for the Line Segment ADT indicates "The line segment can not be vertical."

When implementing abstract data types, it is important that we ensure the proper execution of the various operations by verifying any stated preconditions. The appropriate mechanism when testing preconditions for abstract data types is to test the precondition and raise an exception when the precondition fails. You then allow the user of the ADT to decide how they wish to handle the error, either catch it or allow the program to abort.

Python, like many other object-oriented programming languages, raises an exception when an error occurs. An exception is an event that can be triggered and optionally handled during program execution. When an exception is raised indicating an error, the program can contain code to catch and gracefully handle the exception; otherwise, the program will abort.

Python also provides the assert statement, which can be used to raise an AssertionError exception. The assert statement is used to state what we assume to be true at a given point in the program. If the assertion fails, Python automatically raises an AssertionError and aborts the program, unless the exception is caught.

Returning to the implementation of the slope method for the LineSegment class, a correct solution must first test the precondition. This is done using the assert statement and specifying what we expect to be True, that the line is not vertical:

assert not self.isVertical(), "Line segment can not be vertical."

If the assertion fails, which will happen when the line is vertical, Python will raise an AssertionError. If the error is caught and handled, the program continues. Otherwise, the program aborts and an error message is displayed:

   Traceback (most recent call last):
     File "line.py", line 4, in <module>
   AssertionError: Line segment can not be vertical.

The last line of the error message includes the string provided as the second argument to the assert statement. The purpose of this string is to provide the programmer with information to aide in debugging the error. The first argument of the assert statement can be any logical expression or a function call that returns a Boolean value.

The new implementation of the slope method is provided below:

def slope(self) :
  assert not self.isVertical(), "Line segment can not be vertical."
  rise = self._yCoordA - self._yCoordB
  run = self._xCoordA - self._xCoordB
  return rise / run    

Throughout the text, we use the assert statement to test the preconditions when implementing abstract data types. This allows us to focus on the implementation of the AD Ts instead of having to spend time selecting the proper exception to raise or creating new exceptions for use with our AD Ts. For more information on exceptions and assertions, refer to AppendixC.

A new listing of our implementation of the LineSegment class is provided below with the new line highlighted. The implementation of several operations is left as an exercise.

Program Listing
Program: line.py
  1. # Implementation of the Line Segment ADT in which the coordinates for
  2. # end points are stored in individual instance variables.
  3. from math import sqrt
  4.  
  5. class LineSegment :
  6.    # Creates a new LineSegment instance from the given end point coords.
  7.   def __init__(self, x0, y0, x1, y1) :
  8.     self._xCoordA = x0
  9.     self._yCoordA = y0
  10.     self._xCoordB = x1
  11.     self._yCoordB = y1
  12.  
  13.    # Returns the first end point of the line as a 2-tuple (x, y).
  14.   def endPointA(self) :
  15.     return (self._xCoordA, self._yCoordA)
  16.  
  17.    # Returns the second end point of the line as a 2-tuple (x, y).
  18.   def endPointB(self) :
  19.     return (self._xCoordB, self._yCoordB)
  20.  
  21.    # Returns a Boolean value indicating if this is a vertical line segment.
  22.   def isVertical(self) :
  23.     return self._xCoordA == self._xCoordB
  24.    
  25.    # Returns the Euclidean distance between the two endpoints.    
  26.   def length(self) :
  27.     xDiff = self._xCoordA - self._xCoordB
  28.     yDiff = self._yCoordA - self._yCoordB
  29.     dist = sqrt(xDiff ** 2 + yDiff ** 2)
  30.     return dist
  31.    
  32.    # Returns a new LineSegment instance that is the result of shifting
  33.    # this line segment in both the x- and y-direction.
  34.   def shift(self, xInc, yInc) :
  35.     newX0 = self._xCoordA + xInc
  36.     newY0 = self._yCoordA + yInc
  37.     newX1 = self._xCoordB + xInc
  38.     newY1 = self._yCoordB + yInc
  39.     return LineSegment(newX0, newY0, newX1, newY1)
  40.    
  41.    # Returns the slope of the line segment given as the rise over the run.
  42.   def slope(self) :
  43.     assert not self.isVertical(), "Line segement can not be vertical."
  44.     rise = self._yCoordA - self._yCoordB
  45.     run = self._xCoordA - self._xCoordB
  46.     return rise / run    
  47.    
  48.    # --- The remaining methods go here ---
Page last modified on July 30, 2023, at 08:15 PM