IT 117: Intermediate Scripting
Class 19
Tips and Examples
Review
New Material
Microphone
Graded Quiz
You can connect to Gradescope to take weekly graded quiz
today during the last 15 minutes of the class.
Once you start the quiz you have 15 minutes to finish it.
You can only take this quiz today.
There is not makeup for the weekly quiz because Gradescope does not permit it.
Solution to Homework 7
I have posted a solution to homework 7
here.
Let's take a look.
Homework 9
I have posted homework 9
here.
It is due this coming Sunday at 11:59 PM.
Questions
Are there any questions before I begin?
Tips and Examples
TypeError When Creating Objects
Review
The Constructor Is Special
- A
constructor
is a
method
that creates an object
- And returns a pointer to the object
- It has no
return
statement
- Every time you create a class you should have a constructor
Defining a Class
- The general format for a class is
class CLASS_NAME:
def __init__(self[, PARAMETER, ...]):
...
def METHOD_NAME(self[, PARAMETER, ...]):
...
- By convention, all class names in Python are Capitalized
- A class definition consists of a number of methods
- One of which has the special name __init__
- This method is a constructor, which is used to create the object
- The class is the template used to create the object
- The object is section of RAM ...
- that can hold many values ...
- and the
methods
that work on these values
- Objects do not have a name
- They only have a location in memory
- This location is stored in an object variable
- We use this variable to work with the object
- A class is the code used to create the object
Creating An Object from A Class
The self Variable
The Time Class
- Let's create another class, Time
- The constructor takes a time string using the format
HH:MM:SS
- HH is the hours on a 24 hour clock with values from 0
to 23
- MM is a two digit minute and SS is a two digit second
- Their values range from 0 to 59
- Here is the Time constructor
# expects a string of the form HH:MM:SS
# where hours are expressed in numbers from 0 to 23
def __init__(self, time_string):
self.hours, self.minutes, self.seconds = time_string.split(":")
self.hours = int(self.hours)
self.minutes = int(self.minutes)
self.seconds = int(self.seconds)
- Let's also create the format_time method
- This method will give us a nicely formated string representing the time
# returns a string in the form HH:MM:SS AM/PM
def format_time(self):
hours = self.hours
am_pm = "AM"
if hours > 12:
hours -= 12
am_pm = "PM"
return str(hours) + ":" + str(self.minutes) + ":" + str(self.seconds) + " " + am_pm
- The variables hours and
am_pm are local variables ...
- not attributes
- They only exist when the method is run
- We can use format_time as follows
>>> t1 = Time("10:45:30")
>>> t1.format_time()
'10:45:30 AM'
- Let's now add the method difference.
- It will return the difference between two times in seconds
# returns the difference in seconds between two times
def difference(self, other_time):
seconds = (self.hours - other_time.hours) * 60 * 60
seconds += (self.minutes - other_time.minutes) * 60
seconds += self.seconds - other_time.seconds
return seconds
- We can use this method as follows
>>> midnight = Time("00:00:00")
>>> t2 = Time("03:04:13")
>>> t2.difference(midnight)
11053
Changing the Attributes of a Class
- Classes have many advantages
- One is that we can change the internals of an object ...
- without affecting the client code that uses the object
- But to do this we must make sure ...
- that all the methods give the same results
- In the Time class we represented time with three
attributes
- Instead of using three attributes we can use only one,
seconds
- It will store the number of seconds since midnight
- This will make the difference calculation easier
- The first thing we need to do is to change the constructor
- It will still take a time string ...
- and turn it into integer values for
hours, minutes
and seconds
- But these now will be variables that are local to the constructor
- Here is the new code for the constructor
# expects a string of the form HH:MM:SS
# where hours are expressed in numbers from 0 to 23
def __init__(self, time_string):
hours, minutes, seconds = time_string.split(":")
hours = int(hours)
minutes = int(minutes)
seconds = int(seconds)
self.seconds = hours * 60 * 60 + minutes * 60 + seconds
- Now we need to modify format_time to use seconds
- Here is the new code for format_time
# returns a string in the form HH:MM:SS AM/PM
def format_time(self):
hours = self.seconds // (60 * 60)
remainder = self.seconds % (60 * 60)
minutes = remainder // 60
seconds = remainder % 60
am_pm = "AM"
if hours > 12:
hours -= 12
am_pm = "PM"
return str(hours) + ":" + str(minutes) + ":" + str(seconds ) + " " + am_pm
- The modified code is shown in red
- We also have to modify difference
- But here the code is much simpler
# returns the difference in seconds between two times
def difference(self, other_time):
return self.seconds - other_time.seconds
- other_time is a pointer to the
second time object
Attendance
New Material
Accessing Attributes Directly
Data Hiding
- By default, client code can use a variable that points to an object ...
- to change the value of any attribute
- This might seem harmless
- But it can lead to trouble
- The first version of our Time class had three
attributes
- Each of these attributes can be accessed using dot notation
>>> t1 = Time("14:35:12")
>>> t1.hours
14
>>> t1.minutes
35
>>> t1.seconds
12
- Since attributes are variables ...
- we can change them directly
>>> t1.hours = 15
>>> t1.minutes = 14
>>> t1.seconds = 13
>>> t1.format_time()
'3:14:13 PM'
- But the latest version of the Time class has one
attribute
>>> t1 = Time("14:35:12")
>>> t1.seconds
52512
t1.format_time()
'2:35:12 PM'
- A user can certainly change that value
- But it would take some calculating to get the right value for
seconds
- And it would be easy to make a mistake
- Worse than that, they might enter a totally ridiculous value
>>> t1.seconds = 1000000000
>>> t1.format_time()
'277765:46:40 PM'
- We should prevent things like this from happening
- To do so we have to change the class
- so the client code cannot change the attributes directly
- This is called
data hiding
- We hide attributes from the client code by changing their names
- Any attribute whose name begins with two underscores,
__ ...
- will be invisible to the client code
- Let's create a third version of our Time class
- In this version the name of the attribute will be
__seconds
- The constructor will now look like this
# expects a string of the form HH:MM:SS
# where hours are expressed in numbers from 0 to 23
def __init__(self, time_string):
hours, minutes, seconds = time_string.split(":")
hours = int(hours)
minutes = int(minutes)
seconds = int(seconds )
self.__seconds = hours * 60 * 60 + minutes * 60 + seconds
- It is no longer possible to access attributes directly
>>> t1 = Time("14:35:12")
>>> t1.__seconds
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'Time' object has no attribute '__seconds '
- But the methods still work the way they are supposed to ...
- as long as we change
self.seconds
to
self.__seconds
- With this slight alteration the new method works the same as the
older version
>>> t1.format_time()
'2:35:12 PM'
The __str__ Method
- Python classes can have certain methods that are not only hidden ...
- they are not called directly
- These methods have __ at the beginning and end of
their names
- They are sometime called
magic methods
...
- or dunder methods
- Dunder stands for "double underscore"
- These methods are never called directly by name
- Instead they are called whenever some action is taken
- They are called implicitly ...
- not explicitly
- __init__ is an example of a magic method
- The most commonly used magic method after
__init__ ...
- is __str__
- When the a Time object is created in the client
code
t1 = Time("11:54:12")
- The __init__ method of Time
is called
- Similarly, whenever you try to print an object
- its __str__ method is called ...
- if it has such a method
- __str__ creates a string representation of the object
- At the moment, the Time class has no
__str__
- If we create a Time object and print it here is what
we get
>>> t = Time("09:30:00")
>>> print(t)
<time_3.Time object at 0x1013e8358>
- The __str__ method must return a string ...
- that serves as a good representation of the object
- It is secretly called every time you print an object of the class ...
- or use the object where a string is required
- We can create an __str__ method for
Time ...
- by changing the name of the format_time method to
__str__
# returns a string formated as follows
# H[H] hours M[M] minutes S[S] seconds
def format_seconds (seconds ):
hours = seconds // (60 * 60)
remainder = seconds % (60 * 60)
minutes = remainder // 60
seconds = remainder % 60
return str(hours) + ' hours ' + str(minutes)+ ' minutes ' + str(seconds ) + ' seconds s
- This __str__ method will be called ...
- whenever we print an object
>>> print("Here is the time object we just created: ", t)
Here is the time object we just created: 9:30:0 AM
Accessor and Mutator Methods
- A well designed class hides its attributes
- But the client code might need the values they contain
- To make this happen we need a method for each attribute ...
- that the client code will be allowed to see and use
- These methods are called
accessors
- It is common practice for the name of an accessor to begin with "get" ...
- followed by the attribute name ...
- without the initial __
- So another name for an accessor is a
getter
- Let's say you need to keep track of each computer in an office
- You would need to record the following information about each machine
- Manufacturer
- Model
- Serial number
- Processor
- RAM
- Hostname
- Disk size
- The first thing we need to do is create a constructor
def __init__(self, manufacturer, model, serial_number, processor, ram, hostname, disk_size):
self.__manufacturer = manufacturer
self.__model = model
self.__serial_number = serial_number
self.__processor = processor
self.__ram = ram
self.__hostname = hostname
self.__disk_size = disk_size
- Since all the attributes are hidden we need to create an accessor for each
attribute
def get_manufacturer(self):
return self.__manufacturer
def get_model(self):
return self.__model
def get_serial_number(self):
return self.__serial_number
def get_processor(self):
return self.__processor
def get_processor(self):
return self.__processor
def get_ram(self):
return self.__ram
def get_hostname(self):
return self.__hostname
def get_disk_size(self):
return self.__disk_size
- The values of the following attributes should never change
- __manufacturer
- __model
- __serial_number
- __processor
- That is one of the reasons we hide attributes
- You wouldn't want someone to write code that would change the serial number
- But some attributes could change with time
- __ram
- __hostname
- __disk_size
- These attributes are hidden
- So they cannot be changed directly outside the class
- We need special methods to change each of these values
- Such methods are called
mutators
- It common practice is to name mutator method with "set"
- Followed by the attribute name without the leading __
- So mutators are sometimes called
setters
- Here are the mutators for the Computer class
def set_ram(self, ram):
self.__ram = ram
def set_hostname(self, hostname):
self.__hostname = hostname
def set_disk_size(self, disk_size):
self.__disk_size = disk_siz
Data Validation in The Constructor
- The new Time constructor works ...
- but it is not very smart
- I can give it any values I want for the time
- Even ones that are ridiculous
>>> t1 = Time("500:3000:120000")
- When I do this, some functions don't work properly
>>> t1.format_time()
'571:20:0 PM'
- The constructor needs to check the input it is given
- It should do this before it sets the attribute values
- Checking that values make sense before you use them ...
- is called
data validation
- We have to be careful when changing the constructor
- So we will proceed in a step by step fashion
- First we need to be sure that time_string is
indeed a string
- We can do this with the built-in function
isinstance
isinstance
takes two arguments
- An object
- The name of a class
- It returns
True
if the object is an instance of the
class
>>> isinstance("14:35:12", str)
True
- What should we do if time_string is not a string?
- We could print an error message and quit
- But that is not the best way to go
- Instead we should raise an
exception
- Exceptions allow a program to respond gracefully ...
- when unexpected things happen
- There are specific Python exceptions for different kinds of problems
- Here we need to raise a TypeError like this
if not isinstance(time_string, str):
raise TypeError("Time constructor expects a string argument")
- Now if we call the constructor and time_string
is not a string
- We get the following
>>> t1 = Time(55)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "/Users/glenn/workspace-mars/it117/resources_it117/code_it117/example_code_it117/10_chapter_example_code/time_3.py", line 13, in __init__
raise TypeError('Time constructor expects a string argument')
TypeError: Time constructor expects a string argument
- Next, we need to be sure that time_string has the
right format
- It must have two digits followed by a colon, : ...
- followed by two more digits and another colon ...
- then two more digits
- I don't want the constructor to get too large
- Because then it would be hard to read
- So I am going to place the code to check the format ...
- in a separate method called has_valid_format
- Here is the code
# returns true if the string has the form HH:MM:SS
def has_valid_format(s):
pattern = re.compile("\d\d:\d\d:\d\d")
match = pattern.match(s)
return match
- Here we use regular expressions to try to match the string ...
- and return a pointer to the match object
- If the format is good, this variable will point to a real object ...
- and the calling statement will interpret this as
True
- If the string does not have right format ...
- the value of match will be
None
None
is interpreted as False
in an if
statement
- We also need to make sure that the hours value is between 0 and 23 ...
- and the minutes and seconds are between 0 and 59
- To do this I am going to create a new method
time_numbers_in_range
# returns true if the numbers for hours, minutes and seconds
# are within the proper range
def time_numbers_in_range(hours, minutes, seconds ):
if not 0 <= hours <= 23:
return False
if not 0 <= minutes <= 59:
return False
if not 0 <= seconds <= 59:
return False
return True
- Of course, we have to change the constructor to use these new methods
# expects a string of the form HH:MM:SS
# where hours are expressed in numbers from 0 to 23
def __init__(self, time_string):
if not isinstance(time_string, str):
raise TypeError("Time constructor expects a string argument")
if not self.has_valid_format(time_string):
raise ValueError("String must have the format HH:MM:SS")
hours, minutes, seconds = time_string.split(":")
hours = int(hours)
minutes = int(minutes)
seconds = int(seconds )
if not self.time_numbers_in_range(hours, minutes, seconds ):
raise ValueError("One of the time values is not in the proper range")
self.__seconds = hours * 60 * 60 + minutes * 60 + seconds
Hiding Methods
- An attribute is invisible outside the class ...
- if it's name begins with two underscores, __
- This also works for methods
- In the example above we created two methods
- has_valid_format
- time_numbers_in_range
- These methods are only used by the constructor
- They are not intended for use outside the class
- So we should hide them by adding __ ...
- to the beginning of their names
- Most methods in a class will be used by the client code
- But any method not needed by the client code ...
- should be hidden
- A class should be a black box
- It should have specified inputs and outputs
- Users of the class do not need to know how it works
- Only what goes in ...
- and what comes out
The ACM Code of Ethics and Professional Conduct
Class Exercise
Class Quiz