18.7 PyCalc: A Calculator Program/Object
To wrap up this chapter, I'm going to show you a practical application for some of the parsing technology introduced in the previous section. This section
18.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
Figure 18-8. The calc0 script in action on
|
Lesson 4: Embedding Beats Parsers
The calculator uses
eval
and
exec
to call Python's parser/interpreter at run-time instead of analyzing and evaluating expressions manually. In effect, the calculator runs embedded Python code from a Python program. This works because Python's development environment (the parser and
The net effect here is that the entire expression
Furthermore, Python can take on the flavor of any application. If a language interface requires application-specific extensions, just add Python classes, or export an API for use in embedded Python code as a C extension. By evaluating Python code that uses application-specific extensions, custom parsers become almost completely unnecessary.
There's also a critical added benefit to this approach: embedded Python code has access to all the tools and features of a powerful, full-blown programming language. It can use lists, functions, classes, external modules, and even larger Python tools like Tkinter,
|
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,
exec runs an arbitrary Python statement represented as a string; there's 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
By default a code string's namespace defaults to the caller's namespaces. If we didn't pass in dictionaries here, the strings would run in the eval method's namespace. Since the method's 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
If that fails due to a syntax error, it tries evaluating the string as a statement using exec .
Finally, if both attempts fail, it
Statements and invalid expressions might be parsed twice, but the overhead doesn't matter here, and you can't 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 calculator's namespace.
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 calculator's constructor to add extra widgets.
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 calculator's widget package, and extras, to a common parent.
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.
Of course, real calculators don't usually work by building up expression strings and evaluating them all at once; that approach is really little more than a
Lesson 5: Reusability Is Power
Though simple, attaching and subclassing the calculator graphically, as shown in Figure 18-9, illustrates the power of Python as a tool for writing reusable software. By coding programs with modules and classes,
In fact, code reuse is one of Python's major strengths and has been one of the main themes of this book thus far. Good object-oriented design takes some practice and forethought, and the benefits of code reuse aren't apparent immediately. And sometimes we're more interested in a quick fix rather than a future use for the code.
But coding with some reusability in mind can save development time in the long run. For instance, the
|
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.
As usual, let's 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 PyCalc's main window. By default, it shows operand buttons in black-on-blue (and
If you do run this, you'll 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. I'll explain how this evaluation works in a moment.
PyCalc's
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
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",
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 calculator's namespace dictionary is thrown up in the main window, for use in larger expressions. You can use this as an escape mechanism to
PyCalc supports long integers (unlimited precision),
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
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
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
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 calculator's entry field, ready for use in another operation. This evaluation algorithm is probably best described by working through examples. Let's 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
1) Entered keys: "5 * 3 + 4 <eval>" [result = 19] (['*'], ['5', '3']) [on '+' press: displays "15"] (['+'], ['15', '4']) [on 'eval' press: displays "19"]
Note that the pending (
2) "5 + 3 * 4 <eval>" [result = 17] (['+', '*'], ['5', '3', '4']) [on 'eval' press] (['+'], ['5', '12']) [displays "17"]
Here, the pending
+
isn't evaluated when the
*
button is pressed: since
*
binds tighter, we need to
3) "5 + 3 + 4 <eval>" [result = 12] (['+'], ['5', '3']) [on the second '+'] (['+'], ['8', '4']) [on 'eval']
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't matter for + and * operations:
4) "1 + 3 * ( 1 + 3 * 4 ) <eval>" [result = 40]
(['+', '*', '(', '+', '*'], ['1', '3', '1', '3', '4']) [on ')']
(['+', '*', '(', '+'], ['1', '3', '1', '12']) [displays "13"]
(['+', '*'], ['1', '3', '13']) [on 'eval']
(['+'], ['1', '39'])
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
5) "1 + 3 * ( 1 + 3 * 4 <eval>" [result = *ERROR*]
(['+', '*', '(', '+', '*'], ['1', '3', '1', '3', '4']) [on eval]
(['+', '*', '(', '+'], ['1', '3', '1', '12'])
(['+', '*', '('], ['1', '3', '13'])
(['+', '*'], ['1', '*ERROR*'])
(['+'], ['*ERROR*'])
(['+'], ['*ERROR*', '*ERROR*'])
This string triggers an error. PyCalc is casual about error handling. Many errors are made
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, it's 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. It's a
#!/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 = 'black', 'skyblue', ('courier', 16, 'bold') # 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('<KeyPress>', self.onKeyboard)
self.entry.config(state='disabled')
else:
self.entry.config(state='normal')
self.entry.focus( )
def makeWidgets(self, fg, bg, font): # 7 frames plus text-entry
self.entry = entry(self, TOP, self.text) # font, color configurable
for row in self.Operands:
frm = frame(self, TOP)
for char in row:
button(frm, LEFT, char,
lambda x=self, y=char: x.onOperand(y),
fg=fg, bg=bg, font=font)
frm = frame(self, TOP)
for char in self.Operators:
button(frm, LEFT, char,
lambda x=self, y=char: x.onOperator(y),
fg=bg, bg=fg, font=font)
frm = frame(self, TOP)
button(frm, LEFT, 'cmd ', self.onMakeCmdline)
button(frm, LEFT, 'dot ', lambda x=self: x.onOperand('.'))
button(frm, LEFT, 'long', lambda x=self: x.text.set(x.text.get( )+'L'))
button(frm, LEFT, 'help', self.help)
button(frm, LEFT, 'quit', self.quit) # from guimixin
frm = frame(self, BOTTOM)
button(frm, LEFT, 'eval ', self.onEval)
button(frm, LEFT, 'hist ', self.onHist)
button(frm, LEFT, 'clear', self.onClear)
def onClear(self):
self.eval.clear( )
self.text.set('0')
self.erase = 1
def onEval(self):
self.eval.shiftOpnd(self.text.get( )) # last or only opnd
self.eval.closeall( ) # apply all optrs left
self.text.set(self.eval.popOpnd( )) # need to pop: optr next?
self.erase = 1
def onOperand(self, char):
if char == '(':
self.eval.open( )
self.text.set('(') # clear text next
self.erase = 1
elif char == ')':
self.eval.shiftOpnd(self.text.get( )) # last or only nested opnd
self.eval.close( ) # pop here too: optr next?
self.text.set(self.eval.popOpnd( ))
self.erase = 1
else:
if self.erase:
self.text.set(char) # clears last value
else:
self.text.set(self.text.get( ) + char) # else append to opnd
self.erase = 0
def onOperator(self, char):
self.eval.shiftOpnd(self.text.get( )) # push opnd on left
self.eval.shiftOptr(char) # eval exprs to left?
self.text.set(self.eval.topOpnd( )) # push optr, show opndresult
self.erase = 1 # erased on next opnd'('
def onMakeCmdline(self):
new = Toplevel( ) # new top-level window
new.title('PyCalc command line') # arbitrary python code
frm = frame(new, TOP) # only the Entry expands
label(frm, LEFT, '>>>').pack(expand=NO)
var = StringVar( )
ent = entry(frm, LEFT, var, width=40)
onButton = (lambda s=self, v=var, e=ent: s.onCmdline(v,e))
onReturn = (lambda event, s=self, v=var, e=ent: s.onCmdline(v,e))
button(frm, RIGHT, 'Run', onButton).pack(expand=NO)
ent.bind('<Return>', onReturn)
var.set(self.text.get( ))
def onCmdline(self, var, ent): # eval cmdline popup input
try:
value = self.eval.runstring(var.get( ))
var.set('OKAY')
if value != None: # run in eval namespace dict
self.text.set(value) # expression or statement
self.erase = 1
var.set('OKAY => '+ value)
except: # result in calc field
var.set('ERROR') # status in popup field
ent.icursor(END) # insert point after text
ent.select_range(0, END) # select msg so next key deletes
def onKeyboard(self, event):
pressed = event.char # on keyboard press event
if pressed != '': # pretend button was pressed
if pressed in self.Operators:
self.onOperator(pressed)
else:
for row in self.Operands:
if pressed in row:
self.onOperand(pressed)
break
else:
if pressed == '.':
self.onOperand(pressed) # can start opnd
if pressed in 'Ll':
self.text.set(self.text.get( )+'L') # can't: no erase
elif pressed == '\r':
self.onEval( ) # enter key = eval
elif pressed == ' ':
self.onClear( ) # spacebar = clear
elif pressed == '\b':
self.text.set(self.text.get( )[:-1]) # backspace
elif pressed == '?':
self.help( )
def onHist(self):
# show recent calcs log popup
# self.infobox('PyCalc History', self.eval.getHist( ))
from ScrolledText import ScrolledText
new = Toplevel( ) # make new window
ok = Button(new, text="OK", command=new.destroy)
ok.pack(pady=1, side=BOTTOM) # pack first=clip last
text = ScrolledText(new, bg='beige') # add Text + scrollbar
text.insert('0.0', self.eval.getHist( )) # get Evaluator text
text.pack(expand=YES, fill=BOTH)
# new window goes away on ok press or enter key
new.title("PyCalc History")
new.bind("<Return>", (lambda event, new=new: new.destroy( )))
ok.focus_set( ) # make new window modal:
new.grab_set( ) # get keyboard focus, grab app
new.wait_window( ) # don't return till new.destroy
def help(self):
self.infobox('PyCalc', 'PyCalc 2.0\n'
'A Python/Tk calculator\n'
'August, 1999\n'
'Programming Python 2E\n\n'
'Use mouse or keyboard to\n'
'input numbers and operators,\n'
'or type code in cmd popup')
####################################
# the expression evaluator class
# embedded in and used by a CalcGui
# instance, to perform calculations
####################################
class Evaluator:
def __init__(self):
self.names = {} # a names-space for my vars
self.opnd, self.optr = [], [] # two empty stacks
self.hist = [] # my prev calcs history log
self.runstring("from math import *") # preimport math modules
self.runstring("from random import *") # into calc's namespace
def clear(self):
self.opnd, self.optr = [], [] # leave names intact
if len(self.hist) > 64: # don't let hist get too big
self.hist = ['clear']
else:
self.hist.append('--clear--')
def popOpnd(self):
value = self.opnd[-1] # pop/return toplast opnd
self.opnd[-1:] = [] # to display and shift next
return value
def topOpnd(self):
return self.opnd[-1] # top operand (end of list)
def open(self):
self.optr.append('(') # treat '(' like an operator
def close(self): # on ')' pop downto higest '('
self.shiftOptr(')') # ok if empty: stays empty
self.optr[-2:] = [] # pop, or added again by optr
def closeall(self):
while self.optr: # force rest on 'eval'
self.reduce( ) # last may be a var name
try:
self.opnd[0] = self.runstring(self.opnd[0])
except:
self.opnd[0] = '*ERROR*' # pop else added again next:
afterMe = {'*': ['+', '-', '(', '='], # class member
'/': ['+', '-', '(', '='], # optrs to not pop for key
'+': ['(', '='], # if prior optr is this: push
'-': ['(', '='], # else: pop/eval prior optr
')': ['(', '='], # all left-associative as is
'=': ['('] }
def shiftOpnd(self, newopnd): # push opnd at optr, ')', eval
self.opnd.append(newopnd)
def shiftOptr(self, newoptr): # apply ops with <= priority
while (self.optr and
self.optr[-1] not in self.afterMe[newoptr]):
self.reduce( )
self.optr.append(newoptr) # push this op above result
# optrs assume next opnd erases
def reduce(self):
trace(self.optr, self.opnd)
try: # collapse the top expr
operator = self.optr[-1] # pop top optr (at end)
[left, right] = self.opnd[-2:] # pop top 2 opnds (at end)
self.optr[-1:] = [] # delete slice in-place
self.opnd[-2:] = []
result = self.runstring(left + operator + right)
if result == None:
result = left # assignment? key var name
self.opnd.append(result) # push result string back
except:
self.opnd.append('*ERROR*') # stack/number/name error
def runstring(self, code):
try:
result = `eval(code, self.names, self.names)` # try expr: string
self.hist.append(code + ' => ' + result) # add to hist log
except:
exec code in self.names, self.names # try stmt: None
self.hist.append(code)
result = None
return result
def getHist(self):
import string
return string.join(self.hist, '\n')
def getCalcArgs( ):
from sys import argv
config = {} # get cmdline args in a dict
for arg in argv[1:]: # ex: -bg black -fg red
if arg in ['-bg', '-fg']: # font not yet supported
try:
config[arg[1:]] = argv[argv.index(arg) + 1]
except:
pass
return config
if __name__ == '__main__':
apply(CalcGui, (), getCalcArgs()).mainloop( ) # on default toplevel window
PyCalc serves a standalone program on my desktop, but it's 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 PyCalc's 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
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
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
########################################################################
# 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 entry's 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
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='black') calculator_plus_emb.CalcGuiPlus(fg='black', bg='red') 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, that's how the PyGadgets and PyDemos launcher bars run calculators, so see their code for more details.
Lesson 6: Have Fun
In closing, here's a less
As we've seen in this book, there are a number of factors behind this distinction -- lack of declarations, no compile steps, simple syntax, useful built-in objects, and so on. Python is
For instance, the calculator programs shown earlier were first thrown together in one afternoon, starting from vague, incomplete goals. There was no analysis phase, no formal design, and no official coding stage. I typed up some ideas and they worked. Moreover, Python's interactive nature allowed me to experiment with new ideas and get immediate feedback. Since its initial development, the calculator has been polished and expanded, but the core implementation remains unchanged.
Naturally, such a laid-back programming mode doesn't work for every project. Sometimes more up-front design is warranted. For more demanding
|