Recipe 3.14. Using Python as a Simple Adding Machine
Credit: Brett Cannon
Problem
You want to use Python as a simple
adding machine, with accurate decimal (not binary floating-point!)
computations and a "tape" that
shows the numbers in an uncluttered columnar view.
Solution
To perform the computations, we can rely on the
decimal module. We accept input lines, each made
up of a number followed by an arithmetic operator, an empty line to
request the current total, and q to terminate the
program:
import decimal, re, operator
parse_input = re.compile(r'''(?x)
# allow comments and whitespace in the RE
(\d+\.?\d*) # number with optional decimal part
\s* # optional whitespace
([-+/*]) # operator
$''') # end-of-string
oper = { '+': operator.add, '-': operator.sub,
'*': operator.mul, '/': operator.truediv,
}
total = decimal.Decimal('0')
def print_total( ):
print '== == =\n', total
print ""Welcome to Adding Machine:
Enter a number and operator,
an empty line to see the current subtotal,
or q to quit: ""
while True:
try:
tape_line = raw_input( ).strip( )
except EOFError:
tape_line = 'q'
if not tape_line:
print_total( )
continue
elif tape_line == 'q':
print_total( )
break
try:
num_text, op = parse_input.match(tape_line).groups( )
except AttributeError:
print 'Invalid entry: %r' % tape_line
print 'Enter number and operator, empty line for total, q to quit'
continue
total = oper[op](total, decimal.Decimal(num_text))
Discussion
Python's interactive interpreter is often a useful
calculator, but a simpler "adding
machine" also has its uses. For example, an
expression such as 2345634+2894756-2345823 is not easy to read, so
checking that you're entering the right numbers for
a computation is not all that simple. An adding
machine's tape shows numbers in a simple,
uncluttered columnar view, making it easier to double check what you
have entered. Moreover, the decimal module
performs computations in the normal, decimal-based way we need in
real life, rather than in the floating-point arithmetic preferred by
scientists, engineers, and today's
computers.When you run the script in this recipe from a normal command shell
(this script is not meant to be run from within
a Python interactive interpreter!), the script prompts you once, and
then just sits there, waiting for input. Type a number (one or more
digits, then optionally a decimal point, then optionally more
digits), followed by an operator (/,
*, -, or +
the four operator characters you find on the numeric
keypad on your keyboard), and then press return. The script applies
the number to the running total using the operator. To output the
current total, just enter a blank line. To quit, enter the letter
q and press return. This simple interface matches
the input/output conventions of a typical simple adding machine,
removing the need to have some other form of output.The decimal package
is part of Python's standard library since version
2.4. If you're still using Python 2.3, visit
http://www.taniquetil.com.ar/facundo/bdv/image/library/english/10241_get_decimall
and download and install the package in whatever form is most
convenient for you. decimal allows high-precision
decimal arithmetic, which is more convenient for many uses (such as
any computation involving money) than the binary floating-point
computations that are faster on today's computers
and which Python uses by default. No more lost pennies due to
hard-to-understand issues with binary floating point! As demonstrated
in Recipe 3.13, you can
even change the rounding rules from the default of
ROUND_HALF_EVEN, if you really need to.This recipe's script is meant to be very simple, so
many improvements are possible. A useful enhancement would be to keep
the "tape" on disk for later
checking. You can do that easily, by adding, just before the loop, a
statement to open some appropriate text file for append:
tapefile = open('tapefile.txt', 'a')and, just after the try/except
statement that obtains a value for tape_line, a
statement to write that value to the file:
tapefile.write(tape_line+'\n')If you do want to make these additions, you will probably also want
to enrich function print_total so that it writes to
the "tape" file as well as to the
command window, therefore, change the function to:
def print_total( ):The write method of a file
print '== == =\n', total
tapefile.write('== == =\n' + str(total) + '\n')
object accepts a string as its argument and does not implicitly
terminate the line as the print statement does, so
we need to explicitly call the str built-in
function and explicitly add '\n' as needed.
Alternatively, the second statement in this version of
print_total could be coded in a way closer to the
first one:
print >>tapefile, '== == =\n', totalSome people really dislike this print
>>somefile, syntax, but it can come in handy in cases
such as this one.More ambitious improvements would be to remove the need to press
Return after each operator (that would require performing unbuffered
input and dealing with one character at a time, rather than using the
handy but line-oriented built-in function
raw_input as the recipe doessee Recipe 2.23 for a cross-platform
way to get unbuffered input), to add a clear
function (or clarify to users that inputting 0*
will zero out the "tape"), and even
to add a GUI that looks like an adding machine. However,
I'm leaving any such improvements as exercises for
the reader.One important point about the recipe's
implementation is the oper dictionary, which uses
operator characters (/, *,
-, +) as keys and the
appropriate arithmetic functions from the built-in module
operator, as corresponding
values. The same effect could be obtained, more verbosely, by a
"tree" of
if/elif, such
as:
if op == '+':However, Python dictionaries are very idiomatic and handy for such
total = total + decimal.Decimal(num_text)
elif op == '-':
total = total - decimal.Decimal(num_text)
elif op == '*':
<line_annotation>... and so on ...</line_annotation>
uses, and they lead to less repetitious and thus more maintainable
code.
See Also
decimal is documented in the Python 2.4
Library Reference, and is available for
download to use with 2.3 at http://www.taniquetil.com.ar/facundo/bdv/image/library/english/10241_get_decimall;
you can read the decimal PEP 327 at http://www.python.org/peps/pep-0327l.