It’s time for a
status update. We now have encapsulated in the form of
classes customizable implementations of our records and their processing
logic. Making our class-based records persistent is a minor last step.
We could store them in per-record pickle files again; a shelve-based
storage medium will do just as well for our goals and is often easier to
code.
Example 1-18
shows
how.
Example 1-18. PP4E\Preview\make_db_classes.py
import shelve
from person import Person
from manager import Manager
bob = Person('Bob Smith', 42, 30000, 'software')
sue = Person('Sue Jones', 45, 40000, 'hardware')
tom = Manager('Tom Doe', 50, 50000)
db = shelve.open('class-shelve')
db['bob'] = bob
db['sue'] = sue
db['tom'] = tom
db.close()
This file creates three class instances (two from the original
class and one from its customization) and assigns them to keys in a
newly created shelve file to store them permanently. In other words, it
creates a shelve of class instances; to our code, the database looks
just like a dictionary of class instances, but the top-level dictionary
is mapped to a shelve file again. To check our work,
Example 1-19
reads the shelve and
prints fields of its records.
Example 1-19. PP4E\Preview\dump_db_classes.py
import shelve
db = shelve.open('class-shelve')
for key in db:
print(key, '=>\n ', db[key].name, db[key].pay)
bob = db['bob']
print(bob.lastName())
print(db['tom'].lastName())
Note that we don’t need to reimport thePerson
class here in order to fetch its
instances from the shelve or run their methods. When instances are
shelved or pickled, the underlying pickling system records both instance
attributes and enough information to locate their classes automatically
when they are later fetched (the class’s module simply has to be on the
module search path when an instance is loaded). This is on purpose;
because the class and its instances in the shelve are stored separately,
you can change the class to modify the way stored instances are
interpreted when loaded (more on this later in the book). Here is the
shelve dump script’s output just after creating the shelve with the
maker script:
bob =>
Bob Smith 30000
sue =>
Sue Jones 40000
tom =>
Tom Doe 50000
Smith
Doe
As shown in
Example 1-20
,
database updates are as simple as before (compare this to
Example 1-13
), but dictionary keys
become attributes of instance objects, and updates are implemented by
class method calls instead of hardcoded logic. Notice how we still
fetch, update, and reassign to keys to update the shelve.
Example 1-20. PP4E\Preview\update_db_classes.py
import shelve
db = shelve.open('class-shelve')
sue = db['sue']
sue.giveRaise(.25)
db['sue'] = sue
tom = db['tom']
tom.giveRaise(.20)
db['tom'] = tom
db.close()
And last but not least, here is the dump script again after
running the update script; Tom and Sue have new pay values, because
these objects are now persistent in the shelve. We could also open and
inspect the shelve by typing code at Python’s interactive command line;
despite its longevity, the shelve is just a Python object containing
Python objects.
bob =>
Bob Smith 30000
sue =>
Sue Jones
50000.0
tom =>
Tom Doe
65000.0
Smith
Doe
Tom and Sue both get a raise this time around, because they are
persistent objects in the shelve database. Although shelves can also
store simpler object types such as lists and dictionaries, class
instances allow us to combine both data and behavior for our stored
items. In a sense, instance attributes and class methods take the place
of records and processing programs in more traditional
schemes.
At this point, we
have a full-fledged database system: our classes
simultaneously implement record data and record processing, and they
encapsulate the implementation of the behavior. And the Pythonpickle
andshelve
modules provide simple ways to store
our database persistently between program executions. This is not a
relational database (we store objects, not tables, and queries take the
form of Python object processing code), but it is sufficient for many
kinds of programs.
If we need more functionality, we could migrate this application
to even more powerful tools. For example, should we ever need full-blown
SQL query support, there are interfaces that allow Python scripts to
communicate with relational databases such as MySQL, PostgreSQL, and
Oracle in portable ways.
ORMs (object relational mappers)
such as
SQLObject and SqlAlchemy
offer another approach which retains the Python class
view, but translates it to and from relational database tables—in a
sense providing the best of both worlds, with Python class syntax on
top, and enterprise-level databases underneath.
Moreover, the open source ZODB system
provides a more comprehensive object database for Python,
with support for features missing in shelves, including concurrent
updates, transaction commits and rollbacks, automatic updates on
in-memory component changes, and more. We’ll explore these more advanced
third-party tools in
Chapter 17
. For
now, let’s move on to putting a good face on our
system.
“Buses Considered Harmful”
Over the years, Python has been remarkably well supported by the
volunteer efforts of both countless individuals and formal
organizations. Today, the nonprofit Python Software Foundation
(PSF)
oversees Python conferences and other noncommercial
activities. The PSF was preceded by the PSA, a group that was
originally formed in response to an early thread on the Python
newsgroup that posed the semiserious question: “What would happen if
Guido was hit by a bus?”
These days, Python creator Guido
van Rossum is still the ultimate arbiter of proposed
Python changes. He was officially anointed the BDFL
—Benevolent Dictator for Life—of Python at the first
Python conference and still makes final yes and no decisions on
language changes (and apart from 3.0’s deliberate incompatibilities,
has usually said no: a good thing in the programming languages domain,
because Python tends to change slowly and in backward-compatible
ways).
But Python’s user base helps support the language, work on
extensions, fix bugs, and so on. It is a true community project. In
fact, Python development is now a completely open process—anyone can
inspect the latest source code files or submit patches by visiting a
website (see
http://www.python.org
for
details).
As an open source package, Python development is really in the
hands of a very large cast of developers working in concert around the
world—so much so that if the BDFL ever does pass the torch, Python
will almost certainly continue to enjoy the kind of support its users
have come to expect. Though not without pitfalls of their own, open
source projects by nature tend to reflect the needs of their user
communities more than either individuals or shareholders.
Given Python’s popularity, bus attacks seem less threatening now
than they once did. Of course, I can’t speak for Guido.
So far, our database
program consists of class instances stored in a shelve file,
as coded in the preceding section. It’s sufficient as a storage medium,
but it requires us to run scripts from the command line or type code
interactively in order to view or process its content. Improving on this
is straightforward: simply code more general programs that interact with
users, either from a console window or from a full-blown graphical
interface
.
Let’s start
with something simple. The most basic kind of interface we
can code would allow users to type keys and values in a console window
in order to process the database (instead of writing Python program
code).
Example 1-21
, for
instance, implements a simple interactive loop that allows a user to
query multiple record objects in the shelve by key.
Example 1-21. PP4E\Preview\peopleinteract_query.py
# interactive queries
import shelve
fieldnames = ('name', 'age', 'job', 'pay')
maxfield = max(len(f) for f in fieldnames)
db = shelve.open('class-shelve')
while True:
key = input('\nKey? => ') # key or empty line, exc at eof
if not key: break
try:
record = db[key] # fetch by key, show in console
except:
print('No such key "%s"!' % key)
else:
for field in fieldnames:
print(field.ljust(maxfield), '=>', getattr(record, field))
This script uses thegetattr
built-in function to fetch an object’s attribute when given its name
string, and theljust
left-justify
method of strings to align outputs (maxfield
, derived from a generator expression,
is the length of the longest field name). When run, this script goes
into a loop, inputting keys from the interactive user (technically, from
the standard input stream, which is usually a console window) and
displaying the fetched records field by field. An empty line ends the
session. If our shelve of class instances is still in the state we left
it near the end of the last section:
...\PP4E\Preview>dump_db_classes.py
bob =>
Bob Smith 30000
sue =>
Sue Jones 50000.0
tom =>
Tom Doe 65000.0
Smith
Doe
We can then use our new script to query the object database
interactively, by key:
...\PP4E\Preview>peopleinteract_query.py
Key? =>sue
name => Sue Jones
age => 45
job => hardware
pay => 50000.0
Key? =>nobody
No such key "nobody"!
Key? =>
Example 1-22
goes further
and allows interactive updates. For an input key, it inputs values for
each field and either updates an existing record or creates a new object
and stores it under the key.
Example 1-22. PP4E\Preview\peopleinteract_update.py
# interactive updates
import shelve
from person import Person
fieldnames = ('name', 'age', 'job', 'pay')
db = shelve.open('class-shelve')
while True:
key = input('\nKey? => ')
if not key: break
if key in db:
record = db[key] # update existing record
else: # or make/store new rec
record = Person(name='?', age='?') # eval: quote strings
for field in fieldnames:
currval = getattr(record, field)
newtext = input('\t[%s]=%s\n\t\tnew?=>' % (field, currval))
if newtext:
setattr(record, field, eval(newtext))
db[key] = record
db.close()
Notice the use ofeval
in this
script to convert inputs (as usual, that allows any Python object type,
but it means you must quote string inputs explicitly) and the use ofsetattr
call to assign an attribute
given its name string. When run, this script allows any number of
records to be added and changed; to keep the current value of a record’s
field, press the Enter key when prompted for a new value:
Key? =>tom
[name]=Tom Doe
new?=>
[age]=50
new?=>56
[job]=None
new?=>'mgr'
[pay]=65000.0
new?=>90000
Key? =>nobody
[name]=?
new?=>'John Doh'
[age]=?
new?=>55
[job]=None
new?=>
[pay]=0
new?=>None
Key? =>
This script is still fairly simplistic (e.g., errors aren’t
handled), but using it is much easier than manually opening and
modifying the shelve at the Python interactive prompt, especially for
nonprogrammers. Run the query script to check your work after an update
(we could combine query and update into a single script if this becomes
too cumbersome, albeit at some cost in code and user-experience
complexity):
Key? =>tom
name => Tom Doe
age => 56
job => mgr
pay => 90000
Key? =>nobody
name => John Doh
age => 55
job => None
pay => None
Key? =>
The console-based
interface approach of the preceding section works, and it
may be sufficient for some users assuming that they are comfortable with
typing commands in a console window. With just a little extra work,
though, we can add a GUI that is more modern, easier to use, less error
prone, and arguably sexier.
As we’ll see later in this book, a variety of GUI toolkits and
builders are available for Python programmers: tkinter, wxPython, PyQt,
PythonCard, Dabo, and more. Of these, tkinter ships with Python, and it
is something of a de facto standard.
tkinter is a
lightweight toolkit and so meshes well with a scripting
language such as Python; it’s easy to do basic things with tkinter, and
it’s straightforward to do more advanced things with extensions and
OOP-based code. As an added bonus, tkinter GUIs are portable across
Windows, Linux/Unix, and Macintosh; simply copy the source code to the
machine on which you wish to use your GUI. tkinter doesn’t come with all
the bells and whistles of larger toolkits such as wxPython or PyQt, but
that’s a major factor behind its relative simplicity, and it makes it
ideal for getting started in the GUI domain.
Because tkinter is designed for scripting, coding GUIs with it is
straightforward. We’ll study all of its concepts and tools later in this
book. But as a first example, the first program in tkinter is just a few
lines of code, as shown in
Example 1-23
.
Example 1-23. PP4E\Preview\tkinter001.py
from tkinter import *
Label(text='Spam').pack()
mainloop()
From the tkinter module (really, a module package in Python 3), we
get screen device (a.k.a. “widget”) construction calls such asLabel
; geometry manager methods such aspack
; widget configuration presets
such as theTOP
andRIGHT
attachment side hints we’ll use later
forpack
; and themainloop
call, which starts event
processing.
This isn’t the most useful GUI ever coded, but it demonstrates
tkinter basics and it builds the fully functional window shown in
Figure 1-1
in just three simple lines of code.
Its window is shown here, like all GUIs in this book, running on Windows
7; it works the same on other platforms (e.g., Mac OS X, Linux, and
older versions of Windows), but renders in with native look and feel on
each.
Figure 1-1. tkinter001.py window
You can launch this example in IDLE, from a console command line,
or by clicking its icon—the same way you can run other Python scripts.
tkinter itself is a standard part of Python and works out-of-the-box on
Windows and others, though you may need extra configuration or install
steps on some computers (more details later in this book).
It’s not much more work to code a GUI that actually responds to a
user:
Example 1-24
implements a
GUI with a button that runs thereply
function each time it is pressed.
Example 1-24. PP4E\Preview\ tkinter101.py
from tkinter import *
from tkinter.messagebox import showinfo
def reply():
showinfo(title='popup', message='Button pressed!')
window = Tk()
button = Button(window, text='press', command=reply)
button.pack()
window.mainloop()
This example still isn’t very sophisticated—it creates an explicitTk
main window for the application to
serve as the parent container of the button, and it builds the simple
window shown in
Figure 1-2
(in
tkinter, containers are passed in as the first argument when making a
new widget; they default to the main window). But this time, each time
you click the “press” button, the program responds by running Python
code that pops up the dialog window in
Figure 1-3
.
Figure 1-2. tkinter101.py main window
Figure 1-3. tkinter101.py common dialog pop up
Notice that the pop-up dialog looks like it should for Windows 7,
the platform on which this screenshot was taken; again, tkinter gives us
a native look and feel that is appropriate for the machine on which it
is running. We can customize this GUI in many ways (e.g., by changing
colors and fonts, setting window titles and icons, using photos on
buttons instead of text), but part of the power of tkinter is that we
need to set only the options we are interested in
tailoring.
All of our GUI
examples so far have been top-level script code with a
function for handling events. In larger programs, it is often more
useful to code a GUI as a subclass of the tkinterFrame
widget—a
container for other widgets.
Example 1-25
shows our single-button
GUI recoded in this way as a class.
Example 1-25. PP4E\Preview\tkinter102.py
from tkinter import *
from tkinter.messagebox import showinfo
class MyGui(Frame):
def __init__(self, parent=None):
Frame.__init__(self, parent)
button = Button(self, text='press', command=self.reply)
button.pack()
def reply(self):
showinfo(title='popup', message='Button pressed!')
if __name__ == '__main__':
window = MyGui()
window.pack()
window.mainloop()
The button’s event handler is a
bound method
—self.reply
, an object that remembers bothself
andreply
when later called. This example
generates the same window and pop up as
Example 1-24
(Figures
1-2
and
1-3
); but because it is now a subclass ofFrame
, it automatically becomes an
attachable
component
—i.e., we can add all of the
widgets this class creates, as a package, to any other GUI, just by
attaching thisFrame
to the GUI.
Example 1-26
shows how.
Example 1-26. PP4E\Preview\attachgui.py
from tkinter import *
from tkinter102 import MyGui
# main app window
mainwin = Tk()
Label(mainwin, text=__name__).pack()
# popup window
popup = Toplevel()
Label(popup, text='Attach').pack(side=LEFT)
MyGui(popup).pack(side=RIGHT) # attach my frame
mainwin.mainloop()
This example attaches our one-button GUI to a larger window, here
aToplevel
pop-up window created by
the importing application and passed into the construction call as the
explicit parent (you will also get aTk
main window; as we’ll learn later, you
always do, whether it is made explicit in your code or not). Our
one-button widget package is attached to the right side of its container
this time. If you run this live, you’ll get the scene captured in
Figure 1-4
; the “press” button is our attached customFrame
.
Figure 1-4. Attaching GUIs
Moreover, becauseMyGui
is
coded as a class, the GUI can be customized by the usual inheritance
mechanism; simply define a subclass that replaces the parts that differ.
Thereply
method, for example, can be
customized this way to do something unique, as demonstrated in
Example 1-27
.
Example 1-27. PP4E\Preview\customizegui.py
from tkinter import mainloop
from tkinter.messagebox import showinfo
from tkinter102 import MyGui
class CustomGui(MyGui): # inherit init
def reply(self): # replace reply
showinfo(title='popup', message='Ouch!')
if __name__ == '__main__':
CustomGui().pack()
mainloop()
When run, this script creates the same main window and button as
the originalMyGui
class. But
pressing its button generates a different reply, as shown in
Figure 1-5
, because the
custom version of thereply
method runs.
Figure 1-5. Customizing GUIs
Although these are still small GUIs, they illustrate some fairly
large ideas. As we’ll see later in the book, using OOP like this for
inheritance and attachment allows us to reuse packages of widgets in
other programs—calculators, text editors, and the like can be customized
and added as components to other GUIs easily if they are classes. As
we’ll also find, subclasses of widget class can provide a common
appearance or standardized behavior for all their instances—similar in
spirit to what some observers might call GUI styles or themes. It’s a
normal byproduct of Python and
OOP.