Section 21.7. PyCalc: A Calculator ProgramObject


21.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 preceding 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.

21.7.1. A Simple Calculator GUI

Before I show you how to write a full-blown calculator, though, the module shown in Example 21-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 in order to compose a Python expression string. Fetching and running the string all at once produces results. Figure 21-8 shows the window this module makes when run as a top-level script.

Figure 21-8. The calc0 script in action on Windows (result=160.283)


Example 21-13. PP3E\Lang\Calculator\calc0.py

 #!/usr/local/bin/python # a simple calculator GUI: expressions run all at once with eval/exec from Tkinter  import * from PP3E.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 char=char: text.set(text.get( ) + char))         frm = frame(self, TOP)         for char in "+-*/=":             button(frm, LEFT, char,                         lambda char=char: text.set(text.get( )+ ' ' + char + ' '))         frm = frame(self, BOTTOM)         button(frm, LEFT, 'eval',  lambda: self.eval(text) )         button(frm, LEFT, 'clear', lambda: text.set('') )     def eval(self, text):         try:             text.set(str(eval(text.get( ), self.names, self.names)))    # was 'x'         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( ) 

21.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 calculator's 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't 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 topit's not what you normally do with a calculator, but it is demonstrative nevertheless.[*]

[*] Once again, I need to warn you about running strings like this if you can't be sure they won't cause damage. If these strings can be entered by users you cannot trust, they will have access to anything on the computer that the Python process has access to. See the Chapter 18 discussion of the (now defunct) rexec module for more on this topic.

In CalcGui's constructor, buttons are coded as lists of strings; each string represents a row and each character in the string represents a button. Lambdas are used to save extra callback data for each button. The callback functions retain the button's character and the linked text entry variable so that the character can be added to the end of the entry widget's current string on a press.

Notice how we must pass in the loop variable as a default argument to some lambdas in this code. Recall from Chapter 8 how references within a lambda (or nested def) to names in an enclosing scope are evaluated when the nested function is called, not when it is created. When the generated function is called, enclosing scope references inside the lambda reflect their latest setting in the enclosing scope, which is not necessarily the values they held when the lambda expression ran. By contrast, defaults are evaluated at function creation time instead and so can remember the current values of loop variables. Without the defaults, each button would reflect the last iteration of the loop.

Lesson 4: Embedding Beats Parsers

The calculator uses eval and exec to call Python's parser/interpreter at runtime 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 bytecode compiler) is always a part of systems that use Python. Because there is no difference between the development and the delivery environments, Python's parser can be used by Python programs.

The net effect here is that the entire expression evaluator has been replaced with a single call to eval. In broader terms, this is a powerful technique to remember: the Python language itself can replace many small, custom languages. Besides saving development time, clients have to learn just one language, one that's potentially simple enough for end-user coding.

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 GUIs, shelve storage, multiple threads, network sockets, and web page fetches. You'd probably spend years trying to provide similar functionality in a custom language parser. Just ask Guido.


21.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; 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 related Python function, compile, can be used to precompile code strings to code objects before passing them to eval and exec (use it if you need to run the same string many times).

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:

  1. It first assumes the string is an expression and tries the built-in eval function.

  2. If that fails due to a syntax error, it tries evaluating the string as a statement using exec.

  3. Finally, if both attempts fail, it reports an error in the string (a syntax error, undefined name, and so on).

Statements and invalid expressions might be parsed twice, but the overhead doesn't matter here, and you can't tell whether 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 the letter keys "abcd" (or any name typed directly). They are assigned and evaluated in a dictionary used to represent the calculator's namespace.

21.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 subclassesExample 21-14 customizes the simple calculator's constructor to add extra widgets.

Example 21-14. PP3E\Lang\Calculator\calc0ext.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 classExample 21-15 attaches the simple calculator's widget package, along with extras, to a common parent.

Example 21-15. PP3E\Lang\Calculator\calc0emb.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 21-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 21-9. The calc0 script's object attached and extended


21.7.2. PyCalcA Real Calculator GUI

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 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 requires a bit more work: expressions must be evaluated manually and in parts, instead of calling the eval function only once. But the end result is much more useful and intuitive.

Lesson 5: Reusability Is Power

Though simple, attaching and subclassing the calculator graphically, as shown in Figure 21-9, illustrates the power of Python as a tool for writing reusable software. By coding programs with modules and classes, components written in isolation almost automatically become general-purpose tools. Python's program organization features promote reusable code.

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 handcoded parsers shared a scanner, the calculator GUI uses the guitools module we discussed earlier, and the next section will reuse the GuiMixin class. Sometimes we're able to finish part of a job before we start.


This section presents the implementation of PyCalca Python/Tkinter program that implements such a traditional calculator GUI. It touches on the subject of text and languages in two ways: it parses and evaluates expressions, and it implements a kind of stack-based language to perform the evaluation. Although its evaluation logic is more complex than the simpler calculator shown earlier, it demonstrates advanced programming techniques and serves as an interesting finale for this chapter.

21.7.2.1. Running PyCalc

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 the file calculator.py listed shortly (e.g., click it in a file explorer). Figure 21-10 shows PyCalc's 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 into the GUI class's constructor method. Of course, that means gray-on-gray in this book, so you'll have to run PyCalc yourself to see what I mean.

Figure 21-10. PyCalc calculator at work on Windows


If you do run this, you'll notice that PyCalc implements a normal calculator modelexpressions 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 methods from GuiMixin (presented in Chapter 11), a new "cmd" button that pops up nonmodal dialogs for entry of arbitrary Python code, and a recent calculations history pop up. Figure 21-11 captures some of PyCalc's pop-up windows.

Figure 21-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 the + 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 codepress 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 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 runtime. Variable names can be assigned and referenced in the main window with the letter, =, and "eval" keys; they are assigned in the calculator's 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.

21.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 window's display field at the top. It doesn't 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. In a sense, the calculator implements a little stack-based language, to evaluate the expressions being entered. 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. Because it records states and performs transitions, some might use the term state machine to describe this calculator language implementation.

Here's the general scenario:

  1. 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.

  2. 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).

  3. 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 stdout each time the Evaluator class is about to apply an operator and reduce (pop) the stacks. Run PyCalc with a console window to see the traces. 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 <eval>" [result = 19] (['*'], ['5', '3'])    [on '+' press: displays "15"] (['+'], ['15', '4'])   [on 'eval' press: displays "19"] 

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; we push 3 onto the operands stack, reduce the * subexpression (5 * 3), push its result onto operands, push + onto operators, and continue scanning user inputs. When "eval" is pressed at the end, 4 is pushed onto operators, and the final + on operators is applied to stacked operands.

The text input field and expression stacks are integrated by the calculator class. In general, the text entry field always holds the prior operand when an operator button is pressed; the text in the entry field is pushed onto the operands stack before the operator is resolved. Because of this, we have to pop results before displaying them after "eval" or ) is pressed (otherwise the results are pushed onto the stack twicethey would be both on the stack and in the display field, from which they would be immediately pushed again when the next operator is input). When an operator is seen (or "eval" or ) is applied), we also have to take care to erase the entry field when the next operand's entry is started.

Expression stacks also defer operations of lower precedence as the input is scanned. In the next trace, the pending + isn't evaluated when the * button is pressed: since * binds tighter, we need to postpone the + until the * can be evaluated. The * operator isn't popped until its right operand 4 has been seen. There are two operators to pop and apply to operand stack entries on the "eval" pressthe * at the top of operators is applied to the 3 and 4 at the top of operands, and then + is run on 5 and the 12 pushed for *:

 2) "5 + 3 * 4 <eval>" [result = 17] (['+', '*'], ['5', '3', '4'])   [on 'eval' press] (['+'], ['5', '12'])            [displays "17"] 

For strings of same-precedence operators such as the following, 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). For + and * operations this is irrelevant because order doesn't matter:

 3) "5 + 3 + 4 <eval>" [result = 12] (['+'], ['5', '3'])     [on the second '+'] (['+'], ['8', '4'])     [on 'eval'] 

The following trace is more complex. In this case, all the operators and operands are stacked (postponed) until we press the ) button at the end. To make parentheses work, ( is given a higher precedence than any operator and is pushed onto the operators stack to seal off lower stack reductions until the ) is seen. When the ) button is pressed, the parenthesized subexpression is popped and evaluated ((3 * 4), then (1 + 12)), and 13 is displayed in the entry field. On pressing "eval," the rest is evaluated ((3 * 13), (1 +39)), and the final result (40) is shown. This result in the entry field itself becomes the left operand of a future operator.

 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 fact, any temporary result can be used again: if we keep pressing an operator button without typing new operands, it's reapplied to the result of the prior pressthe value in the entry field is pushed twice and applied to itself each time. Press * many times after entering 2 to see how this works (e.g., 2***). On the first *, it pushes 2 and the *. On the next *, it pushes 2 again from the entry field, pops and evaluates the stacked (2 * 2), pushes back and displays the result, and pushes the new *. And on each following *, it pushes the currently displayed result and evaluates again, computing successive squares.

Figure 21-12 shows how the two stacks look at their highest level while scanning the expression in the prior example trace. On each reduction, the top operator is applied to the top two operands and the result is pushed back for the operator below. Because of the way the two stacks are used, the effect is similar to converting the expression to a string of the form +1*3(+1*34 and evaluating it right to left. In other cases, though, parts of the expression are evaluated and displayed as temporary results along the way, so it's not simply a string conversion process.

Figure 21-12. Evaluation stacks: 1 + 3 * (1 + 3 * 4)


Finally, the next example's string triggers an error. PyCalc is casual about error handling. Many errors are made impossible by the algorithm itself, but things such as unmatched parentheses still trip up the evaluator. 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, numeric errors, undefined name errors, syntax errors, and so on.

Operands and temporary results are always stacked as strings, and each operator is 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* essentially 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:

 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*']) 

Try tracing through these and other examples in the calculator's code to get a feel for the stack-based evaluation that occurs. Once you understand the general shift/reduce (push/pop) mechanism, expression evaluation is straightforward.

21.7.2.3. PyCalc source code

Example 21-16 contains the PyCalc source module that puts these ideas to work in the context of a GUI. It's a single-file implementation (not counting utilities imported and reused). Study the source for more details; as usual, there's no substitute for interacting with the program on your own to get a better feel for its functionality.

Also see the opening comment's "to do" list for suggested areas for improvement. Like all software systems, this calculator is prone to evolve over time (and in fact it has, with each new edition of this book). Since it is written in Python, such future mutations will be easy to apply.

Example 21-16. PP3E\Lang\Calculator\calculator.py

 #!/usr/local/bin/python ############################################################################## # PyCalc 3.0: a Python/Tkinter calculator program and GUI component. # evaluates expressions as they are entered, catches keyboard keys for # expression entry; 2.0 added integrated command-line popups, a recent # calculations history display popup, fonts and colors configuration, # help and about popups, preimported math/random constants, and more; # # 3.0 changes (PP3E): # -use 'readonly' entry state, not 'disabled', else field is greyed #  out (fix for 2.3 Tkinter change); # -avoid extended display precision for floats by using str( ), instead #  of 'x'/repr( ) (fix for Python change); # -apply font to input field to make it larger; # -use justify=right for input field so it displays on right, not left; # -add 'E+' and 'E-' buttons (and 'E' keypress) for float exponents; # -remove 'L' button (but still allow 'L' keypress): superfluous now, #  because Python auto converts up if too big ('L' forced this in past); # -use smaller font size overall; # -use windows.py module to get a window icon; # -auto scroll to the end in the history window # # to do: add a commas-insertion mode, allow '**' as a operator key, allow # '+' and 'J' inputs for complex numbers, use new decimal type for fixed # precision floats; as is, can use 'cmd' popup windows to input and eval # an initial complex, complex exprs, and 'E' '-' key sequences, but can't # be input via main window; caveat: this calulator's precision, accuracy, # and some of its behaviour, is currently bound by result of str( ) call; # # note that the new nested scopes simplify some lambdas here, but we have to # use defaults to pass in some scope values in lambdas here anyhow, because # enclosing scope names are looked up when the nested function is called, not # when it is created (but defaults are);  when the generated function is # called enclosing scope refs are whatever they were set to last in the # enclosing function's block, not what they were when the lambda ran; ############################################################################## from Tkinter  import *                                       # widgets, consts from PP3E.Gui.Tools.guimixin import GuiMixin                 # quit method from PP3E.Dbase.TableBrowser.guitools import *               # widget builders Fg, Bg, Font = 'black', 'skyblue', ('courier', 14, 'bold')   # default config debugme = 1 def trace(*args):     if debugme: print args ############################################################################## # the main class - handles user interface; # an extended Frame, on new Toplevel, orembedded 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 3.0')          # title iff owns window             self.master.iconname("PyCalc")           # ditto for key bindings             self.master.bind('<KeyPress>', self.onKeyboard)             self.entry.config(state='readonly')      # 3.0: not 'disabled'=grey         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         self.entry.config(font=font)                 # 3.0: make display larger         self.entry.config(justify=RIGHT)             # 3.0: on right, not left         for row in self.Operands:             frm = frame(self, TOP)             for char in row:                 button(frm, LEFT, char,                             lambda op=char: self.onOperand(op),                             fg=fg, bg=bg, font=font)         frm = frame(self, TOP)         for char in self.Operators:             button(frm, LEFT, char,                         lambda op=char: self.onOperator(op),                         fg=bg, bg=fg, font=font)         frm = frame(self, TOP)         button(frm, LEFT, 'dot ', lambda: self.onOperand('.'))         button(frm, LEFT, ' E+ ', lambda: self.text.set(self.text.get( )+'E+'))         button(frm, LEFT, ' E- ', lambda: self.text.set(self.text.get( )+'E-'))         button(frm, LEFT, 'cmd ', self.onMakeCmdline)         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 opnd|result         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: self.onCmdline(var, ent))         onReturn = (lambda event: self.onCmdline(var, ent))         button(frm, RIGHT, 'Run', onButton).pack(expand=NO)         ent.bind('<Return>', onReturn)         var.set(self.text.get( ))     def onCmdline(self, var, ent):            # eval cmdline pop-up 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 pop-up 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 'LlEe':                         self.text.set(self.text.get( )+pressed) # 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         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.see(END)                                     # 3.0: scroll to end         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.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 3.0\n'                                'A Python/Tk calculator\n'                                'Programming Python 3E\n'                                'June, 2005\n'                                '(2.0 1999, 1.0 1996)\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 top|last opnd         self.opnd[-1:] = []                     # to display and shift next         return value                            # or x.pop( ), or del x[-1]     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:                                                  # 3.0: not 'x'             result = str(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):         return '\n'.join(self.hist) def getCalcArgs( ):     from sys import argv                   # get cmdline args in a dict     config = {}                            # ex: -bg black -fg red     for arg in argv[1:]:                   # font not yet supported         if arg in ['-bg', '-fg']:          # -bg red' -> {'bg':'red'}             try:                 config[arg[1:]] = argv[argv.index(arg) + 1]             except:                 pass     return config if _ _name_ _ == '_ _main_ _':     CalcGui(**getCalcArgs()).mainloop( )    # in default toplevel window 

21.7.2.4. Using PyCalc as a component

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 attachments. The module in Example 21-17 demonstrates one way to reuse PyCalc's CalcGui class by extending and embedding, similar to what was done for the simple calculator earlier.

Example 21-17. PP3E\Lang\Calculator\calculator_test.py

 ########################################################################## # test calculator use as an extended and embedded GUI component; ########################################################################## from Tkinter import * from calculator import CalcGui from PP3E.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 21-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.

Figure 21-13. The calculator_test script: attaching and extending


These two windows on the right reuse the core PyCalc code running in the window on the left. All of these windows 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 it's 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 they don't overwrite settings made in other windows. Similarly, each calculator has its own evaluation stack manager object, such that calculations in one window don't appear in or impact other windows at all.

The two extensions in this script are artificial, of coursethey simply add labels at the top and bottom of the windowbut the concept is widely applicable. You could reuse the calculator's class by attaching it to any GUI that needs a calculator and customize it with subclasses arbitrarily. It's a reusable widget.

21.7.2.5. Adding new buttons in new components

One obvious way to reuse the calculator is to add additional expression feature buttonssquare 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 21-18 adds a few extra buttons to PyCalc by embedding (i.e., attaching) it in a container.

Example 21-18. PP3E\Lang\Calculator\calculator_plus_emb.py

 ######################################################################## # 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 PP3E.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 = 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 expr=expr: self.onExtra(expr)) )         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)     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 21-19.

Example 21-19. PP3E\Lang\Calculator\calculator_plus_ext.py

 ############################################################################## # 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 PP3E.Dbase.TableBrowser.guitools import * class CalcGuiPlus(CalcGui):     def makeWidgets(self, *args):         label(self, TOP, 'PyCalc Plus - Subclass')         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 expr=expr: self.onExtra(expr)) )         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_ _':     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 operate on the current value in the entry field, immediately. That's 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 makethey use PyCalc as a component, both from the outside and from below.

Finally, to test both of the extended calculator classes, as well as PyCalc configuration options, the script in Example 21-20 puts up four distinct calculator windows (this is the script run by PyDemos).

Example 21-20. PP3E\Lang\Calculator\calculator_plusplus.py

 #!/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 21-14 shows the resultfour independent calculators in top-level windows within the same process. The two windows on the top 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.

Figure 21-14. calculator_plusplus: extend, embed, and configure


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 5. 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 tangible but important aspect of Python programming. A common remark among new users is that it's easy to "say what you mean" in Python without getting bogged down in complex syntax or obscure rules. It's a programmer-friendly language. In fact, it's not too uncommon for Python programs to run on the first attempt.

As we've seen in this book, there are a number of factors behind this distinctionlack of declarations, no compile steps, simple syntax, useful built-in objects, and so on. Python is specifically designed to optimize speed of development (an idea we'll expand on in Chapter 24). For many users, the end result is a remarkably expressive and responsive language, which can actually be fun to use.

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 upfront design is warranted. For more demanding tasks, Python has modular constructs and fosters systems that can be extended in either Python or C. And a simple calculator GUI may not be what some would call "serious" software development. But maybe that's part of the point too.





Programming Python
Programming Python
ISBN: 0596009259
EAN: 2147483647
Year: 2004
Pages: 270
Authors: Mark Lutz

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net