PyCalc: A Calculator Program/Object
To wrap up this chapter, Im going to show you a practical application for some of the parsing technology introduced in the previous section. This section presents PyCalc -- a Python calculator program with a graphical interface similar to the calculator programs available on most window systems. But like most of the GUI examples in this book, PyCalc offers a few advantages over existing calculators. Because PyCalc is written in Python, it is both easily customized and widely portable across window platforms. And because it is implemented with classes, it is both a standalone program and a reusable object library.
.7.1 A Simple Calculator GUI
Before I show you how to write a full-blown calculator, though, the module shown in Example 18-13 starts this discussion in simpler terms. It implements a limited calculator GUI, whose buttons just add text to the input field at the top, to compose a Python expression string. Fetching and running the string all at once produces results. Figure 18-8 shows the window this module makes when run as a top-level script.
Figure 18-8. The calc0 script in action on Windows (result=160.283)
Example 18-13. PP2ELangCalculatorcalc0.py
#!/usr/local/bin/python # a simple calculator GUI: expressions run all at once with eval/exec from Tkinter import * from PP2E.Dbase.TableBrowser.guitools import frame, button, entry class CalcGui(Frame): def __init__(self, parent=None): # an extended frame Frame.__init__(self, parent) # on default top-level self.pack(expand=YES, fill=BOTH) # all parts expandable self.master.title(Python Calculator 0.1) # 6 frames plus entry self.master.iconname("pcalc1") self.names = {} # namespace for variables text = StringVar( ) entry(self, TOP, text) rows = ["abcd", "0123", "4567", "89( )"] for row in rows: frm = frame(self, TOP) for char in row: button(frm, LEFT, char, lambda x=text, y=char: x.set(x.get( ) + y)) frm = frame(self, TOP) for char in "+-*/=": button(frm, LEFT, char, lambda x=text, y=char: x.set(x.get( )+ +y+ )) frm = frame(self, BOTTOM) button(frm, LEFT, eval, lambda x=self, y=text: x.eval(y) ) button(frm, LEFT, clear, lambda x=text: x.set(\) ) def eval(self, text): try: text.set(`eval(text.get( ), self.names, self.names)`) except SyntaxError: try: exec(text.get( ), self.names, self.names) except: text.set("ERROR") # bad as statement too? else: text.set(\) # worked as a statement except: text.set("ERROR") # other eval expression errors if __name__ == \__main__: CalcGui().mainloop( )
.7.1.1 Building the GUI
Now, this is about as simple as a calculator can be, but it demonstrates the basics. This window comes up with buttons for entry of numbers, variable names, and operators. It is built by attaching buttons to frames: each row of buttons is a nested Frame, and the GUI itself is a Frame subclass, with an attached Entry and six embedded row frames (grids would work here, too). The calculators frame, entry field, and buttons are made expandable in the imported guitools utility module.
This calculator builds up a string to pass to the Python interpreter all at once on "eval" button presses. Because you can type any Python expression or statement in the entry field, the buttons are really just a convenience. In fact, the entry field isn much more than a command line. Try typing import sys and then dir(sys) to display sys module attributes in the input field at the top -- its not what you normally do with a calculator, but demonstrative nevertheless.[3]
[3] And once again, I need to warn you about running strings like this if you can be sure they won cause damage. See the rexec restricted execution mode module in Chapter 15, for more details.
In CalcGuis constructor, buttons are coded as lists of strings; each string represents a row and each character in the string represents a button. Lambdas with default argument values are used to set callback data for each button. The callback functions save the buttons character and the linked text entry variable, so that the character can be added to the end of the entry widgets current string on a press.
.7.1.2 Running code strings
This module implements a GUI calculator in 45 lines of code (counting comments and blank lines). But to be honest, it cheats: expression evaluation is delegated to Python. In fact, the built-in eval and exec tools do most of the work here:
- eval parses, evaluates, and returns the result of a Python expression represented as a string.
- exec runs an arbitrary Python statement represented as a string; theres no return value because the code is a string.
Both accept optional dictionaries to be used as global and local namespaces for assigning and evaluating names used in the code strings. In the calculator, self.names becomes a symbol table for running calculator expressions. A related Python function, compile, can be used to precompile code strings before passing them to eval and exec (use it if you need to run the same string many times).
By default a code strings namespace defaults to the callers namespaces. If we didn pass in dictionaries here, the strings would run in the eval methods namespace. Since the methods local namespace goes away after the method call returns, there would be no way to retain names assigned in the string. Notice the use of nested exception handlers in the eval method:
- It first assumes the string is an expression and tries the built-in eval function.
- If that fails due to a syntax error, it tries evaluating the string as a statement using exec.
- Finally, if both attempts fail, it reports an error in the string (a syntax error, undefined name, etc.).
Statements and invalid expressions might be parsed twice, but the overhead doesn matter here, and you can tell if a string is an expression or a statement without parsing it manually. Note that the "eval" button evaluates expressions, but = sets Python variables by running an assignment statement. Variable names are combinations of letter keys abcd (or any name typed directly). They are assigned and evaluated in a dictionary used to represent the calculators namespace.
.7.1.3 Extending and attaching
Clients that reuse this calculator are as simple as the calculator itself. Like most class-based Tkinter GUIs, this one can be extended in subclasses -- Example 18-14 customizes the simple calculators constructor to add extra widgets.
Example 18-14. PP2ELangCalculatorcalc0ext.py
from Tkinter import * from calc0 import CalcGui class Inner(CalcGui): # extend gui def __init__(self): CalcGui.__init__(self) Label(self, text=Calc Subclass).pack( ) # add after Button(self, text=Quit, command=self.quit).pack( ) # top implied Inner().mainloop( )
It can also be embedded in a container class -- Example 18-15 attaches the simple calculators widget package, and extras, to a common parent.
Example 18-15. PP2ELangCalculatorcalc0emb.py
from Tkinter import * from calc0 import CalcGui # add parent, no master calls class Outer: def __init__(self, parent): # embed gui Label(parent, text=Calc Attachment).pack( ) # side=top CalcGui(parent) # add calc frame Button(parent, text=Quit, command=parent.quit).pack( ) root = Tk( ) Outer(root) root.mainloop( )
Figure 18-9 shows the result of running both of these scripts from different command lines. Both have a distinct input field at the top. This works; but to see a more practical application of such reuse techniques, we need to make the underlying calculator more practical, too.
Figure 18-9. The calc0 scripts object attached and extended
.7.2 Pycalc -- A Real Calculator GUI
Of course, real calculators don usually work by building up expression strings and evaluating them all at once; that approach is really little more than a glorified Python command line. Traditionally, expressions are evaluated in piecemeal fashion as they are entered, and temporary results are displayed as soon as they are computed. Implementing this behavior is a bit more work: expressions must be evaluated manually instead of calling the eval function only once. But the end result is much more useful and intuitive.
This section presents the implementation of PyCalc -- a Python/Tkinter program that implements such a traditional calculator GUI. Although its evaluation logic is more complex than the simpler calculator above, it demonstrates advanced programming techniques and serves as an interesting finale for this chapter.
.7.2.1 Running PyCalc
As usual, lets look at the GUI before the code. You can run PyCalc from the PyGadgets and PyDemos launcher bars at the top of the examples tree, or by directly running file calculator.py listed below (e.g., click it in a file explorer). Figure 18-10 shows PyCalcs main window. By default, it shows operand buttons in black-on-blue (and opposite for operator buttons), but font and color options can be passed in to the GUI classs constructor method. Of course, that means gray-on-gray in this book, so youll have to run PyCalc yourself to see what I mean.
Figure 18-10. PyCalc calculator at work on Windows
If you do run this, youll notice that PyCalc implements a normal calculator model -- expressions are evaluated as entered, not all at once at the end. That is, parts of an expression are computed and displayed as soon as operator precedence and manually typed parentheses allow. Ill explain how this evaluation works in a moment.
PyCalcs CalcGui class builds the GUI interface as frames of buttons much like the simple calculator of the previous section, but PyCalc adds a host of new features. Among them are another row of action buttons, inherited methods from GuiMixin (presented in Chapter 9), a new "cmd" button that pops up nonmodal dialogs for entry of arbitrary Python code, and a recent calculations history pop-up. Figure 18-11 captures some of PyCalcs pop-up windows.
Figure 18-11. PyCalc calculator with some of its pop-ups
You may enter expressions in PyCalc by clicking buttons in the GUI, typing full expressions in command-line pop-ups, or typing keys on your keyboard. PyCalc intercepts key press events and interprets them the same as corresponding button presses; typing + is like pressing button +, the space bar key is "clear", Enter is "eval", backspace erases a character, and ? is like pressing "help".
The command-line pop-up windows are nonmodal (you can pop up as many as you like). They accept any Python code -- press the Run button or your Enter key to evaluate text in the input field. The result of evaluating this code in the calculators namespace dictionary is thrown up in the main window, for use in larger expressions. You can use this as an escape mechanism to employ external tools in your calculations. For instance, you can import and use functions coded in Python or C within these pop-ups. The current value in the main calculator window is stored in newly opened command-line pop-ups, too, for use in typed expressions.
PyCalc supports long integers (unlimited precision), negatives, and floating-point numbers, just because Python does: individual operands and expressions are still evaluated with the eval built-in, which calls the Python parser/interpreter at run-time. Variable names can be assigned and referenced in the main window with the letter, =, and "eval" keys; they are assigned in the calculators namespace dictionary (more complex variable names may be typed in command-line pop-ups). Note the use of pi in the history window: PyCalc preimports names in the math and random modules into the namespace where expressions are evaluated.
.7.2.2 Evaluating expressions with stacks
Now that you have the general idea of what PyCalc does, I need to say a little bit about how it does what it does. Most of the changes in this version involve managing the expression display and evaluating expressions. PyCalc is structured as two classes:
- The CalcGui class manages the GUI itself. It controls input events and is in charge of the main windows display field at the top. It doesn evaluate expressions, though; for that, it sends operators and operands entered in the GUI to an embedded instance of the Evaluator class.
- The Evaluator class manages two stacks. One stack records pending operators (e.g., +), and one records pending operands (e.g, 3.141). Temporary results are computed as new operators are sent from CalcGui and pushed onto the operands stack.
As you can see from this, the magic of expression evaluation boils down to juggling the operator and operand stacks. While scanning expression strings from left to right as they are entered, operands are pushed along the way, but operators delimit operands and may trigger temporary results before they are pushed. Heres the general scenario:
- When a new operator is seen (i.e., when an operator button or key is pressed), the prior operand in the entry field is pushed onto the operands stack.
- The operator is then added to the operators stack, but only after all pending operators of higher precedence have been popped and applied to pending operands (e.g., pressing + makes any pending * operators on the stack fire).
- When "eval" is pressed, all remaining operators are popped and applied to all remaining operands, and the result is the last remaining value on the operands stack.
In the end, the last value on the operands stack is displayed in the calculators entry field, ready for use in another operation. This evaluation algorithm is probably best described by working through examples. Lets step through the entry of a few expressions and watch the evaluation stacks grow.
PyCalc stack tracing is enabled with the debugme flag in the module; if true, the operator and operand stacks are displayed on stdout each time the Evaluator class is about to apply an operator and reduce (pop) the stacks. A tuple holding the stack lists (operators, operands) is printed on each stack reduction; tops of stack are at the ends of the lists. For instance, here is the console output after typing and evaluating a simple string:
1) Entered keys: "5 * 3 + 4 Note that the pending (stacked) * subexpression is
evaluated when the + is pressed:
* operators bind tighter than
+, so the code is evaluated immediately before the
+ operator is pushed. When the
+ button is pressed, the entry field contains 3.
In general, the entry field always holds the prior operand when an
operator button is pressed. Since the text entrys value is
pushed onto the operands stack before the operator is applied, we
have to pop results before displaying them after "eval"
or ) is pressed (otherwise the results are pushed
onto the stack twice):
2) "5 + 3 * 4 Here, the pending + isn evaluated when the
* button is pressed: since *
binds tighter, we need to postpone the + until the
* can be evaluated. The *
operator isn popped until its right operand has been seen. On
the "eval" press there are two operators to pop and apply
to operand stack entries:
3) "5 + 3 + 4 For strings of same-precedence operators like this one, we pop and
evaluate immediately as we scan left to right, instead of postponing
evaluation. This results in a left-associative evaluation, in the
absence of parentheses: 5+3+4 is evaluated as
((5+3)+4). Order doesn matter for
+ and * operations:
4) "1 + 3 * ( 1 + 3 * 4 ) In this case, all the operators and operands are stacked (postponed)
until we press the ) button at the end. When the
) button is pressed, the parenthesized
subexpression is popped and evaluated, and 13 is displayed in the
entry field. On pressing "eval", the rest is evaluated,
and the final result (40) is shown. The result is the left operand of
another operator. In fact, any temporary result can be used again: if
we keep pressing an operator button without typing new operands,
its reapplied to the result of the prior press. Figure 18-12 shows how the two stacks look at their highest
level while scanning the expression in the preceding example trace.
The top operator is applied to the top two operands and the result is
pushed back for the operator below:
5) "1 + 3 * ( 1 + 3 * 4
This string triggers an error. PyCalc is casual about error handling.
Many errors are made impossible by the algorithm itself, but things
like unmatched parentheses still trip up the evaluator. But instead
of trying to detect all possible error cases explicitly, a general
try statement in the reduce
method is used to catch them all: expression errors, undefined name
errors, syntax errors, etc.
Operands and temporary results are always stacked as strings, and
each operator are applied by calling eval. When an
error occurs inside an expression, a result operand of
*ERROR* is pushed, which makes all remaining
operators fail in eval, too.
*ERROR* percolates to the top of the expression.
At the end, its the last operand and is displayed in the text
entry field to alert you of the mistake.
Example 18-16 contains the PyCalc source module that puts
these ideas to work in the context of a GUI. Its a single-file
implementation (not counting utilities imported and reused). Study
the source for more details; and as usual, theres no
substitute for interacting with the program on your own to get a
better feel for its functionality.
#!/usr/local/bin/python
#########################################################################
# PyCalc 2.0: a Python/Tkinter calculator program and GUI component.
# evaluates expressions as they are entered, catches keyboard keys
# for expression entry; adds integrated command-line popups, recent
# calculations history display popup, fonts and colors configuration,
# help and about popups, preimported math/random constants, and more;
#########################################################################
from Tkinter import * # widgets, consts
from PP2E.Gui.Tools.guimixin import GuiMixin # quit method
from PP2E.Dbase.TableBrowser.guitools import * # widget builders
Fg, Bg, Font = lack, skyblue, (courier, 16, old) # default config
debugme = 1
def trace(*args):
if debugme: print args
###########################################
# the main class - handles user interface;
# an extended Frame, on new Toplevel, or
# embedded in another container widget
###########################################
class CalcGui(GuiMixin, Frame):
Operators = "+-*/=" # button lists
Operands = ["abcd", "0123", "4567", "89( )"] # customizable
def __init__(self, parent=None, fg=Fg, bg=Bg, font=Font):
Frame.__init__(self, parent)
self.pack(expand=YES, fill=BOTH) # all parts expandable
self.eval = Evaluator( ) # embed a stack handler
self.text = StringVar( ) # make a linked variable
self.text.set("0")
self.erase = 1 # clear "0" text next
self.makeWidgets(fg, bg, font) # build the gui itself
if not parent or not isinstance(parent, Frame):
self.master.title(PyCalc 2.0) # title iff owns window
self.master.iconname("PyCalc") # ditto for key bindings
self.master.bind(
PyCalc serves a standalone program on
my desktop, but its also useful in the context of other GUIs.
Like most of the GUI classes in this book, PyCalc can be customized
with subclass extensions, or embedded in a larger GUI with
attachment. The module in Example 18-17 demonstrates
one way to reuse PyCalcs CalcGui class by
extending and embedding, much as done for the simple calculator
earlier.
##########################################################################
# test calculator use as an extended and embedded gui component;
##########################################################################
from Tkinter import *
from calculator import CalcGui
from PP2E.Dbase.TableBrowser.guitools import *
def calcContainer(parent=None):
frm = Frame(parent)
frm.pack(expand=YES, fill=BOTH)
Label(frm, text=Calc Container).pack(side=TOP)
CalcGui(frm)
Label(frm, text=Calc Container).pack(side=BOTTOM)
return frm
class calcSubclass(CalcGui):
def makeWidgets(self, fg, bg, font):
Label(self, text=Calc Subclass).pack(side=TOP)
Label(self, text=Calc Subclass).pack(side=BOTTOM)
CalcGui.makeWidgets(self, fg, bg, font)
#Label(self, text=Calc Subclass).pack(side=BOTTOM)
if __name__ == \__main__:
import sys
if len(sys.argv) == 1: # % calculator_test.py
root = Tk( ) # run 3 calcs in same process
CalcGui(Toplevel( )) # each in a new toplevel window
calcContainer(Toplevel( ))
calcSubclass(Toplevel( ))
Button(root, text=quit, command=root.quit).pack( )
root.mainloop( )
if len(sys.argv) == 2: # % calculator_testl.py -
CalcGui().mainloop( ) # as a standalone window (default root)
elif len(sys.argv) == 3: # % calculator_test.py - -
calcContainer().mainloop( ) # as an embedded component
elif len(sys.argv) == 4: # % calculator_test.py - - -
calcSubclass().mainloop( ) # as a customized superclass
Figure 18-13 shows the result of running this script
with no command-line arguments. We get instances of the original
calculator class, plus the container and subclass classes defined in
this script, all attached to new top-level windows.
These two windows on the right reuse the core PyCalc code running in
the window on the left. All these windows all run in the same process
(e.g., quitting one quits them all), but they all function as
independent windows. Note that when running three calculators in the
same process like this, each has its own distinct expression
evaluation namespace because its a class instance attribute,
not a global module-level variable. Because of that, variables set in
one calculator are set in that calculator only, and don
overwrite settings made in other windows. Similarly, each calculator
has its own evaluation stack manager object, such that calculations
in one window don appear in or impact other windows at all.
The two extensions in this script are artificial, of
course -- they simply add labels at the top and bottom of the
window -- but the concept is widely applicable. You could reuse
the calculators class by attaching it to any GUI that needs a
calculator, and customize it with subclasses arbitrarily. Its
a reusable widget.
One obvious way to reuse the
calculator is to add additional expression feature
buttons -- square roots, inverses, cubes, and the like. You can
type such operations in the command-line pop-ups, but buttons are a
bit more convenient. Such features could also be added to the main
calculator implementation itself; but since the set of features that
will be useful may vary per user and application, a better approach
may be to add them in separate extensions. For instance, the class in
Example 18-18 adds a few extra buttons to PyCalc by
embedding (i.e., attaching) it in a container.
########################################################################
# a container with an extra row of buttons for common operations;
# a more useful customization: adds buttons for more operations (sqrt,
# 1/x, etc.) by embedding/composition, not subclassing; new buttons are
# added after entire CalGui frame because of the packing order/options;
########################################################################
from Tkinter import *
from calculator import CalcGui, getCalcArgs
from PP2E.Dbase.TableBrowser.guitools import frame, button, label
class CalcGuiPlus(Toplevel):
def __init__(self, **args):
Toplevel.__init__(self)
label(self, TOP, PyCalc Plus - Container)
self.calc = apply(CalcGui, (self,), args)
frm = frame(self, BOTTOM)
extras = [(sqrt, sqrt(%s)),
(x^2 , (%s)**2),
(x^3 , (%s)**3),
(1/x , 1.0/(%s))]
for (lab, expr) in extras:
button(frm, LEFT, lab, (lambda m=self.onExtra, e=expr: m(e)) )
button(frm, LEFT, pi , self.onPi)
def onExtra(self, expr):
text = self.calc.text
eval = self.calc.eval
try:
text.set(eval.runstring(expr % text.get( )))
except:
text.set(ERROR)
def onPi(self):
self.calc.text.set(self.calc.eval.runstring(pi))
if __name__ == \__main__:
root = Tk( )
button(root, TOP, Quit, root.quit)
apply(CalcGuiPlus, (), getCalcArgs()).mainloop( ) # -bg,-fg to calcgui
Because PyCalc is coded as a Python class, you can always achieve a
similar effect by extending PyCalc in a new subclass instead of
embedding it, as shown in Example 18-19.
##############################################################################
# a customization with an extra row of buttons for common operations;
# a more useful customization: adds buttons for more operations (sqrt,
# 1/x, etc.) by subclassing to extend the original class, not embedding;
# new buttons show up before frame attached to bottom be calcgui class;
##############################################################################
from Tkinter import *
from calculator import CalcGui, getCalcArgs
from PP2E.Dbase.TableBrowser.guitools import *
class CalcGuiPlus(CalcGui):
def makeWidgets(self, *args):
label(self, TOP, PyCalc Plus - Subclass)
apply(CalcGui.makeWidgets, (self,) + args)
frm = frame(self, BOTTOM)
extras = [(sqrt, sqrt(%s)),
(x^2 , (%s)**2),
(x^3 , (%s)**3),
(1/x , 1.0/(%s))]
for (lab, expr) in extras:
button(frm, LEFT, lab, (lambda m=self.onExtra, e=expr: m(e)) )
button(frm, LEFT, pi , self.onPi)
def onExtra(self, expr):
try:
self.text.set(self.eval.runstring(expr % self.text.get( )))
except:
self.text.set(ERROR)
def onPi(self):
self.text.set(self.eval.runstring(pi))
if __name__ == \__main__:
apply(CalcGuiPlus, (), getCalcArgs()).mainloop( ) # passes -bg, -fg on
Notice that these buttons callbacks use
1.0/x to force float-point division to be used for
inverses (integer division truncates remainders), and wrap entry
field values in parentheses (to sidestep precedence issues). They
could instead convert the entrys text to a number and do real
math, but Python does all the work automatically when expression
strings are run raw.
Also note that the buttons added by these scripts simply operate on
the current value in the entry field, immediately. Thats not
quite the same as expression operators applied with the stacks
evaluator (additional customizations are needed to make them true
operators). Still, these buttons prove the point these scripts are
out to make -- they use PyCalc as a component, both from the
outside and below.
Finally, to test both of the extended calculator classes, as well as
PyCalc configuration options, the script in Example 18-20 puts up four distinct calculator windows (this
is the script run by PyDemos).
#!/usr/local/bin/python
from Tkinter import Tk, Button, Toplevel
import calculator, calculator_plus_ext, calculator_plus_emb
# demo all 3 calculator flavors at once
# each is a distinct calculator object and window
root=Tk( )
calculator.CalcGui(Toplevel( ))
calculator.CalcGui(Toplevel( ), fg=white, bg=purple)
calculator_plus_ext.CalcGuiPlus(Toplevel( ), fg=gold, bg=lack)
calculator_plus_emb.CalcGuiPlus(fg=lack, bg=
ed)
Button(root, text=Quit Calcs, command=root.quit).pack( )
root.mainloop( )
Figure 18-14 shows the result -- four independent
calculators in top-level windows within the same process. The windows
on the left and right represent specialized reuses of PyCalc as a
component. Although it may not be obvious in this book, all four use
different color schemes; calculator classes accept color and font
configuration options and pass them down the call chain as needed.
As we learned earlier, these calculators could also be run as
independent processes by spawning command lines with the
launchmodes module we met in Chapter 3. In fact, thats how the PyGadgets and
PyDemos launcher bars run calculators, so see their code for more
details.
Figure 18-12. Evaluation stacks: 1 + 3 * (1 + 3 * 4)
.7.2.3 PyCalc source code
Example 18-16. PP2ELangCalculatorcalculator.py
.7.2.4 Using PyCalc as a component
Example 18-17. PP2ELangCalculatorcalculator_test.py
Figure 18-13. The calculator_test script: attaching and extending
.7.2.5 Adding new buttons in new components
Example 18-18. PP2ELangCalculatorcalculator_plus_emb.py
Example 18-19. PP2ELangCalculatorcalculator_plus_ext.py
Example 18-20. PP2ELangCalculatorcalculator_plusplus.py
Figure 18-14. The calculator_plusplus script: extend, embed, and configure
Категории