**Homework 8: Object Classes and Inheritance** **Reed CSCI 121 Fall 2022** *complete the exercises by 9am on 11/1* This lab has you work with Python's object-oriented facilities. We look at defining Python object classes, a means for inventing new data types that your programs can use. In doing so, we describe the components that make up the object and also the operations that can be performed on that object. The components are an object's *instance variables* and the operations' code defines an object's *methods*. Typical methods access (or "get") the information stored in an object's components. Other methods might also modify them. Below we talk through the design of two object classes described in lecture. These are a `class Rational` and a `class GiftCard`. After describing these two classes, we then talk about inheritance. This [**zip file**](homework8code.zip) has the Python source code for the examples in these descriptions. (##) A `Rational` class We first invented a data structure for representing rational numbers. A rational number was represented by its numerator and its denominator. You can multiply them and sum them. We also described "getters" for accessing its fraction's two components and a method converting it to a printable string. If you check out the file `Rational.py` you will see the implementation of a Python class for a new type `Rational`. In the interaction below, we can see import and use of this class: ~~~ none >>> from Rational import Rational >>> a = Rational(1, -3) >>> a.stringOf() '-1/3' >>> b = Rational(3, 6) >>> b.stringOf() '1/2' >>> c = a.sumWith(b) >>> c.output() 1/6 ~~~ The first line loads the definition of the `Rational` class into Python. The second and fourth lines each explicitly construct a `Rational` object. The behavior of the constructor is given by the lines at the start of `Rational.py`, namely ~~~ python class Rational: def __init__(self,n,d): # Normalize it so the sign is up top. if d < 0: n *= -1 d *= -1 # Reduce the fraction. g = GCD(n,d) self.num = n // g self.den = d // g ~~~ Here we see the main goal of a class constructor's initialization method `__init__`. It sets the two attributes of a rational number object, named `num` and `den`. This is performed in the last two lines of the definition: ~~~ python self.num = n // g self.den = d // g ~~~ Within that code, `self.num` is how we add a new attribute to a newly created object. `self` is assumed to be that object. The code takes two integer values, and divides out their common divisor. It makes sure that, if the fraction is negative, that the numerator holds the negative value. Since the constructor takes two parameters, the definition of the `__init__` method takes three parameters. This is because every method has to take the receiving object, denoted `self`, as its first argument, like so: ~~~ python def __init__(self, n, d): ~~~ The lines below take the two parameters `n` and `d`, work with them to set up the components of `self`. The `__init__` method is typically not called directly. Rather, it is the code that's run when we construct a new `Rational` with an expression like: ~~~ python Rational(36,24) ~~~ Other methods are defined similarly. The method that requests that a `Rational` number be added to another, and that that third number be returned, is given below: ~~~ python def sumWith(self, other): ns = self.numerator() ds = self.denominator() no = other.numerator() do = other.denominator() return Rational(ns*do + no*ds, ds*do) ~~~ The first four lines get the four integer components of the two objects, the receiving object `self` and the parameter `other`. This code gets used when we write something like ~~~ python c = a.sumWith(b) ~~~ In this case. `a` will be what `self` refers to when the method's code runs, and `b` will be `other`. The last line of the method returns a new rational number by calling the constructor. `c` will get set to refer to that new object. Note that `Rational` number objects aren't designed to be *mutable*. (##) A `GiftCard` class Another example we showed was a class that represented a card with a balance. You can see its definition in the file `GiftCard.py`. These lines below work with an `GiftCard` object after importing the `GiftCard` code: ~~~ python >>> from GiftCard import GiftCard >>> gc = GiftCard(100) >>> gc.spend(20) 80 >>> gc.addFunds(10) 90 ~~~ The `GiftCard` is a simple example of a mutable object. It stores an amount `balance` that can be increased by `addFunds` and decreased by `spend`. Here is further interaction ~~~ python >>> gc.spend(55) 35 >>> gc.spend(50) 'Insufficient funds' >>> gc.getBalance() 35 ~~~ The spend method is written like so: ~~~ python def spend(self, amount): if amount > self.balance: return "Insufficient funds" self.balance = self.balance - amount return self.balance ~~~ Here, again, we see two parameters for a one-parameter method. There is the receiver `self` and then also the `amount` that should be withdrawn from the card's balance. The line ~~~ python self.balance = self.balance - amount ~~~ modifies the card's balance, and this only occurs if `amount <= self.balance`. If instead `amount > self.balance` then the method returns an error string. Take a look at the rest of its method definitions, including its `__init__` method (##) Inheritance In object-oriented languages, we can construct hierarchies of object classes. In lecture we did this with bank account classes `Account`, `Checking`, and `Savings`, the latter two being *subclasses* of the former. A subclass inherits the methods of their designated superclass, but then might override some of its behavior and perhaps have additional methods. This can be seen as a *specialization* of their superclass. Designation of which superclass gets extended by a newly defined subclass is simple code syntax. Suppose we have a class `A` and want to invent a class `B` that inherits from `A`. We then write ~~~ class B(A): ... ~~~ putting the superclass `A` in parentheses. This tells Python that all the class attributes and instance methods of `A` should also be defined for `B`. And then, under `...` we usually introduce other class attributes and some other of its instances' methods. We might also override methods that we're inherited. To see a short, but rich-enough example, review the `Account.py` code. In that code, there are moments when we are overriding a method, where we choose to modify the behavior of the subclass relative to its superclass, and we explicitly need to call the method code of the superclass. We do this for `Saving`'s `withdraw` method by calling ~~~ python `Account.withdraw(self,...)` ~~~ This is a slightly different syntax than normal method invocation. The `self` parameter is included in the parentheses, and this is because we are using the `Account...` notation to indicate that we want to run the method defined for class `Account`. We do this also for `Checking.payInterest` and also `PromotionalChecking.__init__`. (#) Problems (##) `[HW8 P1]` Dial A `Dial` is an object that can be controlled to be set to a certain level, and that level can be displayed. It has an upper limit on its level that can be configured. Its lower limit is 0. Below we see the construction and use of a `Dial` object whose limit is `10`. ~~~ python >>> d = Dial(10,'O','.') >>> d.display() [..........] >>> d.increaseBy(7) >>> d.display() [OOOOOOO...] >>> d.decreaseBy(2) >>> d.display() [OOOOO.....] ~~~ When first constructed, its initial level is `0`. But then that level can be increased with `increaseBy`. It can also be decreased using `decreaseBy`. To configure its display, you build a dial object by feeding its constructor two characters. The first is used to display a unit of the dial's current level, and the other is for the remaining characters. Here is the behavior of a different dial ~~~ python >>> d = Dial(7,'#','-') >>> d.increaseBy(3) >>> d.display() [###----] >>> d.increaseBy(10) >>> d.display() [#######] >>> d.decreaseBy(1) >>> d.display() [######-] >>> d.decreaseBy(17) >>> d.display() [-------] >>> d.increaseBy(1) >>> d.display() [#------] ~~~ The method `increaseBy` takes an integer (assumed to be positive) and increased by that amount. If the increase would take the dial over its limit, the level gets set to the limit. The same goes for `decreaseBy`. It takes a positive integer and lowers the level by that amount, but only if that decrease wouldn't set the level negative. If it would, the level gets set to 0. In a file named `Dial.py`, write the code for a class definition that defines `class Dial` and its methods. In addition to `__init__` you should define `increaseBy`, `decreaseBy`, and `display`. The method `display` prints the characters of the display showing the current level. It should work as our examples illustrate. The method does not return the display string. Instead, it prints it out to the console. To test your code, you'll want to run Python with no arguments like so: ~~~ python % python3 >>> from Dial import Dial ~~~ The first thing you'll want to enter is the `import` line. This will load in the definition of `Dial`. (##) `[HW8 P2]` Car The next class we build is called `Car`. Make a new file called `Car.py` and write its class definition. It represents automobiles driving around some area. We will assume for this assignment that our world is a flat plane with no obstacles, meaning all trips take the straight-line path between the start and end points. There is a car factory where cars are built and introduced into the world. We identify where cars are located by a pair of coordinates--- a *y*-coordinate describing how many miles to the north or south a car sits from their origin and an *x*-coordinate describing how many miles to the east or west that a cars sits from their origin. So a car sitting at `[1.0,0.0]` is one mile east of the factory. A car sitting at `[0.0,-1.0]` is one mile south of the factory. A car sitting at `[-1.414, 1.414]` is about two miles northwest of the factory. The `Car` constructor should set up the basic starting values of things we might care about for a given instance of the class. For this assignment, that should be the miles per gallon that the car gets and the size of the car’s gas tank. Please have your `Car` constructor function `__init__` take these two additional values as parameters in that order: miles per gallon, then fuel tank capacity. The constructor should initialize instance variables to carry these values, along with the *x* and *y* position where the car sits (initially at the origin), along with how much fuel is in the tank (initially, make it a full tank). Now, add a `Car` method `driveTo`. It should take two additional parameters, namely, the x and y coordinates of a location for the car to attempt to move. If the car has enough gas to make the trip, the car should be moved, the amount of gas remaining should be updated, and the method should return `True`. If the car does not have enough gas, it should not be moved or changed at all, and the method should return `False`. To perform this check of the gas, you'll need to figure out the distance that a car would travel. This distance is $$ \sqrt{(x_2 - x_1)^2 + (y_2 - y_1)^2} $$ were the car to move from a location $[x_1,y_1]$ to a location $[x_2,y_2]$. So that we can test your class, we need you to add some additional methods that allow us to find out information about the car. Add two methods named `getLocationX` and `getLocationY` These in tandem return the current location of the car, by returning its *x* coordinate and its *y* coordinate. Also add a `getGas` method which returns the number of gallons of gas the car has left. Finally, add a `getToFill` method which returns how much gas the car would need to fill its tank back to capacity. Though these methods are used for our testing, it is normal to add these "getter" methods that access the contents of an object's instance variables, rather than having them accessed directly. This provides a reasonable "layer of abstraction", allowing you to change the internals of this class without having to change all the code that relies on this class. You can test this code by creating a car and driving it around. Again, you will want to import the definition using the Python `import` statement like so: ~~~ python >>> from Car import Car ~~~ (##) `[HW8 P3]` VerboseDial Let's work from your `Dial.py` code. Make a copy of your `Dial.py` as a file named `VerboseDial.py`. In that file add a new class definition below `Dial` named `VerboseDial` that is a subclass of `Dial`. This object is just like `Dial` except that it always outputs the display of the dial after its level has been increased or decreased. Here is an example interaction: ~~~ python >>> vbd = VerboseDial(10,'O','.') >>> vbd.display() [..........] >>> vbd.increaseBy(7) [OOOOOOO...] >>> vbd.decreaseBy(2) [OOOOO.....] >>> vbd.decreaseBy(5) [..........] >>> vbd.decreaseBy(2) [..........] ~~~ Note that each call to `increaseBy` and `decreaseBy` leads to a `print` of the dial's display. This happens even if the dial's level doesn't actually change. In the last decrease, the level stayed at 0, but the verbose version of the dial displays after each call of `increaseBy` and `decreaseBy`, regardless. (##) `[HW8 P4]` OnOffDial Now we invent a subclass of `VerboseDial` named `OnOffDial`. Make a copy of `Verbose.py` as a file `OnOffDial.py`. You will add a third class definition to this file. An on/off dial's verbose behavior can be turned on or off. When first built it behaves the same as a `VerboseDial` like below: ~~~ python >>> ood = OnOffDial(10,'O','.') >>> ood.display() [..........] >>> ood.increaseBy(7) [OOOOOOO...] >>> ood.decreaseBy(2) [OOOOO.....] ~~~ It has two additional methods `turnOff` and `turnOn`. These methods take no parameters other than `self` and so are passed no arguments. They behave like so, continuing from the interaction above: ~~~ python >>> ood.turnOff() >>> ood.display() >>> ood.increaseBy(4) >>> ood.decreaseBy(1) >>> ood.turnOn() >>> ood.display() [OOOOOOOO..] >>> ood.decreaseBy(3) [OOOOO.....] ~~~ When an `OnOffDial` is turned off, it never displays, not even when the `display` method is called. But then it can be turned back on. When on, it behaves like a `VerboseDisplay` unless and until it is turned off again. (##) `[HW8 P5]` Taxi Let's build another kind of car for simulating a taxi. We create a `Taxi` class that inherits from `Car`. Taxis have the additional behavior that they can pick up passengers and drop them off. Also, a taxi's driver earns money as a result of a trip taken with a passenger. When a taxi is first created, its driver has no money. To do this coding, make a copy of `Car.py` called `Taxi.py`. Here you'll add your definition of the `Taxi` class. Add `pickup` and `dropoff` methods to taxis. The `pickup` method changes the status of the taxi so that it is holding a passenger (it does not change any other aspect of the taxi). At that point, the taxi should track the miles it drives while carrying a passenger. The `dropoff` method drops off a passenger. When that happens, the taxi gets paid at $2 for the pickup plus $3 per mile driven with the passenger. The method should also change the status of the taxi to indicate that it is no longer holding a passenger. In addition, the `pickup` and `dropoff` methods should return `True` or `False` to signify if the method executed successfully. In particular, `False` should be returned if `pickup` is run on a taxi that already has a passenger. `False` should be returned by `dropoff` if is run on a taxi that has no passenger. Also add a getter method `getMoney` that reports the amount of money that the taxi has earned by transporting its passenger.