External students: Each week, you will do your work in a new branch,
then do a pull request, mentioning a new/different student to get (and give)
a code review. So, this week, before starting the prac, create a new branch, prac_06_feedback
.
See how this works? Please ask for help if you're not sure.
We have seen how to work with lists, tuples and dictionaries to store and process data appropriate for those types:
-
list is useful for storing an ordered sequence of data (e.g. monthly rainfall data)
-
tuple is useful for storing fixed (not changing) data with multiple parts (e.g. date of birth)
-
dict is useful when the data has a 'mapping' relationship (e.g. name -> date of birth)
Very often we want to combine data into one object in a way that does not suit one of the built-in types, so we write our own classes for these situations. That's what this practical is all about. As you do the prac, pay attention to how the class construct helps us combine data and functions in one entity. Ask questions as needed!
Get (remember to click Raw) car.py and used_cars.py and add them to your PyCharm project in this week's prac folder.
# used_cars.py
from prac_06.car import Car
def main():
"""Demo test code to show how to use car class."""
my_car = Car(180)
my_car.drive(30)
print("fuel =", my_car.fuel)
print("odo =", my_car.odometer)
print(my_car)
print("Car {}, {}".format(my_car.fuel, my_car.odometer))
print("Car {self.fuel}, {self.odometer}".format(self=my_car))
Note that the import statement assumes you have your car.py file in a folder called prac_06 as we suggested.
Run your program and it should work.
Spend some time studying the Car class.
In the used_cars program file, write one new line of code for each of the following:
-
Create a new Car object called "limo" that is initialised with 100 units of fuel.
-
Add 20 more units of fuel to this new car object using the add method.
-
Print the amount of fuel in the car.
-
Attempt to drive the car 115km using the drive method.
-
Print the car's odometer reading.
-
Now add the
__str__
method to the Car class in car.py.
Using {} string formatting, have it return a string in the following format:
Car, fuel=42, odometer=277
Remember that you can run this method by printing your car object, or passing the car object to the str() function.
Do NOT call the method explicitly likemy_car.__str__()
-
Now add a
name
field to the Car class (in car.py), and adjust the__init__
and__str__
methods to set and display this respectively. Make the str method return the car's name instead of just "Car".
Now add names (literals) to the constructors where you create Car objects in the used_cars.py program.
Test your work and make sure you can now make and view named cars.
Let's make our own simple class for a programming language in the file
programming_language.py (note that the file/module name is the same as the
class but in lower case and with optional _
)
Call your class ProgrammingLanguage
(using Python's recommended
"PascalCase" or "CapWords" style)
There are lots of things we could store, but we'll consider only the following, based mostly on the information found at this Programming Language Comparison page.
For each language, we want to store the following fields - the row names from this table:
(Field) | Java | C++ | Python | Visual Basic | Ruby |
---|---|---|---|---|---|
Typing | Static | Static | Dynamic | Static | Dynamic |
Reflection | Yes | No | Yes | No | Yes |
Year | 1995 | 1983 | 1991 | 1991 | 1995 |
Define the following methods:
-
__init__
- like most init functions, create the fields and set them to the parameters passed in -
is_dynamic()
- which returns True/False if the programming language is dynamically typed or not
Note: it's really important that you understand this function will take no parameters (other than self). The information is already stored inside the object, so you don't need to tell the object its own data. This function's purpose is to encapsulate the Boolean functionality that would make the class more helpful. See how the function name starts with "is", likeisupper()
,isnumeric()
, etc.? So, it returns a Boolean.
Create a simple program in a file called languages.py.
Import the class, then copy these 3 lines into your new program:
ruby = ProgrammingLanguage("Ruby", "Dynamic", True, 1995)
python = ProgrammingLanguage("Python", "Dynamic", True, 1991)
visual_basic = ProgrammingLanguage("Visual Basic", "Static", False, 1991)
Now add the __str__
method to the class (not the client code),
which should return a string like:
Python, Dynamic Typing, Reflection=True, First appeared in 1991
Print the python object and see if your __str__
function is working
properly.
Now create a new list that contains these three existing ProgrammingLanguage objects.
Do this next part on paper first, then copy it into PyCharm to see how
you went.
Remember that writing code on paper (or a whiteboard) is good
practice, helps you learn it better (since you can't depend on the
IDE's help) and encourages you to be consistent and clear with syntax,
indenting, etc.
Loop through and print the names of all of the languages with dynamic
typing (make sure you use your new is_dynamic
method!),
which should produce output like:
The dynamically typed languages are:
Ruby
Python
Remember the string formatting example from prac 2:
name = "Gibson L-5 CES"
year = 1922
cost = 16035.40
print("My guitar: {0}, first made in {1}".format(name, year))
You should notice that we have multiple values to store for one guitar entity:
name, year and cost... and that guitars are awesome! What if we owned 9 guitars?
We'd want to use a collection like a list... but what would each element in the list be?
... A tuple? A dictionary? No... This is a classic case for a class!
Write a Guitar class that allows you to store one guitar with those fields
(attributes):
- name (we could split this into make and model, but one name field will do us for now)
- year
- cost
Define the following methods:
-
__init__
- with defaultsname="", year=0, cost=0
-
__str__
- which uses {} string formatting to return something like (using the values from above):
Gibson L-5 CES (1922) : $16,035.40
-
get_age()
- which returns how old the guitar is in years (e.g. the L-5 is 2018 - 1922 = 96) -
is_vintage()
- which returns True if the guitar is 50 or more years old, False otherwise
Hint: try usingget_age()
to simplify the implementation of this method!
Remember that methods should not take in any data that the object already knows (like age, year, etc.).
Now write a guitar_test.py program with at least enough code to
test that the last two methods work as expected.
So to test that the get_age() method works, you could test that the
above example guitar does indeed output 95 as expected. Here is some
sample output for testing two guitars where the second is called
Another Guitar and has year=2012:
Gibson L-5 CES get_age() - Expected 96. Got 96
Another Guitar get_age() - Expected 6. Got 6
Gibson L-5 CES is_vintage() - Expected True. Got True
Another Guitar is_vintage() - Expected False. Got False
Do you see how this works?
We print our own literal for what we expect if the function works (e.g. 96),
then we print what the actual method returns and we look at the output
to see if they match.
This form of testing is quite 'manual' since we need to read the output and
compare it ourselves, but it is a good start.
Let's say we wrote the is_vintage()
method incorrectly, then we want
to see something like:
50-year old guitar is_vintage() - Expected True. Got False
We can see that the actual does not match the expected, so we know we need to fix something.
Got your class working (tested) now? Great!
Write a program that uses it in a file called guitars.py
The program should use a list to store all of the user's guitars (keep inputting until they enter a blank name), then print their details.
Read the full question including the notes before starting.
We've written helpful comments to make it easier and to teach you useful
lessons.
Sample Output (bold is user entry):
My guitars! Name: Fender Stratocaster Year: 2014 Cost: $765.4 Fender Stratocaster (2014) : $765.40 added. Name: ... snip ... These are my guitars: Guitar 1: Fender Stratocaster (2014), worth $ 765.40 Guitar 2: Gibson L-5 CES (1922), worth $ 16,035.40 (vintage) Guitar 3: Line 6 JTV-59 (2010), worth $ 1,512.90
Programmer Efficiency Note
When testing a program like this you can waste a lot of time typing in
input... then changing something, running it again and... typing the
same thing again...
So don't do it!
Instead, comment out the user input lines, and put in lines like this to 'get' the data for testing:
guitars.append(Guitar("Gibson L-5 CES", 1922, 16035.40))
guitars.append(Guitar("Line 6 JTV-59", 2010, 1512.9))
According to Wikipedia's page on the abstraction principle:
"When read as recommendation to the programmer, the abstraction principle can be generalised as the "don't repeat yourself" principle, which recommends avoiding the duplication of information in general, and also avoiding the duplication of human effort involved in the software development process."
-
The sample output uses some nice string formatting. Feel free to try and figure this out, or just use our code (the width we use for guitar.name is just a guesstimate):
print("Guitar {}: {:>20} ({}), worth ${:10,.2f}{}".format(i + 1, guitar.name, guitar.year, guitar.cost, vintage_string))
The variable
vintage_string
is set to""
or" (vintage)"
depending on theis_vintage()
method.
If you're keen, try using Python's ternary operator to do this in one line.
E.g. to set the value ofis_adult
to True or False depending on age, you could use:is_adult = True if age >= 18 else False
-
See guitar.year, guitar.cost...? You can do this another way if you want...
E.g. for the car class example above, the following two lines are equivalent. This can be a useful way to make the code more readable because you can see the name of the variable you're printing in the actual placeholder.
print("Car {}, {}".format(my_car.fuel, my_car.odometer)) print("Car {car.fuel}, {car.odometer}".format(car=my_car))
-
For this particular code, we've used both
i
and the target variableguitar
(instead ofguitars[i]
) by using the built-inenumerate()
function. You don't have to do it this way, but if you want to, it's like this:for i, guitar in enumerate(guitars): # do something with i (the index) and guitar (the element)
So
enumerate()
must return what type? A tuple!
enumerate
can also take a second parameter, the starting number for iteration:for i, guitar in enumerate(guitars, 1) # i starts at 1 instead of 0
Use these exercises as much-needed practice and as ways to learn new things.
-
Create a car driving simulator in car_simulator.py that uses the Car class that works like the following sample output...
Note: Please do this (and every problem of significant size) incrementally:
- Start by just testing one method call,
- then another,
- then write the menu and put it all together.
(Do not start with the menu.)
Remember to use the class's functionality - don't rewrite anything you've already got.
Do you remember how to construct a simple menu-driven program? If not, it's very important that you revise earlier lectures and practicals (it was in prac 1).
You will need to import the car module, create aCar
object, and use appropriate methods on that object.Let's drive! Enter your car name: The bomb The bomb, fuel=100, odo=0 Menu: d) drive r) refuel q) quit Enter your choice: f Invalid choice The bomb, fuel=100, odo=0 Menu: d) drive r) refuel q) quit Enter your choice: d How many km do you wish to drive? 39 The car drove 39km. The bomb, fuel=61, odo=39 Menu: d) drive r) refuel q) quit Enter your choice: d How many km do you wish to drive? -25 Distance must be >= 0 How many km do you wish to drive? 100 The car drove 61km and ran out of fuel. The bomb, fuel=0, odo=100 Menu: d) drive r) refuel q) quit Enter your choice: r How many units of fuel do you want to add to the car? -80 Fuel amount must be >= 0 How many units of fuel do you want to add to the car? 120 Added 120 units of fuel. The bomb, fuel=120, odo=100 Menu: d) drive r) refuel q) quit Enter your choice: d How many km do you wish to drive? 25 The car drove 25km. The bomb, fuel=95, odo=125 Menu: d) drive r) refuel q) quit Enter your choice: q Good bye The bomb's driver.
-
Create a Date class, storing the fields:
- day
- month
- year
Write some useful methods, including:
__init__
and__str__
add_days(n)
- which should add n days to the stored date (perhaps harder than it seems)
Test the class.
Note: Python has built in date and time functionality in the datetime module, so you would not usually write your own class to store a date, but this is a good practice exercise.
-
Create a program that uses a list of Person objects.
Each Person object records the first-name, last-name and age.
The user can type in the details of any number of people. The code generates a table formatted with the first-names, last-names, and ages of the people (perhaps sort the people into order based on their ages).
It's a valuable skill! On the lab computers, you should be
able to use "Git Bash". Right-click on the folder where your files are
and select "Git Bash here". On a Mac, just use Terminal.
A great place to learn git commands is safely in your browser at:
try.github.io
Why? Here's a quote from one of our students who completed this subject:
"Now I'm moving into 2nd yr subjects, I found the integrated git/pycharm a good intro step, but I'm finding git bash with a simple cheat sheet is a nice progression. I think 99% of us could use some more training/practice with branching, merging, etc. Still kinda confusing in the terminology."
The more you have to do by yourself, the more you will really understand what's happening. Some things (like removing a file from the index) cannot be done with the IDE integration. So, as soon as you are happy to, start getting used to using git from a console.
So, create a new repo with git init
, add some code, add files with git add
,
commit with git commit
...
Then, next time you're ready to commit and push your practical work, try using the command line to do it!