Bundling information into objects in Python (Part 2)


Functions versus methods

Now that we've seen how to use methods, we can summarize how they differ from functions.

Function

Defined outside class

Object can be any parameter

Parameter has any name

Function call syntax

All inputs in parentheses

Method

Defined inside class definition

Object is the first parameter

Parameter has name self

Dot notation for call

First input before dot, rest in parentheses

We've seen that functions are defined outside the class definition but methods are defined inside the class definition.

There are no restrictions on the order of parameters to a function, but in a method, the object is the first parameter.

Moreover, although there are no restrictions additional on the name of the parameter for a function, for a method the name is self.

There are also differences on how these are used. For a function, we use the syntax for a function call, where all inputs are put in parentheses after the name of the function.

For the method, we use dot notation. That is, the object corresponding to the name self is put before the dot, and all other inputs are put in parentheses after the dot.


Built-in functions

Python has some built-in functions that can be used with any type of object.

For example, isinstance consumes the name of an object and the name of a class and returns True if the object is of that class. Example:

isinstance(eye, Circle)

Calling the function isinstance on the inputs eye and Circle will produce True, since the object eye is a Circle.

hasattr determines whether an object has an attribute of the given name.

hasattr(eye, "colour")

Notice that the attribute name is given as a string. Calling the function on eye and the string "colour" will produce True, since colour is one of the attributes of eye.

Finally, there is our old friend dir, the directory function.

dir(Circle)

When called on a user-defined class, it lists any method we have created, as well as other automatically-created information about the class, much of which is beyond the scope of this course.


Example: Time class

Suppose we want to create a class for time.

class Time:

    """Time stored as hour and minutes

       Methods:
       __init__: initializes a new object
       __str__: prints an object

       Attributes:
       hour: int, 0 <= value < 24
       minute: int, 0 <= value < 60
    """

We start with a class statement, and then provide a docstring that gives a summary, followed by any methods that can be used (here __init__ and __str__) and the types and restrictions on the attributes.

We'll use 24-hour time here.


    def __init__(self, hour, minute):
        """Initializes a new object.

           Preconditions:
           hour: int, 0 <= value < 24
           minute: int, 0 <= value < 60

           Parameters:
           hour: hour in time
           minute: minutes in time

           Side effect: attributes set with values
        """
        self.hour = hour
        self.minute = minute

We create an initialization method, which will allow us to assign hours and minutes to create new objects.

Our initialization method has a docstring, which looks like the docstring for a function. That is, aside from self, we mention the preconditions and parameters, and the side effect that occurs.


    def __str__(self):
        """Prints time.

           Side effect: prints 
        """
        if self.minute < 10:
            minute_word = "0" + str(self.minute)
        else:
            minute_word = str(self.minute)
        return str(self.hour) + ":" + \
            minute_word

We also create a function to print a time.

We can put a colon between the hours and minutes, but first we need to add an extra zero if the number of minutes is less than 10.

We also need to make sure that anything being concatenated has been converted to a string.


lunch = Time(12, 0)
print(lunch)

Here we create a time object and print it, giving the following output:


12:00
lunch.minute = 30
print(lunch)
print(isinstance(lunch, Time))
print(hasattr(lunch, "minute"))
print(Time.__doc__)
print(dir(Time))

We can use mutation to change the value of an attribute. We can use the isinstance function, the hasattr function, print out the docstring, and see everything that is automatically created for the Time class. Appending this code now generates the following:


12:00
12:30
True
True
Time stored as hour and minutes

       Methods:
       __init__: initializes a new object
       __str__: prints an object

       Attributes:
       hour: int, 0 <= value < 24
       minute: int, 0 <= value < 60
    
['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__']

Adding a method

Now let's create a method which compares two time objects and returns the earlier of the two times, or the second time if the times are equal.

class Time:

    """Docstring here.
    """

    def __init__(self, hour, minute):
        """Details omitted.
        """

    def __str__(self):
        """Details omitted.
        """

    def earlier_time(self, other):
        """Determines earlier of two Times.

           Preconditions:
           other: Time object

           Parameters:
           other: Time compared to self

           Returns: earlier of two times, 
           or other if equal 
        """

In our header we have both self, that is, the time object to which the function is applied using dot notation, and other, the second time, which appears in parentheses when the function is called. The input other is also a time object.


How do we figure out whether self or other is an earlier time?

        if self.hour < other.hour:
            return self

If the hours are different, then we can ignore the minutes. So if the hour for self is earlier than the hour for other, we can return self.


        elif other.hour < self.hour:
            return other

If the hour for other is earlier than the hour for self, we can return other.

In all remaining cases, the values for hour are the same. So now all we need to do is compare the values for minute.


        elif self.minute < other.minute:
            return self

That is, if the minute value for self is less, we return self.


        else:
            return other

Otherwise, we return other. Notice that this means that if self and other are equal, it is other that will be returned, which is what we claimed would happen.


lunch = Time(12, 0)
eight_forty = Time(8, 40)
eight_five = Time(8, 5)
eight_five_again = Time(8, 5)

We create some more times for our tests.


def test_earlier():
    assert(lunch.earlier_time(eight_forty)) \
    == eight_forty
    assert(eight_forty.earlier_time(lunch)) \
    == eight_forty
    assert(eight_forty.earlier_time(eight_five)) \
    == eight_five
    assert(eight_five.earlier_time(eight_forty)) \
    == eight_five
    assert(eight_five_again.earlier_time \
    (eight_five)) is eight_five

test_earlier()

Notice how the method is used in each statement.

We have the value of the first parameter, a dot, the name of the function, and then any remaining parameters, here just other, in parentheses.

We check that eight_forty is given as earlier than lunch, even though the minutes value is larger, and that eight_five is earlier than eight_forty, since minutes are being compared when hours are the same.

In our final assertion we use is rather than == to show that when two objects of equal value are used, it is other that is returned.