class/instance variables
In your code, you use class variables instead of instance variables.
You have to know that class variables are shared in all instance, for example:
class CoffeeMachine:
water = 400
my_machine = CoffeeMachine()
your_machine = CoffeeMachine()
CoffeeMachine.water = 0
print(my_machine.water)
print(your_machine.water)
You get 0 in both machines!
The right way is to used instance variable. Instance variables determine the state of your object:
class CoffeeMachine:
def __init__(self):
self.water = 400
my_machine = CoffeeMachine()
your_machine = CoffeeMachine()
my_machine.water = 0
print(my_machine.water)
print(your_machine.water)
So, in your code, you can replace CoffeeMachine.sothing
by self.sothing
.
See the chapter Class and Instance Variables in the Python documentation.
Your constructor become:
class CoffeeMachine:
def __init__(self):
self.water = 400
self.milk = 540
self.coffee_beans = 120
self.cups = 9
self.money = 550
self.running = False
infinite recursion
I have detected a potential infinite recursion:
- The
__init__
function calls `start``,
- The
start
function calls on of the actions,
- Each action calls
return_to_menu
,
- And the
return_to_menu
function calls start
again…
To avoid that, you can use an infinite loop, which will be controlled by the running attribute.
Here is the scenario:
The machine is initialised: running is True
,
While running is True
:
- The user enter the action he wants to do
- The machine execute the action
You can easily translate into a main
function:
def main():
machine = CoffeeMachine()
while machine.running:
action = ask_action()
machine.execute_action(action)
if __name__ == '__main__':
main()
Of course, we need to change the implementation slightly:
the initialisation must set running to True
,
def __init__(self):
...
self.running = False
the old start
method is divided into 2 functions with a single role: prompting the user and running an action.
The return_to_menu
is removed.
Prompting the user
When you ask something to the user you generally need to check the input to make sure it matches what we need. If not, we loop forever.
For the ask_action
function, we have a set of acceptable answers: "buy", "fill", "take", "exit", "remaining". So, we can loop forever until the user enter an acceptable answer.
In Python, we can use an enumeration for that:
import enum
class Action(enum.Enum):
BUY = "buy"
FILL = "fill"
TAKE = "take"
EXIT = "exit"
REMAINING = "remaining"
Here is a small demo of the possibilities:
>>> possible_values = [action.value for action in Action]
>>> possible_values
['buy', 'fill', 'take', 'exit', 'remaining']
>>> action = Action("fill")
>>> action
<Action.FILL: 'fill'>
>>> action = Action("quit")
Traceback (most recent call last):
...
ValueError: 'quit' is not a valid Action
Here is how you can define the ask_action
function:
import enum
class Action(enum.Enum):
BUY = "buy"
FILL = "fill"
TAKE = "take"
EXIT = "exit"
REMAINING = "remaining"
def ask_action():
possible_values = ", ".join([action.value for action in Action])
while True:
answer = input(f"Write action ({possible_values}):\n")
try:
return Action(answer)
except ValueError:
print(f"This answer is not valid: {answer}")
Note: ask_action
is a function here, there no need to turn it into a method since it doesn't access the class variables or methods.
executing an action
It easy to change the old start
method into a execute_action
method. This method has the parameter action:
def execute_action(self, action):
if action == Action.BUY:
self.buy()
elif action == Action.FILL:
self.fill()
elif action == Action.TAKE:
self.take()
elif action == Action.EXIT:
self.running = False
elif action == Action.REMAINING:
self.show_remaining()
else:
raise NotImplementedError(action)
The implementation is slightly changed:
- The exit action is changed to set running to
False
.
NotImplementedError
is raised if the action is unknown: this prevent unwanted behavior if your Action
enumaration changes in the future but you forget to update execute_action
.
status
(which is renamed show_remaining
) is fixed: no need to take the class in parameter.
As you can see, it is very simple.
Show remaining
The status
function was renamed show_remaining
to use a verb and match the term used in Action
.
But you can also change the Action into "status" if you prefer.
The status don't need to have any parameter because you only want to display the instance variable values.
So, you can write:
def show_remaining(self):
"""
Display the quantities of supplies in the machine at the moment
"""
print(f"The coffee machine has:")
print(f"{self.water} of water")
print(f"{self.milk} of milk")
print(f"{self.coffee_beans} of coffee beans")
print(f"{self.cups} of disposable cups")
print(f"${self.money} of money")
Instead of using a comment you can use a docstring. This is that way we document function and classes in Python.
You can read The Hitchhiker Guide to Python about docstring and API documentation in general. Very good book.
Ask for a drink
The "buy" action is similar to the "ask_action/execute_action".
If you use the same logic, you'll see that you can drop or reimplement the deduct_supplies
function too.
The difference is that you want the user to enter a number instead of a text.
You have: 1 - "espresso", 2 - "latte", 3 - "cappuccino", for "back to main menu", you can choose 9.
All that can be stored in a class Python dict
to do the mapping between numbers and labels.
Note that ask_drink
is a good name for this function:
def ask_drink():
choices = {1: "espresso", 2: "latte", 3: "cappuccino", 9: "back to main menu"}
possible_values = ", ".join(f"{value} - {name}" for value, name in sorted(choices.items()))
while True:
answer = input(f"What do you want to buy? ({possible_values}):\n")
try:
value = int(answer)
if value in choices:
return value
print(f"This answer is not valid: {answer}")
except ValueError:
print(f"This is not a number: {answer}")
Remarks:
sorted
is required because dict
keys are unordered (well, actually, recent versions of Python keep keys order),
- Using
value in choices
is a good way to check if a key is in a dictionary.
Consumption (deduced supplies)
In your coffee machine, deduced supplies is represented as a list of 5 elements.
For instance, we have [250, 0, 16, 1, 4]
for water, milk, coffee beans, cups and money.
If you have a list, you need to access the items by index. But I would be easier to access the items by name. To do that, you can use a collections.namedtuple
. A namedtuple
is a factory function which creates a class (a subclass of tuple
).
First, you can define a new tuple class, we call it Consumption
:
import collections
Consumption = collections.namedtuple("Consumption", "water, milk, coffee_beans, cups, money")
You can instanciate the Consumption
like a classic tuple
or with key/value pairs:
espresso_cons = Consumption(250, 0, 16, 1, 4)
latte_cons = Consumption(water=350, milk=75, coffee_beans=20, cups=1, money=7)
cappuccino_cons = Consumption(water=200, milk=100, coffee_beans=12, cups=1, money=6)
note: the second form is really more readable.
Checking availability
When you need to "check" something, you can think about exceptions. The idea behind this is: I do some tests and if something is wrong I raise an exception. The exception type and/or the exception message can detail the problem. Then I can use an exception handler to display the message.
To define an exception, a good practice is to inherit the Exception
class like this:
class NotEnoughSupplyError(Exception):
def __init__(self, supply):
msg = f"Sorry, not enough {supply}"
super(NotEnoughSupplyError, self).__init__(msg)
This exception take a supply parameter which is the name of the missing supply.
You can then implement the available_check
method as below:
def available_check(self, consumption):
"""
Checks if it can afford making that type of coffee at the moment
:param consumption: the Consumption
:raise NotEnoughSupplyError: if at least one supply is missing.
"""
if self.water - consumption.water < 0:
raise NotEnoughSupplyError("water")
elif self.milk - consumption.milk < 0:
raise NotEnoughSupplyError("milk")
elif self.coffee_beans - consumption.coffee_beans < 0:
raise NotEnoughSupplyError("coffee beans")
elif self.cups - consumption.cups < 0:
raise NotEnoughSupplyError("cups")
Really simple, isn't it?
The buy
method
You know have all the elements in hand to implement the buy
method:
def buy(self):
drink = ask_drink()
if drink == 9:
return
espresso_cons = Consumption(250, 0, 16, 1, 4)
latte_cons = Consumption(water=350, milk=75, coffee_beans=20, cups=1, money=7)
cappuccino_cons = Consumption(water=200, milk=100, coffee_beans=12, cups=1, money=6)
consumption = {1: espresso_cons, 2: latte_cons, 3: cappuccino_cons}[drink]
try:
self.available_check(consumption)
except NotEnoughSupplyError as exc:
print(exc)
else:
print("I have enough resources, making you a coffee!")
self.water -= consumption.water
self.milk -= consumption.milk
self.coffee_beans -= consumption.coffee_beans
self.cups -= consumption.cups
self.money += consumption.money
To get the consumption
we introduce a small mapping between each drink value and each Consumption
instances.
Of course, instead of an exception handler you can use a classic if
. But I wanted to show you something powerful.
The fill
method
Again, to implement the fill
method, you can introduce a function ask_quantity
which ask for a quantity of a given supply. This function take a message in parameter:
def ask_quantity(msg):
while True:
answer = input(msg + "\n")
try:
value = int(answer)
if value >= 0:
return value
print(f"This answer is not valid: {answer}")
except ValueError:
print(f"This is not a number: {answer}")
The fill
method can be implemented as follow:
def fill(self):
"""
Add supplies to the machine
"""
self.water += ask_quantity("Write how many ml of water do you want to add:")
self.milk += ask_quantity("Write how many ml of milk do you want to add:")
self.coffee_beans += ask_quantity("Write how many grams of coffee beans do you want to add:")
self.cups += ask_quantity("Write how many disposable cups of coffee do you want to add:")
The take
method.
Not sure to understand what the take
method do: money is always reset to 0!?
Putting everything together
As you can see, I have done a lot of improvements. You can certainly go further, but write something simple and easy to read.
import collections
import enum
class Action(enum.Enum):
BUY = "buy"
FILL = "fill"
TAKE = "take"
EXIT = "exit"
REMAINING = "remaining"
def ask_action():
possible_values = ", ".join([action.value for action in Action])
while True:
answer = input(f"Write action ({possible_values}):\n")
try:
return Action(answer)
except ValueError:
print(f"This answer is not valid: {answer}")
def ask_drink():
choices = {1: "espresso", 2: "latte", 3: "cappuccino", 9: "back to main menu"}
possible_values = ", ".join(f"{value} - {name}" for value, name in sorted(choices.items()))
while True:
answer = input(f"What do you want to buy? ({possible_values}):\n")
try:
value = int(answer)
if value in choices:
return value
print(f"This answer is not valid: {answer}")
except ValueError:
print(f"This is not a number: {answer}")
def ask_quantity(msg):
while True:
answer = input(msg + "\n")
try:
value = int(answer)
if value >= 0:
return value
print(f"This answer is not valid: {answer}")
except ValueError:
print(f"This is not a number: {answer}")
Consumption = collections.namedtuple("Consumption", "water, milk, coffee_beans, cups, money")
class NotEnoughSupplyError(Exception):
def __init__(self, supply):
msg = f"Sorry, not enough {supply}"
super(NotEnoughSupplyError, self).__init__(msg)
class CoffeeMachine:
def __init__(self):
# quantities of items the coffee machine already had
self.water = 400
self.milk = 540
self.coffee_beans = 120
self.cups = 9
self.money = 550
self.running = True
def execute_action(self, action):
if action == Action.BUY:
self.buy()
elif action == Action.FILL:
self.fill()
elif action == Action.TAKE:
self.take()
elif action == Action.EXIT:
self.running = False
elif action == Action.REMAINING:
self.show_remaining()
else:
raise NotImplementedError(action)
def available_check(self, consumption):
"""
Checks if it can afford making that type of coffee at the moment
:param consumption: the Consumption
:raise NotEnoughSupplyError: if at least one supply is missing.
"""
if self.water - consumption.water < 0:
raise NotEnoughSupplyError("water")
elif self.milk - consumption.milk < 0:
raise NotEnoughSupplyError("milk")
elif self.coffee_beans - consumption.coffee_beans < 0:
raise NotEnoughSupplyError("coffee beans")
elif self.cups - consumption.cups < 0:
raise NotEnoughSupplyError("cups")
def buy(self):
drink = ask_drink()
if drink == 9:
return
espresso_cons = Consumption(250, 0, 16, 1, 4)
latte_cons = Consumption(water=350, milk=75, coffee_beans=20, cups=1, money=7)
cappuccino_cons = Consumption(water=200, milk=100, coffee_beans=12, cups=1, money=6)
consumption = {1: espresso_cons, 2: latte_cons, 3: cappuccino_cons}[drink]
try:
self.available_check(consumption)
except NotEnoughSupplyError as exc:
print(exc)
else:
print("I have enough resources, making you a coffee!")
self.water -= consumption.water
self.milk -= consumption.milk
self.coffee_beans -= consumption.coffee_beans
self.cups -= consumption.cups
self.money += consumption.money
def fill(self):
"""
Add supplies to the machine
"""
self.water += ask_quantity("Write how many ml of water do you want to add:")
self.milk += ask_quantity("Write how many ml of milk do you want to add:")
self.coffee_beans += ask_quantity("Write how many grams of coffee beans do you want to add:")
self.cups += ask_quantity("Write how many disposable cups of coffee do you want to add:")
def take(self):
"""
Take the money from the machine
"""
print(f"I gave you ${self.money}")
self.money = 0
def show_remaining(self):
"""
Display the quantities of supplies in the machine at the moment
"""
print(f"The coffee machine has:")
print(f"{self.water} of water")
print(f"{self.milk} of milk")
print(f"{self.coffee_beans} of coffee beans")
print(f"{self.cups} of disposable cups")
print(f"${self.money} of money")
def main():
machine = CoffeeMachine()
while machine.running:
action = ask_action()
machine.execute_action(action)
if __name__ == '__main__':
main()
IMO, the money should not be a supply like water...