#
Lab 7: Object-Oriented Programming

*Due at 11:59:59 pm on Tuesday, 10/24/2023.*

## Starter Files

Download lab07.zip. Inside the archive, you will find starter files for the questions in this lab, along with a copy of the OK autograder.

## OOP terminology

**Object-oriented programming** (OOP) is a style of programming that
allows you to think of code in terms of "objects." Here's an example of
a `Car`

class:

class Car(object):

```
num_wheels = 4
gas = 30
headlights = 2
size = 'Tiny'
def __init__(self, make, model):
self.make = make
self.model = model
self.color = 'No color yet. You need to paint me.'
self.wheels = Car.num_wheels
self.gas = Car.gas
def paint(self, color):
self.color = color
return self.make + ' ' + self.model + ' is now ' + color
def drive(self):
if self.wheels < Car.num_wheels or self.gas <= 0:
return 'Cannot drive!'
self.gas -= 10
return self.make + ' ' + self.model + ' goes vroom!'
def pop_tire(self):
if self.wheels > 0:
self.wheels -= 1
def fill_gas(self):
self.gas += 20
return 'Gas level: ' + str(self.gas)
```

Here's some terminology:

**class**: a blueprint for how to build a certain type of object. The`Car`

class (shown above) describes the behavior and data that all`Car`

objects have.**instance**: a particular occurrence of a class. In Python, we create instances of a class like this:`>>> my_car = Car('Tesla', 'Model S')`

`my_car`

is an instance of the`Car`

class.**attribute**or**field**: a variable that belongs to the class. Think of an attribute as a quality of the object: cars have*wheels*and*size*, so we have given our`Car`

class`self.wheels`

and`self.size`

attributes. We can access attributes using**dot notation**:`>>> my_car.size 'Tiny' >>> my_car.wheels 4`

**method**: Methods are just like normal functions, except that they are tied to an instance or a class. Think of a method as a "verb" of the class: cars can*drive*and also*pop their tires*, so we have given our`Car`

class the methods`drive`

and`pop_tire`

. We call methods using**dot notation**:`>>> my_car = Car('Tesla', 'Model S') >>> my_car.drive() 'Tesla Model S goes vroom!'`

**constructor**: As with data abstraction, constructors describe how to build an instance of the class. Most classes have a constructor. In Python, the constructor of the class defined as`__init__`

. For example, here is the`Car`

class's constructor:`def __init__(self, make, model): self.make = make self.model = model self.color = 'No color yet. You need to paint me.' self.wheels = Car.num_wheels self.gas = Car.gas`

The constructor takes in two arguments,

`make`

and`model`

. As you can see, the constructor also creates the`self.color`

,`self.wheels`

and`self.gas`

attributes.`self`

: in Python,`self`

is the first parameter for many methods (in this class, we will only use methods whose first parameter is`self`

). When a method is called,`self`

is bound to an instance of the class. For example:`>>> my_car = Car('Tesla', 'Model S') >>> my_car.drive()`

Notice that the

`drive`

method takes in`self`

as an argument, but it looks like we didn't pass one in! This is because the dot notation*implicitly*passes in`car`

as`self`

for us.

## Car WWPD

### Question 1: Car

Use OK to test your knowledge with the following What would Python print questions:

`python3 ok -q car -u`

If you get stuck try typing these in the interpreter yourself

`python3 -i`

## Make Change

### Question 2

Implement `make_change`

, which takes a positive integer `amount`

and a
dictionary of `coins`

. The `coins`

dictionary keys are positive integer
denominations and its values are positive integer coin counts. For example,
`{1: 4, 5: 2}`

represents four pennies and two nickels. The `make_change`

function returns a list of coins that sum to `amount`

, where the count of
any denomination `k`

in the return value is at most `coins[k]`

.

If there are multiple ways to make change for `amount`

, prefer to use as many
of the smallest coins available and place the smallest coins first in the
returned list.

Before you begin, you should review the `count_change`

function from the lecture slides. `make_change`

is derivative of this problem, but the resurive path should follow a similar structure.

**Hint**: Try using the smallest coin to make change. If it turns out that there
is no way to make change using the smallest coin, then try making change
without the smallest coin.

**Hint**: The simplest solution does not involve defining any local functions,
but you can define additional functions if you wish.

**Hint:** The *base case* returns `None`

. You may need to take special care to write something like `result = function_call(...)`

and then check `if result:`

to avoid adding a list to `None`

.

```
def make_change(amount, coins):
"""Return a list of coins that sum to amount, preferring the smallest coins
available and placing the smallest coins first in the returned list.
The coins argument is a dictionary with keys that are positive integer
denominations and values that are positive integer coin counts.
>>> make_change(2, {2: 1})
[2]
>>> make_change(2, {1: 2, 2: 1})
[1, 1]
>>> make_change(4, {1: 2, 2: 1})
[1, 1, 2]
>>> make_change(4, {2: 1}) == None
True
>>> coins = {2: 2, 3: 2, 4: 3, 5: 1}
>>> make_change(4, coins)
[2, 2]
>>> make_change(8, coins)
[2, 2, 4]
>>> make_change(25, coins)
[2, 3, 3, 4, 4, 4, 5]
>>> coins[8] = 1
>>> make_change(25, coins)
[2, 2, 4, 4, 5, 8]
"""
if not coins:
return None
smallest = min(coins)
rest = remove_one(coins, smallest)
"*** YOUR CODE HERE ***"
if amount == smallest:
return [smallest]
result = make_change(amount-smallest, rest)
if result:
return [smallest] + result
else:
return make_change(amount, rest)
```

You can use the `remove_one`

function in your implementation:

```
def remove_one(coins, coin):
"""Remove one coin from a dictionary of coins. Return a new dictionary,
leaving the original dictionary coins unchanged.
>>> coins = {2: 5, 3: 2, 6: 1}
>>> remove_one(coins, 2) == {2: 4, 3: 2, 6: 1}
True
>>> remove_one(coins, 6) == {2: 5, 3: 2}
True
>>> coins == {2: 5, 3: 2, 6: 1} # Unchanged
True
"""
copy = dict(coins)
count = copy.pop(coin) - 1
if count:
copy[coin] = count
return copy
```

Use OK to test your code:

`python3 ok -q make_change`

### Question 3

Complete the `change`

method of the `ChangeMachine`

class. A `ChangeMachine`

instance holds some `coins`

, which are initially all pennies. The `change`

method takes a positive integer `coin`

, adds that coin to its `coins`

, and then
returns a list that sums to `coin`

. The machine prefers to return as many of
the smallest coins available, ordered from smallest to largest. The coins
returned by `change`

are removed from the machine's `coins`

.

*Hint*: Call the `make_change`

function in order to compute the result of
`change`

, but update `self.coins`

before returning that result.

*Hint*: To remove key-value pairs from a dictionary, you can use use `.pop(<key>)`

. For example, `d.pop("first_key")`

will remove the key-value pair associated with `"first_key"`

from `d`

.

```
class ChangeMachine:
"""A change machine holds a certain number of coins, initially all pennies.
The change method adds a single coin of some denomination X and returns a
list of coins that sums to X. The machine prefers to return the smallest
coins available. The total value in the machine never changes, and it can
always make change for any coin (perhaps by returning the coin passed in).
The coins attribute is a dictionary with keys that are positive integer
denominations and values that are positive integer coin counts.
>>> m = ChangeMachine(2)
>>> m.coins == {1: 2}
True
>>> m.change(2)
[1, 1]
>>> m.coins == {2: 1}
True
>>> m.change(2)
[2]
>>> m.coins == {2: 1}
True
>>> m.change(3)
[3]
>>> m.coins == {2: 1}
True
>>> m = ChangeMachine(10) # 10 pennies
>>> m.coins == {1: 10}
True
>>> m.change(5) # takes a nickel & returns 5 pennies
[1, 1, 1, 1, 1]
>>> m.coins == {1: 5, 5: 1} # 5 pennies & a nickel remain
True
>>> m.change(3)
[1, 1, 1]
>>> m.coins == {1: 2, 3: 1, 5: 1}
True
>>> m.change(2)
[1, 1]
>>> m.change(2) # not enough 1's remaining; return a 2
[2]
>>> m.coins == {2: 1, 3: 1, 5: 1}
True
>>> m.change(8) # cannot use the 2 to make 8, so use 3 & 5
[3, 5]
>>> m.coins == {2: 1, 8: 1}
True
>>> m.change(1) # return the penny passed in (it's the smallest)
[1]
>>> m.change(9) # return the 9 passed in (no change possible)
[9]
>>> m.coins == {2: 1, 8: 1}
True
>>> m.change(10)
[2, 8]
>>> m.coins == {10: 1}
True
>>> m = ChangeMachine(9)
>>> [m.change(k) for k in [2, 2, 3]]
[[1, 1], [1, 1], [1, 1, 1]]
>>> m.coins == {1: 2, 2: 2, 3: 1}
True
>>> m.change(5) # Prefers [1, 1, 3] to [1, 2, 2] (more pennies)
[1, 1, 3]
>>> m.change(7)
[2, 5]
>>> m.coins == {2: 1, 7: 1}
True
"""
def __init__(self, pennies):
self.coins = {1: pennies}
def change(self, coin):
"""Return change for coin, removing the result from self.coins."""
"*** YOUR CODE HERE ***"
self.coins[coin] = 1 + self.coins.get(coin, 0)
result = make_change(coin, self.coins)
for coin in result:
count = self.coins.pop(coin) - 1
if count:
self.coins[coin] = count
return result
```

Use OK to test your code:

`python3 ok -q ChangeMachine`

## Submission

When you are done, submit your file to Gradescope. You only need to upload the following files:

`lab07.py`