Project 3: Objects in Python

In this project you will implement and test a Date class.

Begin by creating a file date.py:

class Date:
    """ a user-defined data structure that
        stores and manipulates dates
    """

    # the constructor is always named __init__ !
    def __init__(self, month, day, year):
        """ the constructor for objects of type Date """
        self.month = month
        self.day = day
        self.year = year


    # the "printing" function is always named __repr__ !
    def __repr__(self):
        """ This method returns a string representation for the
            object of type Date that calls it (named self).

             ** Note that this _can_ be called explicitly, but
                it more often is used implicitly via the print
                statement or simply by expressing self's value.
        """
        s =  "%02d/%02d/%04d" % (self.month, self.day, self.year)
        return s


    # here is an example of a "method" of the Date class:
    def isLeapYear(self):
        """ Returns True if the calling object is
            in a leap year; False otherwise. """
        if self.year % 400 == 0: return True
        elif self.year % 100 == 0: return False
        elif self.year % 4 == 0: return True
        return False

Understanding Date

Notice that in this Date class there are three data members:

Note that self is used to denote any object (that is, any variable or value) of class Date.

Methods are just functions…

Object-oriented programming tends to have some of its own names for familiar things. For example, method is the “OOP” name for function. In particular, a method is a function whose first argument is self.

Note that the Date class has an __init__ method and a __repr__ method. As we’ve discussed in class, Python expects to see these special methods in virtually every class. The double underscores before and after these method names indicate that these methods are special ones that Python knows to look for. In the case of __init__, this is the method that Python looks for when making a new Date object. In the case of __repr__, this is the method that Python looks for when it needs to represent the object as a string.

Notice the line

s =  "%02d/%02d/%04d" % (self.month, self.day, self.year)

in the repr method. This constructs a string with the month, day, and the year, formatted very nicely to have exactly two digits places for the month, two digit places for the day, and four for the year.

We’ve also defined our own isLeapYear method. There are no double-underscores here, because Python didn’t “expect” this method, but it certainly doesn’t “object” to it either. (Clearly our puns have no class!)

Note on “method”:

Traditionally, functions called by objects are called methods. There is no really good reason for this. They are just like functions—-the only thing special about them is that they are defined in a class and they are called after a dot or period following the name of an object. For example, try these:

>>> from date import *
>>> d = Date(4, 7, 2015)
>>> d.isLeapYear()
False

>>> d2 = Date(3, 15, 2016)
>>> d2.isLeapYear()
True

>>> Date(1, 1, 1900).isLeapYear()   # no variable needed!
False

What’s up with self?

One odd thing about the above example is that three different objects of type Date are calling the same isLeapYear code. How does the isLeapYear method tell the different objects apart?

The method does not know the name of the variable that calls it! In fact, in the third example, there is no variable name!

The answer is self. The self variable holds the object that calls the method, including all of its data members.

This is why self is always the first argument to all of the methods in the Date class (and in any class that you define): because self is how the method can access the individual data members in the object that called it.

Please notice also: this means that a method always has at least one argument, namely self. However, this value is passed in implicitly when the method is called. For example, isLeapYear is invoked in the example above as Date(1,1,1900).isLeapYear(), and Python automatically passed self, in this case the object Date(1,1,1900), as the first argument to the isLeapYear method.

Testing your initial Date class:

Just to get a feel for how to test your new datatype, try out the following calls:

# create an object named d with the constructor
>>> d = Date(4, 7, 2015)  # use another day if you prefer...

# show d's value
>>> d
04/07/2015

# a printing example
>>> print 'Tuesday is', d
Tuesday is 04/07/2015

# create another object named d2
# of *the same date*
>>> d2 = Date(4, 7, 2015)

# show its value
>>> d2
04/07/2015

# are they the same?
>>> d == d2
False

# look at their memory locations
>>> id(d)   # return memory address
413488      # your result will be different...

>>> id(d2)  # again...
430408      # and this should differ from above!

# check if d2 is in a leap year...
>>> d2.isLeapYear()
False

# yet another object of type Date
# a distant New Year's Day
# Q: where will you be on this date?
>>> d3 = Date(1, 1, 2020)

# check if d3 is in a leap year
>>> d3.isLeapYear()
True

copy and equals

Add the following code to your Date class and then test that it works.

def copy(self):
    """ Returns a new object with the same month, day, year
        as the calling object (self).
    """
    dnew = Date(self.month, self.day, self.year)
    return dnew

This copy method returns a newly constructed object of type Date with the same month, day, and year that the calling object has. Remember that the calling object is named self, so the calling object’s month is self.month, the calling object’s day is self.day, and so on.

Since you want to create a newly constructed object, you need to call the constructor. This is what you see happening in the copy method.

Try out these examples, which use next year’s New Year’s Day. First we don’t use copy:

>>> d = Date(1, 1, 2016)
>>> d2 = d
>>> id(d)
430542      # your memory address may differ
>>> id(d2)
430542      # but d2 should be the SAME as d!
>>> d == d2
True           # so this should be True...

Next, you’ll show that copy does make a deep copy (instead of a copy of only the reference, or “shallow” copy):

>>> import date; from imp import reload; reload(date); from date import *  # reload your file...
>>> d = Date(1, 1, 2016)
>>> d2 = d.copy()
>>> d
01/01/2016
>>> d2
01/01/2016

>>> id(d)
430568      # your memory address will differ
>>> id(d2)
413488      # but d2 should be different from d!
>>> d == d2
False         # thus, this should be false...

Next, add an equals method:

def equals(self, d2):
    """ Decides if self and d2 represent the same calendar date,
        whether or not they are the in the same place in memory.
    """
    if self.year == d2.year and self.month == d2.month and self.day == d2.day:
        return True
    else:
        return False

This method should return True if the calling object (named self) and the argument (named d2) represent the same calendar date. If they do not represent the same calendar date, this method should return False. The examples above show that the same calendar date may be represented at multiple locations in memory—in that case the == operator returns False. This method can be used to see if two objects represent the same calendar date, regardless of whether they are at the same location in memory.

As a reminder, here is the line to reload a file without exiting Python:

import date; reload(date); from date import *

Try these examples (after reloading date.py with the above line) to get the hang of how this equals method works.

>>> d = Date(1, 1, 2016)
>>> d2 = d.copy()
>>> d
01/01/2016
>>> d2
01/01/2016
>>> d == d2
False       # this should be False!

>>> d.equals(d2)
True        # but this should be True!

>>> d.equals(Date(1, 1, 2016))  # this is OK, too!
True

>>> d == Date(1, 1, 2016)       # this tests memory addresses
False                           # so it should be False

Now, the next part of the lab asks you to implement a few methods for the Date class from scratch.

Modifications

Add the following methods to your Date class. Be sure to add a docstring to each of the methods you write. (Recall that the term method refers to a function that is a member of a user-defined class.)

isAfter(self, d2)

This method should return whether (True/False) Date d2 is after Date self. Hint: see the isBefore example from class.

tomorrow(self)

This method should NOT RETURN ANYTHING. Rather, it should change the calling object so that it represents one calendar day after the date it originally represented. This means that self.day will definitely change. What’s more, self.month and self.year might change.

Make sure to test your method with some tricky examples. Here are a couple of randomly chosen ones to get you started:

>>> import date; from imp import reload; reload(date); from date import *  # re-read file...

>>> d = Date(12, 31, 2015)
>>> d
12/31/2015
>>> d.tomorrow()
>>> d
01/01/2016

>>> d = Date(2, 28, 2016)
>>> d.tomorrow()
>>> d
02/29/2016
>>> d.tomorrow()
>>> d
03/01/2016

addNDays(self, N)

This method only needs to handle nonnegative integer arguments N. Like the tomorrow method, this method should not return anything. Rather, it should change the calling object so that it represents N calendar days after the date it originally represented.

Don’t copy any code from the tomorrow method! Instead, consider how you could call the tomorrow method inside a for loop in order to implement this.

In addition, this method should print all of the dates from the starting date to the finishing date, inclusive of both endpoints. Remember that the line print(self) can be used to print an object from within one of that object’s methods.

Make sure to test your method. Here are some to start with:

>>> import date; from imp import reload; reload(date); from date import *  # re-read file...

>>> d = Date(4, 7, 2015)
>>> d.addNDays(3)
04/07/2015  # printing the first one is optional...
04/08/2015
04/09/2015
04/10/2015
>>> d
04/10/2015

>>> d = Date(4, 7, 2015)  # re-create this one
>>> d.addNDays(1132)
04/07/2015 
04/08/2015
... lots of dates skipped ...
05/12/2018
05/13/2018  
>>> d
05/13/2018    # graduation! (if you're now a first-year...)

You can check your own date arithmetic with this website: http://www.timeanddate.com/date/dateadd.html.

main

Your main class should be a program that tests all of the functionality that you added to your Date class. Make sure your main class tests all the functionality that you have implemented: isAfter, tomorrow, addNDays. How do you know if your methods are implemented correctly? Does running each method once verify their correctness, or do you need to run them multiple times? For example, there are many different scenarios that can occur for the end of a month or the end of a year (leap year or not). Make sure you test all the cases you can think of.

Assignment adapted from Harvey Mudd’s CS 5 Virtual Art Lab.

Grading

You will be graded on the following:

Submitting

When complete, your project should include the following python files:

Zip your project using the same steps as for the earlier project, giving the zip file the same name as your project3_uLogin folder name, and submit to canvas.