Create a class.

  I don’t deny there is a remarkable quantity of ivory—mostly fossil. We must save it, at all events—but look how precarious the position is—and why? Because the method is unsound.  

Joseph Conrad, Heart of Darkness, Chapter III

Documentation

  1. Objects

Classes and objects

The word class means “data type”, and the word object means “value”. For example, the following code puts a value of type int into the variable i, and puts an object of class Date into the variable d.

i = 10
s = "hello"
m = ["January", "February", "March"]
d = Date(12, 31, 2019)

I’ve oversimplified a bit. The above code does put the value 10 into i. But we already know that the above code does not actually put a string into s, or a list into m. It merely puts a reference to the string into s, and a reference to the list into m, and a reference to an object into d. We first encountered references in Sort.

The class definition

A class is a data type that is not built into the Python language. We must therefore create the class ourselves. To create a class, we have to execute the class definition in lines 10–85 of classDate.py. This class definition is a blueprint for each object of the class that we create. For example, it is a blueprint for the object d in line 89. (More precisely, it is the blueprint for the object of class Date which we create in line 89 and to which the variable d refers.)

Instance attributes

The d in line 89 contains three int variables named year, month, and day. (In fact, every object of class Date would contain three int variables named year, month, and day.) The variables contained in an object are called the instance attributes of that object; they are created in lines 36–38.

Instance methods

Line 99 shows that d also has a function named getMonth that belongs to it. (In fact, every object of class Date would have a function named getMonth that belongs to it.) A function that belongs to an object is called an instance method of that object. It looks like line 99 is calling getMonth with no arguments,

print(f"month = {d.getMonth()}")   #Call the instance method in line 46.
but line 99 is really doing this:
print(f"month = {getMonth(d)}")    #Call the instance method in line 46.
When getMonth is called from line 99, the argument self in line 46 is therefore the object d in line 99. The first argument of an instance method is always named self, and is always the object to which the instance method belongs.

Two different ways of belonging

When we say that the three instance attributes (i.e., variables) year, month, and day “belong to” d, we mean that they are stored inside of d. Each object of class Date has its own trio of instance attributes stored inside of it. We could have

d = Date(12, 31, 2019)
e = Date( 7,  4, 1776)

But when we say that line 99 calls the instance method (i.e., function) named getMonth that “belongs to” d, we do not mean that the instance method is stored inside of d. All we mean is that a reference to d is being passed by line 99 as the self argument of the function. Both of the following statements call the same getMonth function, but they pass it different arguments.

print(d.getMonth())   #really doing print(getMonth(d))
print(e.getMonth())   #really doing print(getMonth(e))

The __str__ method

__str__ has two underscores in front, and two in back.

Line 99 calls the getMonth function that belongs to d (i.e., it passes d as the self argument of the instance method getMonth in line 46). Similarly, line 96 calls the __str__ function that belongs to d (i.e., it passes d as the self argument of the instance method __str__ in line 54). The __str__ instance method of an object should always return a string containing a picture of the values of the instance attributes inside of the object. See also __str__ vs. __repr__.

Line 95 does the same thing as 96. In other words, when you pass an object to the str function, you’re really passing the object to the __str__ function that belongs to the object.

Line 94 does the same thing as 95. In other words, when you print an object, you’re really converting the object to a string and printing the string.

Instance methods that read and write the instance attributes

The following instance methods return information about the content of the object to which they belong:

  1. getYear (lines 42, 101, and 104)
  2. getMonth (lines 46 and 99)
  3. getDay (lines 50 and 100)
  4. __str__ (lines 54 and 94–96)
  5. dayOfYear (lines 58 and 104)

The following instance methods can change the content of the object to which they belong:

  1. nextDay (lines 62, 78, and 105)
  2. nextDays (lines 74 and 107)

Note that line 78 is an example of one instance method (nextDays) calling another instance method (nextDay) belonging to the same object.

The __init__ method

Like __str__, the __init__ method has two underscores in front, two in back.

Line 89 calls the __init__ instance method in line 32. It looks like line 89 is passing three arguments to this method, but it is actually passing four. Whenever you call an instance method, you are actually passing one extra argument in addition to the arguments you write in the parentheses. We saw this for the first time when line 99 called the getMonth in line 46.

An __init__ method should error-check all its arguments (lines 33–35) before storing any of them in the instance attributes of the newborn object (lines 36–38). In other words, the object in the throes of birth should end up being 100% constructed or 0% constructed. A partially constructed object is a greater source of bugs than no object at all.

Instead of assert, an __init__ method could also respond to a bad argument by raise’ing an exception such as ValueError. And instead of the assignment statements in lines 36–38, we could have called the built-in function setattr. But setattr is intended for situations where you don’t know the name of the instance attribute when you’re writing the script; we’ll see an example in line 88 of models.py in Tweepy.

See also __init__ vs. __new__.

Class attributes

Each object of class Date contains its own year instance attribute (the self.year in lines 36, 44, 56, and 72). If you create 100 objects of class Date, you will have 100 year variables.

But all the objects of class Date share the same lengths variable (the list in lines 16, 35, 60, 64, and 83). If you instantiate (create) 100 objects of class Date, you will have exactly one lengths variable. In fact, you will also have exactly one lengths variable before you instantiate any object of class Date, and also after the last object of class Date has been destroyed, and also even if your script never instantiates any objects of class Date.

A variable that is shared by all the objects of a class is called a class attribute. lengths is a class attribute because the point at which it was created, in lines 16–30, is within the class definition (lines 10–85) but outside the definition of any method (function).

Since a class attribute belongs to no object, do not write the word self in front of it when you use it (lines 35, 60, 64, and 83). Write the name of the class (Date) in front of it.

Selfless methods

The method monthsInYear (lines 81 and 88) always returns 12. This method belongs to class Date because its definition in lines 80–83 is within the class definition in lines 10–85. But this method does not belong to any object of class Date, i.e., it does not receive any self argument in line 81. Let’s call this a selfless method, although the official name is staticmethod (line 80). A selfless method can use the class attributes (e.g., the Date.lengths in line 83), but it has no self object to which it belongs (i.e., it receives no self argument), and therefore cannot mention self.year, self.month, or self.day. (We will see later that there is another kinds of selfless method, called a class method.)

To emphasize that monthsInYear belongs to no object, we deliberately call this selfless method in line 88 at a time when no object of class Date exists. The first Date object is not created until later, in line 89.

Why write a function that always returns the same number? Just in case we ever have to generalize this class to work with non-Gregorian calendars. In the Jewish calendar, for example, some years have 13 months.

The Python script

classDate.py

months in year = 12
type(d) = <class '__main__.Date'>

d = 12/31/2019
d = 12/31/2019
d = 12/31/2019

month = 12
day = 31
year = 2019

12/31/2019 is day number 365 of the year 2019.
01/01/2020 is the next day.
01/08/2020 is a week after that.

Things to try

  1. Observe that the script still works correctly if you change line 99 from
    print(f"month = {d.getMonth()}")   #Print the return value of the instance method in line 46.
    
    to
    print(f"month = {d.month}")        #Print the value of the instance attribute in line 37.
    
    Then change it back because you should never do this. Code that is outside of the class definition (i.e., outside of lines 10–85) should never mention the attributes of a class. (One exception: an object of the class Person in Attributes had no methods. Its only purpose was to carry around a bunch of instance attributes without using them in any calculations. It would be okay to mention the attributes of a Person outside of the class definition.)
  2. Create the following twelve class attributes at line 15 or at line 31. They are int variables.
        january = 1
        february = 2
        march = 3
        april = 4
        may = 5
        june = 6
        july = 7
        august = 8
        september = 9
        october = 10
        november = 11
        december = 12
    
    Then change line 89 from
    d = Date(12, 31, 2019)
    
    to
    d = Date(Date.december, 31, 2019)
    
    to make it clear what the number 12 means. After all, suppose the date had been
    d = Date(12, 11, 2019)   #December 11 or November 12?
    
  3. Don’t reinvent the wheel. Change line 68 from
                if self.month < len(Date.lengths) - 1:
    
    to
                if self.month < Date.monthsInYear():
    
    And don’t hardcode in the number 12 into line 34. Change this line from
        assert isinstance(month, int) and 1 <= month <= 12
    
    to
        assert isinstance(month, int) and 1 <= month <= Date.monthsInYear()
    
  4. We called the selfless method monthsInYear in line 88 without using any object:
    print(f"months in year = {Date.monthsInYear()}")
    
    Verify that we can also call this method with an object and a dot in front of it:
    #after creating d in line 89
    print(f"months in year = {d.monthsInYear()}")
    
    This is a bit misleading because it makes it look like the method uses the instance attributes inside of d, which it doesn’t. The function decorator @staticmethod in line 80 is what makes it possible to write d.monthsInYear() anyway, as well as the more sensible Date.monthsInYear().

    For the commercial at sign @, see decorator expressions, decorated classes, and the other function decorator @classmethod below.

  5. Add another static method to class date. Name it daysInYear, decorate it with @staticmethod, give it no arguments (not even a self), and have it return the sum of the numbers in lengths[1:]. (It should return 365.) Do not accidentally include the None in line 17 in the sum. Use the sum function as in line 60.
  6. I wish we could give class Date an additional __init__ method that could be called with one argument:
    newYearsDay = Date(2019)   #Create a Date containing January 1, 2019.
    
    Here’s how to get almost the same effect. Add the following method to class Date immediately after the __init__ method. I wanted to name the first argument class, but I couldn’t do that because class is a keyword.
        @classmethod
        def newYearsDay(cls, year):
            print(f"cls = {cls}")
            print(f"type(cls) = {type(cls)}")
            return cls(1, 1, year)    #calls Date(1, 1, year)
    
    Create another object of class Date after creating d.
    newYearsDay = Date.newYearsDay(2019)   #ultimately calls Date(1, 1, 2019)
    print(f"newYearsDay = {newYearsDay}")
    
    cls = <class '__main__.Date'>
    type(cls) = <class 'type'>
    newYearsDay = 01/01/2019
    
  7. Add two more instance methods to class date, named prevDay and prevDays. They should be just like nextDay and nextDays, except that they should go backwards. Make sure that the day before a New Year’s Day is the New Year’s Eve of the previous year. Do not inadvertently reintroduce a hardcoded number 12 like the one you eliminated in the above exercise 3. Do not change the contents of the class attribute lengths in lines 16–30. For example, lengths should continue to hold only one copy of the length of December.
  8. Make it possible to get the distance in days between two Date objects by using the subtraction operator:
    d = Date(Date.december, 31, 2020)
    e = Date(Date.december, 31, 2019)
    
    print(d - e)   #means print(__sub__(d, e))
    
    365
    
    Add the following instance method to class Date. Two underscores in front, two in back.
        def __sub__(self, other):
            """
            Return the distance in days between the two Date objects.
            The return value is positive is self is later than other,
            negative if self is earlier than other, and zero if they're the same Date.
            """
            iself  = 365 *  self.year +  self.dayOfYear()
            iother = 365 * other.year + other.dayOfYear()
            return iself - iother
    
  9. Make it possible to compare two Date objects with a < sign to find out if the left one is earlier than the right one.
    d = Date(Date.december, 31, 2020)
    e = Date(Date.december, 31, 2019)
    
    if e < d:   #means if __lt__(e, d):
        print(f"{e} is earlier than {d}.")
    else:
        print(f"{e} is equal to or later than {d}.")
    
    12/31/2019 is earlier than 12/31/2020.
    
    Add the following instance method to class Date. Lowercase LT stands for “less than”; two underscores in front, two in back. __lt__ does almost all of its work by calling the __sub__ we wrote in the previous exercise.
        def __lt__(self, other):
            "Return True if self is earlier than the other Date, False otherwise."
            return self - other < 0   #means return __sub__(self, other) < 0
    
    Could you write a __lt__ that does its work without calling __sub__? Would it be faster than the above __lt__?
  10. I invented this class Date only to show you how to create a class and objects thereof. But if all you wanted to do was perform date arithmetic, you could do it with Python’s existing classes datetime.date and datetime.timedelta:
    import datetime
    
    print("months in year =", datetime.date.max.month)
    d = datetime.date(2019, 12, 31)   #d is an object of class datetime.date
    print(f"type(d) = {type(d)}")
    print()
    
    #These three statements do the same thing:
    print(f"d = {d}")
    print(f"d = {str(d)}")
    print(f"d = {d.__str__()}")
    print()
    
    print(f"month = {d.month}")
    print(f"day = {d.day}")
    print(f"year = {d.year}")
    print()
    
    tuple = d.timetuple()       #tuple is a named tuple
    dayOfYear = tuple.tm_yday   #dayOfYear is an int
    print(f"{d} is day number {dayOfYear} of the year {d.year}.")
    
    delta = datetime.timedelta(days = 1)
    d += delta                  #means d = d + delta
    print(f"{d} is the next day.")
    
    delta = datetime.timedelta(days = 7)
    d += delta                  #means d = d + delta
    print(f"{d} is a week after that.")
    
    months in year = 12
    type(d) = <class 'datetime.date'>
    
    d = 2019-12-31
    d = 2019-12-31
    d = 2019-12-31
    
    month = 12
    day = 31
    year = 2019
    
    2019-12-31 is day number 365 of the year 2019.
    2020-01-01 is the next day.
    2020-01-08 is a week after that.