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.