CIS 1051 - Temple Rome Spring 2023¶

Intro to Problem solving and¶

Programming in Python¶

LOGO

LOGO

Classes and Functions¶

Prof. Andrea Gallegati

( tuj81353@temple.edu )

Let's write functions that take programmer-defined objects as parameters and return them as results.

Time¶

In [1]:
class Time:
    """Represents the time of day.
       
    attributes: hour, minute, second
    """

this class records the time of day. Let's create a new Time object to assign attributes for hours, minutes, and seconds:

In [2]:
time = Time()
time.hour = 11
time.minute = 59
time.minute = 30

... and a function to print it out

In [9]:
def print_time(time):
    print('%.2d:%.2d:%.2d'  % (time.hour, time.minute, time.minute))

The format sequence '%.2d' prints an integer using at least two digits, including a leading zero if necessary.

Pure Functions¶

The prototype and patch development plan, to tackle complex problems, just starts with a simple prototype and incrementally deals with the complications.

In [4]:
def add_time(t1, t2):
    sum = Time()
    sum.hour = t1.hour + t2.hour
    sum.minute = t1.minute + t2.minute
    sum.second = t1.second + t2.second
    return sum

This simple prototype of add_time

  • creates a new Time object
  • assigns its attributes
  • returns a reference to the new object

is a pure function: not modifying any object passed as argument and with no effect (e.g. printing value), but returning a value.

Let's pass a couple of Time objects to test it:

  • start time of "Monty Python and the Holy Grail" movie
  • its runtime (1 hour 35 minutes)

To figure out when the movie will be done

In [11]:
start = Time()
start.hour = 9
start.minute = 45
start.second =  0

duration = Time()
duration.hour = 1
duration.minute = 35
duration.second = 0

done = add_time(start, duration)
print_time(done)
10:80:80

The result might not be what you were hoping for.

... not dealing with cases where seconds/minutes are more than sixty.

We have to “carry”

  • the extra seconds into the minute column
  • the extra minutes into the hour column
In [13]:
def add_time(t1, t2):
    sum = Time()
    sum.hour = t1.hour + t2.hour
    sum.minute = t1.minute + t2.minute
    sum.second = t1.second + t2.second

    if sum.second >= 60:
        sum.second -= 60
        sum.minute += 1

    if sum.minute >= 60:
        sum.minute -= 60
        sum.hour += 1

    return sum

Although correct, we could dream of a shorter alternative.

Modifiers¶

It turns useful for a function to modify the objects it gets as parameters.

In [ ]:
def increment(time, seconds):
    time.second += seconds

    if time.second >= 60:
        time.second -= 60
        time.minute += 1

    if time.minute >= 60:
        time.minute -= 60
        time.hour += 1

this is a modifiers, as these changes are visible to the caller.

  • the first line performs basic operations
  • the remainder deals with special cases

What happens if seconds is **much greater** than 60?

Not enough to "carry" once: just keep doing it until time.second is less than 60.

Thus, replacing if statements with** while statements** (not very efficient).

Anything done with modifiers can also be done with pure functions.

Some programming languages allows pure functions only.

Programs that use pure functions are faster to develop and less error-prone, but modifiers are convenient at times and usually more efficient!

Better to:

  • write pure functions whenever it is reasonable
  • resort to modifiers if there is a compelling advantage

( functional programming style )

Prototyping versus Planning¶

Following a prototype and patch development plan:

  • write a prototype with basic calculations
  • then test it
  • patch errors along the way

Really effective, without a deep understanding of the problem.

However, incremental corrections generates code:

  • unnecessarily complicated (special cases to deal with)
  • unreliable (did you really find all the errors?)

Alternatively, follow a designed development plan:

  • high-level insight into the problem
  • much easier programming

For example, Time object is a three-digit number in base 60 !

(see http://en.wikipedia.org/wiki/Sexagesimal)

... suggesting another approach:

convert Time to integers, taking advantage of their simpler arithmetic!

In [16]:
def time_to_int(time):
    minutes = time.hour * 60 + time.minute
    seconds = minutes * 60 + time.second
    return seconds
In [17]:
def int_to_time(seconds):
    time = Time()
    minutes, time.second = divmod(seconds, 60)
    time.hour, time.minute = divmod(minutes, 60)
    return time

and the way back, using divmod to divide: this returns the quotient and remainder as a tuple.

This approach requires to:

  • think a bit more
  • run some tests

before geting convinced the solution is correct. For example:

In [21]:
x = 120
time_to_int(int_to_time(x)) == x
Out[21]:
True

Once convinced, let's rewrite add_time.

In [ ]:
def add_time(t1, t2):
    seconds = time_to_int(t1) + time_to_int(t2)
    return int_to_time(seconds)

We get a program that is:

  • shorter
  • easier to read/debug
  • more reliable
  • easier to extend

( imagine subtracting two `Times` to find the duration in between)

Base conversion is more abstract.

Dealing with Time objects is more intuitive, but:

  • if we have the insight to treat Time as base 60 numbers
  • if we make the investment of writing the conversion functions

... ironically: making this problem harder (more general) it becomes easier (fewer special cases/possible errors).

Debugging¶

Time objects are well-formed if:

  • minute and second are between 0 and 60 (not included)
  • hour is positive
  • hour and minute are integers values
  • second can be a float

These requirements are invariants because they should always be true.

... otherwise, something has gone wrong.

Write code (boolean functions) to check invariants helps to detect errors, for example:

In [22]:
def valid_time(time):
    if time.hour < 0 or time.minute < 0 or time.second < 0:
        return False
    if time.minute >= 60 or time.second >= 60:
        return False
    return True

To validate arguments at the beginning of each function

In [23]:
def add_time(t1, t2):
    if not valid_time(t1) or not valid_time(t2):
        raise ValueError('invalid Time object in add_time')
    seconds = time_to_int(t1) + time_to_int(t2)
    return int_to_time(seconds)

Even using an assert statement, to raise an exception:

In [24]:
def add_time(t1, t2):
    assert valid_time(t1) and valid_time(t2)
    seconds = time_to_int(t1) + time_to_int(t2)
    return int_to_time(seconds)

(it distinguishes code that checks for errors)