PyTree: A Generic Tree Object Viewer

PyTree A Generic Tree Object Viewer

Up to now, this chapter has been command-line-oriented. To wrap up, I want to show you a program that merges the GUI technology we studied earlier in the book with some of the data structure ideas we've met in this chapter.

This program is called PyTree, a generic tree data structure viewer written in Python with the Tkinter GUI library. PyTree sketches out the nodes of a tree on screen as boxes connected by arrows. It also knows how to route mouseclicks on drawn tree nodes back to the tree, to trigger tree-specific actions. Because PyTree lets you visualize the structure of the tree generated by a set of parameters, it's a fun way explore tree-based algorithms.

PyTree supports arbitrary tree types by "wrapping" real trees in interface objects. The interface objects implement a standard protocol by communicating with the underlying tree object. For the purposes of this chapter, PyTree is instrumented to display binary search trees ; for the next chapter, it's also set up to render expression parse trees. New trees can be viewed by coding wrapper classes to interface to new tree types.

The GUI interfaces PyTree utilizes were covered in depth earlier in this book, so I won't go over this code in much detail here. See Part II, for background details, and be sure to run this program on your own computer to get a better feel for its operation. Because it is written with Python and Tkinter, it should be portable to Windows, Unix, and Macs.

17.10.1 Running PyTree

Before we get to the code, let's see what PyTree looks like. You can launch PyTree from the PyDemos launcher bar (see the top-level of the examples distribution source tree at http://examples.oreilly.com/python2), or by directly running the treeview.py file listed in Example 17-27. Figure 17-2 shows PyTree in action displaying the binary tree created by the "test1" button. Trees are sketched as labels embedded in a canvas, and connected by lines with arrows. The lines reflect parent-to-child relationships in the actual tree; PyTree attempts to layout the tree to produce a more or less uniform display like this one.

Figure 17-2. PyTree viewing a binary search tree (test1)

figs/ppy2_1702.gif

PyTree's window consists of a canvas with vertical and horizontal scrolls, and a set of controls at the bottom -- radiobuttons for picking the type of tree you wish to display, a set of buttons that trigger canned tree drawing tests, and an input field for typing text to specify and generate a new tree. The set of test buttons changes if you pick the Parser radiobutton (you get one less test button); PyTree use widget pack_forget and pack methods to hide and show tree-specific buttons on the fly.

When you pick one of the canned test buttons, it displays in the input field the string you would type to generate the tree drawn. For binary trees, type a list of values separated by spaces and press the "input" button or the Enter key to generate a new tree; the new tree is the result of inserting the typed values from left to right. For parse trees, you input an expression string in the input field instead (more on this later). Figure 17-3 shows the result of typing a set of values into the input field and submitting; the resulting binary tree shows up in the canvas.

Figure 17-3. A binary tree typed manually with on-click pop-up

figs/ppy2_1703.gif

Notice the pop-up in this screen shot; left-clicking on a displayed tree node with your mouse runs whatever action a tree wrapper class defines, and displays its result in the pop-up. Binary trees have no action to run, so we get a default message in the pop-up, but parse tress use the mouseclick to evaluate the subtree rooted at the clicked node (again, more on parse trees later).

Just for fun, maximize this window and press the "test4" button -- it inserts 100 numbers from through 99 into a new binary tree at random, and displays the result. Figure 17-4 captures one portion of this tree; it's much too large to fit on one screen (or on one book page), but you can move around the tree with the canvas scrollbars.

Figure 17-4. PyTree viewing a large binary search tree (test4)

figs/ppy2_1704.gif

PyTree uses an algorithm to connect all parents to their children in this tree without crossing their connecting lines. It does some up-front analysis to try and arrange descendents at each level to be as close to their parents as possible. This analysis step also yields the overall size of a new tree -- PyTree uses it to reset the scrollable area size of the canvas for each tree drawn.

17.10.2 Pytree Source Code

Let's move on to the code. Similar to PyForm in the prior chapter, PyTree is coded as two modules; here, one module handles the task of sketching trees in the GUI, and another implements wrappers to interface to various tree types and extends the GUI with extra widgets.

17.10.2.1 Tree-independent GUI implementation

The module in Example 17-26 does the work of drawing trees in a canvas. It's coded to be independent of any particular tree structure -- its TreeViewer class delegates to its TreeWrapper class when it needs tree-specific information for the drawing (e.g., node label text, node child links). TreeWrapper in turn expects to be subclassed for a specific kind of tree; in fact it raises assertion errors if you try to use it without subclassing. In design terms, TreeViewer embeds a TreeWrapper; it's almost as easy to code TreeViewer subclasses per tree type, but that limits a viewer GUI to one particular kind of tree (see treeview_subclasses.py at http://examples.oreilly.com/python2 for a subclassing-based alternative).

Trees are drawn in two steps -- a planning traversal the builds a layout data structure that links parents and children, and a drawing step that uses the generated plan to draw and link node labels on the canvas. The two-step approach simplifies some of the logic required to layout trees uniformly. Study this listing for more details.

Example 17-26. PP2EDstructTreeView reeview_wrappers.py

#########################################################################
# PyTree: sketch arbitrary tree data structures in a scrolled canvas;
# this version uses tree wrapper classes embedded in the viewer gui 
# to support arbitrary trees (i.e.. composition, not viewer subclassing);
# also adds tree node label click callbacks--run tree specific actions;
# see treeview_subclasses.py for subclass-based alternative structure;
# subclassing limits one tree viewer to one tree type, wrappers do not;
# see treeview_left.py for an alternative way to draw the tree object;
# see and run treeview.py for binary and parse tree wrapper test cases;
#########################################################################

from Tkinter import *
from tkMessageBox import showinfo

Width, Height = 350, 350 # start canvas size (reset per tree)
Rowsz = 100 # pixels per tree row
Colsz = 100 # pixels per tree col

###################################
# interface to tree object's nodes
###################################

class TreeWrapper: # subclass for a tree type
 def children(self, treenode):
 assert 0, 'children method must be specialized for tree type'
 def label(self, treenode):
 assert 0, 'label method must be specialized for tree type'
 def value(self, treenode):
 return ''
 def onClick(self, treenode): # node label click callback
 return ''
 def onInputLine(self, line, viewer): # input line sent callback
 pass

###########$######################
# tree view gui, tree independent
##################################

class TreeViewer(Frame):
 def __init__(self, wrapper, parent=None, tree=None, bg='brown', fg='beige'):
 Frame.__init__(self, parent)
 self.pack(expand=YES, fill=BOTH)
 self.makeWidgets(bg) # build gui: scrolled canvas
 self.master.title('PyTree 1.0') # assume I'm run standalone
 self.wrapper = wrapper # embed a TreeWrapper object
 self.fg = fg # setTreeType changes wrapper 
 if tree:
 self.drawTree(tree)

 def makeWidgets(self, bg):
 self.title = Label(self, text='PyTree 1.0')
 self.canvas = Canvas(self, bg=bg, borderwidth=0)
 vbar = Scrollbar(self)
 hbar = Scrollbar(self, orient='horizontal')

 self.title.pack(side=TOP, fill=X)
 vbar.pack(side=RIGHT, fill=Y) # pack canvas after bars
 hbar.pack(side=BOTTOM, fill=X)
 self.canvas.pack(side=TOP, fill=BOTH, expand=YES)

 vbar.config(command=self.canvas.yview) # call on scroll move
 hbar.config(command=self.canvas.xview)
 self.canvas.config(yscrollcommand=vbar.set) # call on canvas move
 self.canvas.config(xscrollcommand=hbar.set)
 self.canvas.config(height=Height, width=Width) # viewable area size

 def clearTree(self):
 mylabel = 'PyTree 1.0 - ' + self.wrapper.__class__.__name__
 self.title.config(text=mylabel)
 self.unbind_all('')
 self.canvas.delete('all') # clear events, drawing

 def drawTree(self, tree):
 self.clearTree( )
 wrapper = self.wrapper
 levels, maxrow = self.planLevels(tree, wrapper)
 self.canvas.config(scrollregion=( # scrollable area
 0, 0, (Colsz * maxrow), (Rowsz * len(levels)) )) # upleft, lowright
 self.drawLevels(levels, maxrow, wrapper)

 def planLevels(self, root, wrap):
 levels = []
 maxrow = 0 # traverse tree to 
 currlevel = [(root, None)] # layout rows, cols
 while currlevel:
 levels.append(currlevel)
 size = len(currlevel)
 if size > maxrow: maxrow = size
 nextlevel = []
 for (node, parent) in currlevel:
 if node != None:
 children = wrap.children(node) # list of nodes
 if not children:
 nextlevel.append((None, None)) # leave a hole
 else:
 for child in children:
 nextlevel.append((child, node)) # parent link
 currlevel = nextlevel
 return levels, maxrow

 def drawLevels(self, levels, maxrow, wrap):
 rowpos = 0 # draw tree per plan
 for level in levels: # set click handlers
 colinc = (maxrow * Colsz) / (len(level) + 1) # levels is treenodes
 colpos = 0
 for (node, parent) in level:
 colpos = colpos + colinc
 if node != None:
 text = wrap.label(node)
 more = wrap.value(node)
 if more: text = text + '=' + more
 win = Label(self.canvas, text=text, 
 bg=self.fg, bd=3, relief=RAISED)
 win.pack( )
 win.bind('', 
 lambda e, n=node, handler=self.onClick: handler(e, n))
 self.canvas.create_window(colpos, rowpos, anchor=NW, 
 window=win, width=Colsz*.5, height=Rowsz*.5)
 if parent != None:
 self.canvas.create_line(
 parent.__colpos + Colsz*.25, # from x-y, to x-y
 parent.__rowpos + Rowsz*.5,
 colpos + Colsz*.25, rowpos, arrow='last', width=1)
 node.__rowpos = rowpos
 node.__colpos = colpos # mark node, private attrs
 rowpos = rowpos + Rowsz

 def onClick(self, event, node):
 label = event.widget
 wrap = self.wrapper
 text = 'Label = ' + wrap.label(node) # on label click
 value = wrap.value(node)
 if value: 
 text = text + '
Value = ' + value # add tree text if any
 result = wrap.onClick(node) # run tree action if any
 if result:
 text = text + '
' + result # add action result
 showinfo('PyTree', text) # popup std dialog

 def onInputLine(self, line): # feed text to tree wrapper
 self.wrapper.onInputLine(line, self) # ex: parse and redraw tree

 def setTreeType(self, newTreeWrapper): # change tree type drawn
 if self.wrapper != newTreeWrapper: # effective on next draw
 self.wrapper = newTreeWrapper
 self.clearTree( ) # else old node, new wrapper

17.10.2.2 Tree wrappers and test widgets

The other half of PyTree consists of a module that defines TreeWrapper subclasses that interface to binary and parser trees, implements canned test case buttons, and adds the control widgets to the bottom of the PyTree window.[4] These control widgets were split off into this separate module (in Example 17-27) on purpose, because the PyTree canvas might be useful as a viewer component in other GUI applications.

[4] If you're looking for a coding exercise, try adding another wrapper class and radiobutton to view the KeyedBinaryTree we wrote earlier in this chapter. You'll probably want to display the key in the GUI, and pop up the associated value on clicks.

Example 17-27. PP2EDstructTreeView reeview.py

# PyTree launcher script
# wrappers for viewing tree types in the book, plus test cases/gui

import string
from Tkinter import *
from treeview_wrappers import TreeWrapper, TreeViewer
from PP2E.Dstruct.Classics import btree
from PP2E.Lang.Parser import parser2

###################################################################
# binary tree wrapper
###################################################################

class BinaryTreeWrapper(TreeWrapper): # embed binary tree in viewer
 def children(self, node): # adds viewer protocols
 try: # to interface with tree
 return [node.left, node.right]
 except: 
 return None
 def label(self, node):
 try: 
 return str(node.data)
 except: 
 return str(node)
 def onInputLine(self, line, viewer): # on test entry at bottom
 items = string.split(line) # make tree from text input
 t = btree.BinaryTree( ) # draw resulting btree
 for x in items: t.insert(x) # no onClick handler here
 viewer.drawTree(t.tree)

###################################################################
# binary tree extension
###################################################################

class BinaryTree(btree.BinaryTree):
 def __init__(self, viewer): # embed viewer in tree
 btree.BinaryTree.__init__(self) # but viewer has a wrapper
 self.viewer = viewer
 def view(self):
 self.viewer.drawTree(self.tree)

###################################################################
# parse tree wrapper
###################################################################

class ParseTreeWrapper(TreeWrapper):
 def __init__(self): # embed parse tree in viewer
 self.dict = {} # adds viewer protocols
 def children(self, node):
 try:
 return [node.left, node.right]
 except:
 try:
 return [node.var, node.val]
 except: 
 return None
 def label(self, node):
 for attr in ['label', 'num', 'name']:
 if hasattr(node, attr):
 return str(getattr(node, attr))
 return 'set'
 def onClick(self, node): # on tree label click
 try: # tree-specific action
 result = node.apply(self.dict) # evaluate subtree
 return 'Value = ' + str(result) # show result in popup
 except:
 return 'Value = '
 def onInputLine(self, line, viewer): # on input line
 p = parser2.Parser( ) # parse expr text
 p.lex.newtext(line) # draw resulting tree
 t = p.analyse( )
 if t: viewer.drawTree(t)

###################################################################
# canned test cases (or type new nodelists/exprs in input field)
###################################################################

def shownodes(sequence):
 sequence = map(str, sequence) # convert nodes to strings
 entry.delete(0, END) # show nodes in text field
 entry.insert(0, string.join(sequence, ' '))

def test1_binary( ): # tree type is binary wrapper
 nodes = [3, 1, 9, 2, 7] # make a binary tree
 tree = BinaryTree(viewer) # embed viewer in tree
 for i in nodes: tree.insert(i) 
 shownodes(nodes) # show nodes in input field
 tree.view( ) # sketch tree via embedded viewer

def test2_binary( ):
 nodes = 'badce'
 tree = btree.BinaryTree( ) # embed wrapper in viewer
 for c in nodes: tree.insert(c) # make a binary tree
 shownodes(nodes)
 viewer.drawTree(tree.tree) # ask viewer to draw it

def test3_binary( ):
 nodes = 'abcde'
 tree = BinaryTree(viewer)
 for c in nodes: tree.insert(c)
 shownodes(nodes)
 tree.view( )

def test4_binary( ):
 tree = BinaryTree(viewer)
 import random # make a big binary tree
 nodes = range(100) # insert 100 nodes at random
 order = [] # and sketch in viewer
 while nodes:
 item = random.choice(nodes)
 nodes.remove(item)
 tree.insert(item)
 order.append(item)
 shownodes(order)
 tree.view( )

def test_parser(expr):
 parser = parser2.Parser( ) # tree type is parser wrapper
 parser.lex.newtext(expr) # subtrees evaluate when clicked
 tree = parser.analyse( ) # input line parses new expr
 entry.delete(0, END) # vars set in wrapper dictionary
 entry.insert(0, expr) # see lang/text chapter for parser
 if tree: viewer.drawTree(tree)

def test1_parser( ): test_parser("1 + 3 * (2 * 3 + 4)") 
def test2_parser( ): test_parser("set temp 1 + 3 * 2 * 3 + 4")
def test3_parser( ): test_parser("set result temp + ((1 + 3) * 2) * (3 + 4)")

###################################################################
# build viewer with extra widgets to test tree types
###################################################################

if __name__ == '__main__':
 root = Tk( ) # build a single viewer gui
 bwrapper = BinaryTreeWrapper( ) # add extras: input line, test btns
 pwrapper = ParseTreeWrapper( ) # make wrapper objects
 viewer = TreeViewer(bwrapper, root) # start out in binary mode

 def onRadio( ):
 if var.get( ) == 'btree':
 viewer.setTreeType(bwrapper) # change viewer's wrapper
 for btn in p_btns: btn.pack_forget( ) # erase parser test buttons
 for btn in b_btns: btn.pack(side=LEFT) # unhide binary buttons
 elif var.get( ) == 'ptree':
 viewer.setTreeType(pwrapper)
 for btn in b_btns: btn.pack_forget( )
 for btn in p_btns: btn.pack(side=LEFT)

 var = StringVar( )
 var.set('btree')
 Radiobutton(root, text='Binary', command=onRadio, 
 variable=var, value='btree').pack(side=LEFT)
 Radiobutton(root, text='Parser', command=onRadio, 
 variable=var, value='ptree').pack(side=LEFT)
 b_btns = []
 b_btns.append(Button(root, text='test1', command=test1_binary))
 b_btns.append(Button(root, text='test2', command=test2_binary))
 b_btns.append(Button(root, text='test3', command=test3_binary))
 b_btns.append(Button(root, text='test4', command=test4_binary))
 p_btns = []
 p_btns.append(Button(root, text='test1', command=test1_parser))
 p_btns.append(Button(root, text='test2', command=test2_parser))
 p_btns.append(Button(root, text='test3', command=test3_parser))
 onRadio( )

 def onInputLine( ):
 line = entry.get( ) # use per current tree wrapper type
 viewer.onInputLine(line) # type a node list or expression

 Button(root, text='input', command=onInputLine).pack(side=RIGHT)
 entry = Entry(root)
 entry.pack(side=RIGHT, expand=YES, fill=X)
 entry.bind('', lambda event: onInputLine( )) # button or enter key
 root.mainloop( ) # start up the gui

17.10.3 Pytree Does Parse Trees, Too

Finally, I want to show you what happens when you click the Parser radiobutton in the PyTree window. The GUI changes over to an expression parse tree viewer, by simply using a different tree wrapper class: the label at the top changes, the test buttons change, and input is now entered as an arithmetic expression to be parsed and sketched. Figure 17-5 shows a tree generated for the expression string displayed in the input field.

Figure 17-5. PyTree viewing an expression parse tree

figs/ppy2_1705.gif

PyTree is designed to be generic -- it displays both binary and parse trees, but is easy to extend for new tree types with new wrapper classes. On the GUI, you can switch between binary and parser tree types at any time by clicking the radiobuttons. Input typed into the input field is always evaluated according to the current tree type. When the viewer is in parse tree mode, clicking on a node in the tree evaluates the part of the expression represented by the parse tree rooted at the node you clicked. Figure 17-6 shows the pop-up you get when you click the root node of the tree displayed.

Figure 17-6. PyTree pop-up after clicking a parse tree node

figs/ppy2_1706.gif

When viewing parse trees, PyTree becomes a sort of visual calculator -- you can generate arbitrary expression trees and evaluate any part of them by clicking on nodes displayed. But at this point, there is not much more I can tell you about these kinds of trees until you move on to Chapter 18.

Introducing Python

Part I: System Interfaces

System Tools

Parallel System Tools

Larger System Examples I

Larger System Examples II

Part II: GUI Programming

Graphical User Interfaces

A Tkinter Tour, Part 1

A Tkinter Tour, Part 2

Larger GUI Examples

Part III: Internet Scripting

Network Scripting

Client-Side Scripting

Server-Side Scripting

Larger Web Site Examples I

Larger Web Site Examples II

Advanced Internet Topics

Part IV: Assorted Topics

Databases and Persistence

Data Structures

Text and Language

Part V: Integration

Extending Python

Embedding Python

VI: The End

Conclusion Python and the Development Cycle



Programming Python
Python Programming for the Absolute Beginner, 3rd Edition
ISBN: 1435455002
EAN: 2147483647
Year: 2000
Pages: 245

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