This is my third version of building a simple Python calculator. The first two versions only operate on two inputs to perform calculation, this version allows multiple inputs. However, the result will leave a .0
. Other than that, is there any way I can further improve this program? I've also included docstrings to explain the function of each code block.
import operator
from functools import reduce
import sys
# Display a list of math operators that the user can choose to perform their calculation
MATH_OPERATIONS = """\n\nList of math operations:
1. Addition (+)
2. Subtraction (-)
3. Multiplication (*)
4. Division (/)
5. Exponentiation (**)
"""
# A dictionary that contain options of operations to perform the calculation
OPERATIONS = {
1: operator.add,
2: operator.sub,
3: operator.mul,
4: operator.truediv,
5: operator.pow
}
def ask_user_yes_no(yes_no_question):
"""
Simplifies if/else in determining the correct answers from the user input.
Returns True if the user answer the prompt with any of the values in choice_yes.
Returns False if the user enters any of the values in choice_no
"""
choice_yes = ["yes", 'y']
choice_no = ["no", 'n']
while True:
user_choice = input(yes_no_question).lower()
if user_choice in choice_yes:
return True
elif user_choice in choice_no:
return False
else:
print("\n\nInvalid Input. Try again.")
def count_number_input():
"""
Takes a user input and count the amount of numbers that the user wants to calculate.
Prints out a message if ValueError occurs.
"""
while True:
try:
count_input = int(input("\nHow many number would you like to calculate?: "))
except ValueError:
print("\nINVALID INPUT - Field must not be blank or contained non-integer or non-numerical values.")
else:
return count_input
def get_number_list():
"""
Calls count_number_input function to get how many numbers that the user wants to calculate.
Iterates over the range of the elements in the function and then
asks the user to input the number that they would want to calculate.
Prints out messages if a ValueError occurs.
"""
input_amount = count_number_input()
while True:
try:
numbers_list = [float(input("\nNumbers: ")) for _ in range(input_amount)]
except ValueError:
print("\nInvalid input, try again.")
print("\nPlease ensure that the prompt does not contain a null or non-integer or non-numerical values.")
else:
return numbers_list
def select_choice():
"""
Prints out a list of math operations.
Asks the user to select an option to use in the calculation.
Check user's selection for ValueError and skip if none is found.
Prints out a message if the user has selected an option beyond the specified range of options.
"""
print(MATH_OPERATIONS)
while True:
try:
user_choice = int(input("Select an option | 1 | 2 | 3 | 4 | 5 |: "))
except ValueError:
print("\nINVALID INPUT - Field must not be blank or contained non-integer or non-numerical values.\n")
continue
if user_choice > 5:
print("\nOption selected must be from 1 to 5 only!\n")
else:
return user_choice
def calculate(numbers, choice):
"""
Applies operations across all numbers and return the result.
Calculation:
>>> calculate([2, 2], 1)
4.0
>>> calculate([5, 3], 2)
2.0
>>> calculate([5, 5] 3)
25.0
>>> calculate([9, 3] 4)
3.0
>>> calculate([4, 4] 5)
256.0
"""
return reduce(OPERATIONS[choice], numbers)
def start_program():
"""
Starts the program by asking the user the amount of numbers that they want to calculate,
and then perform a calculation on those numbers.
Prints out the result of calculation.
"""
user_number = get_number_list()
user_choice = select_choice()
print("\nResult: ", calculate(user_number, user_choice))
def should_calculate_again():
"""
Calls start_program function to run the program first.
Asks the user if they want to perform more calculation.
Restarts the program if ask_user_yes_no returns True.
Exits the program telling the user that the program has exited
if ask_user_yes_no returns False.
"""
while True:
start_program()
if not ask_user_yes_no("\n\nWould you like to perform more calculation? (Y/N): "):
sys.exit("\n\n-----Program Exited-----\n")
should_calculate_again()
1 Answer 1
Doc test
It looks like you've intended to include "doctests" in your """docstrings"""
, but it doesn't appear that you've used them.
Running the tests ...
>>> import doctest
>>> doctest.testmod()
... shows all 5 tests fail!
Test failures (wrong output)
Integer input produces integer output, not floating point:
Failed example:
calculate([2, 2], 1)
Expected:
4.0
Got:
4
Failed example:
calculate([5, 3], 2)
Expected:
2.0
Got:
2
Test failures (syntax errors)
You're missing commas in these examples:
Failed example:
calculate([5, 5] 3)
Exception raised:
Traceback (most recent call last):
File "C:\Program Files\Python39\lib\doctest.py", line 1336, in __run
exec(compile(example.source, filename, "single",
File "<doctest __main__.calculate[2]>", line 1
calculate([5, 5] 3)
^
SyntaxError: invalid syntax
Failed example:
calculate([9, 3] 4)
Exception raised:
Traceback (most recent call last):
File "C:\Program Files\Python39\lib\doctest.py", line 1336, in __run
exec(compile(example.source, filename, "single",
File "<doctest __main__.calculate[3]>", line 1
calculate([9, 3] 4)
^
SyntaxError: invalid syntax
Failed example:
calculate([4, 4] 5)
Exception raised:
Traceback (most recent call last):
File "C:\Program Files\Python39\lib\doctest.py", line 1336, in __run
exec(compile(example.source, filename, "single",
File "<doctest __main__.calculate[4]>", line 1
calculate([4, 4] 5)
^
SyntaxError: invalid syntax
Killing the interpreter
If you add a call to doctest.testmod()
after the call to should_calculate_again()
, you find the doctests never run. The reason: sys.exit()
kills the interpreter. Any tests you intend to run cannot run, or never get a chance to report their final status.
Changing sys.exit(...)
to print(...)
and break
statements would terminate the while loop, and should_calculate_again()
would return normally.
TL;DR: You almost never need to or want to call sys.exit()
.
-
\$\begingroup\$ Thank you for your feedback. I didn't know about the
doctest
module, I thought that the docstring just explains what the code do. So, for the wrong outputs part, I'm still working on returning the right data type based on user input. I have fixed the syntax errors and changed thesys.exit()
to the ones you provided. And aboutsys
.exit(), would it still be the same if I just use
exit()`? \$\endgroup\$CoreVisional– CoreVisional2021年08月18日 17:01:19 +00:00Commented Aug 18, 2021 at 17:01 -
\$\begingroup\$ The docstring is describing how to use the function. After the module is imported, typing
help(calculate)
at the Python prompt will present the help information to the user. Including examples of how the function behaves or is used is a great addition to the documentation. As a bonus, it can double as a built-in unit test, via thedoctest
module. I wouldn't include every last test inside the docstring -- that makes the help text too long and unreadable. External unit tests should be used for complete code coverage. But the tests you do include in the help text should pass. \$\endgroup\$AJNeufeld– AJNeufeld2021年08月18日 17:18:02 +00:00Commented Aug 18, 2021 at 17:18 -
\$\begingroup\$ My test of
exit(message)
andsys.exit(message)
show the same behaviour. One is probably implemented in terms of the other. The point is not to call either of those functions. Instead, let the script natural terminate by returning from the last function that was called and reaching the end of top-level script. Consider a unit test, written in Python, that sets the standard input to"2\n2.0\n3.03円\nNo\n"
, captures standard output, callsshould_calculate_again()
and tries to check the correct output is generated. It would neither pass nor fail because the Python interpreter exited. \$\endgroup\$AJNeufeld– AJNeufeld2021年08月18日 17:26:27 +00:00Commented Aug 18, 2021 at 17:26 -
\$\begingroup\$ Hmm, still new to this
doctest
module. So, how much is too much to include each test in the docstring? I might've confused myself thinking that docstring is just there to explain the code because I've seen so many examples providing explanation of the function. As for your unit test example, I'm not not sure what do you mean by neither pass nor fail. You would get your result before the program asks if you want to continue or not. \$\endgroup\$CoreVisional– CoreVisional2021年08月18日 22:13:06 +00:00Commented Aug 18, 2021 at 22:13 -
1\$\begingroup\$ See what about exceptions for how to use docktest to test the correct exceptions are raised. But again, is the docktest example with an exception helping a user understand how to use the function??? If not, the test is better off done in a unittest instead of polluting the
"""docstring"""
. \$\endgroup\$AJNeufeld– AJNeufeld2021年08月19日 04:15:17 +00:00Commented Aug 19, 2021 at 4:15