Vous êtes sur la page 1sur 19

Search

this site

belmontpython
Object-Oriented Programming, a unit
converter program, and The Model-View-
Controller organization

Contents

1 Object-Oriented Programming
2 A Units Converter
3 The Model-View-Controller organization

O b je c t - O r ie n t e d P r o g r a m m in g

The basic idea of Object-Oriented Programming, or OOP for short, is that a


computer program and the data on which it operates should be packaged as a
single entity called an Object. The blueprint for constructing an object is called a
Class; the constructed object is called an instance of the Class; and the code
that accesses the object is called a Method of the class. Together, the Class and
Method definitions can be regarded as a contract with a user: if you create a Class
and call one of its Methods exactly as stated, the Class will do exactly what you
requested.

Here's an example. Suppose you need a program to manage a fleet of taxis. In


non-OOP, sometimes called Procedural Programming, you would need a large
main program that maintained many lists, each with an entry for each cab; one list
would have cab licenses, others makes, models, years, mileages, fuel,
destination, drivers, ... The main program would continually maintain all of these
lists in real time. However, in OOP, each cab would be represented as an
instance of the Cab class, say, which would contain all of its own properties. Its
methods, like fuelremaining() or refuel(), could return that information, as well as
changing information such as location, destination, estimated arrival, and so on. It
would use exactly the same programming, but some operations, such as adding
or removing a cab, would be much easier, and because each Class object is
complete, or encapsulated, it can easily be copied and modified to handle
different objects like airplanes, or personnel.

This very brief introduction would immediately be pounced on and ripped apart by
any of the host of dedicated writers on the subject, and I suggest you consult
them, in particular Toby Donaldson's book referenced earlier.

So far, we've studied some simple and some complicated Python applications,
but without ever mentioning OOP. Is Python an OOP language? Absolutely! But
as with other aspects, Python hides the OOP machinery, to make programming as
simple as possible. Python Functions, identified by def() statements, are actually
the Methods of OOP terminology, written in an equivalent but simpler form. To
demonstrate this, we'll begin with a fairly simple application, called Converter,
without exposing its OOP structure, and then rewrite exactly the same application
in object-oriented language.

A U n it s C o n v e r t e r

Nowadays you can find unit converters-- pounds to kilograms, miles to kilometers,
Fahrenheit to Celsius, and the like-- on the Web. We'll create a version that might
be useful if you needed to convert something but didn't have access to the
internet. The Converter uses several Controls that we've already studied: an
Entry, into which the user enters a value in the old system of units, two
Comboboxes, one for selecting the old unit, the other for selecting an allowed
new unit-- you can't convert pounds to feet, for example-- and a Label to display
the converted value in the new unit. We also want to check the validity of the
typed-in value in order to avoid an error if Python tries to convert something
invalid to an actual number. We'll do this with a parsing function called
entrytype() that rejects invalid entries such as, say, 1.2x or 1.2.3, but also allows
entries in scientific notation like 5.2e+3. Entries have a slightly unexpected
property: the Entry.get() function returns the contents up to, but NOT including, the
most recently entered character, even though it is displayed on the screen, so to
check the entire current entry, we have to get the last character from a separate
property of the Entry.

The procedure is as usual for our GUI examples: the program creates the controls,
and then calls the root mainloop function to wait for a user action. When the user
types something, the associated event handler passes information to a process
function that carries out any indicated actions, including creating a list of allowed
unit conversions, checking the Entry for validity, converting the value if valid to a
real number, and displaying the result in the Label.
Download converter.py and run it. Note that when the old value is complete and
valid, the conversion may take place even before the return or enter key is
pressed. Try entering illegal values, which will cause the entry to turn red until the
error is corrected.
Here's the program:

# converter.py: a Python application to convert amounts


between different units.
# The user specifies the value and units for the
conversion. The program checks the input
# for validity, then displays the value in the new units.
# There are no explicitly declared classes.
# 03-May-17: Robin Verdier, for the Belmont Programming
Courses

import sys # for Python version


pv3 = sys.version_info[0] > 2 # True for Python version
3, else 2
if pv3:
import tkinter as tk
from tkinter import ttk # for the Combobox
else:
import Tkinter as tk
import ttk
root = tk.Tk()
root.title('Converter')

icunittype = 0
cboxindices = []
controls = []
# Create arrays of conversion data: the second, third,
... entries contain the data
# to allow them to be converted to and from the first
entry

Forceconversions = (('Newtons', 1.0, 0.), ('pounds-


force', 4.448222, 0.),
('dynes', 1.0e-5, 0.), ('kilogram-force', 9.80665,
0.))

Lengthconversions = (('cms', 1.0, 0.), ('meters', 1.0e2,


0.), ('kilometers', 1.0e5, 0.),
('inches', 2.54, 0.), ('feet', 30.48, 0.), ('yards',
91.44, 0.),
('miles', 1.609344e5, 0.))

Massconversions = (('grams', 1.0, 0.), ('kilograms',


1.0e3, 0.),
('ounces', 28.34952, 0.), ('pounds', 453.59237, 0.),
('tons', 9.07185e5, 0.))

Temperatureconversions = (('Celsius', 1.0, 0.),


('Fahrenheit', 100./180, -160./9),
('Kelvin', 1.0, -274.15))

conversiondata = (('Mass', Massconversions), ('Length',


Lengthconversions),
('Weight', Forceconversions), ('Force',
Forceconversions),
('Temperature', Temperatureconversions))

# Create a list of controls and indices to them


controldata = (('unit type', 'Combobox'), ('old value',
'Entry'),
('old unit', 'Combobox'), (' = ', 'Label'), ('new
value', 'Label'),
('new unit', 'Combobox'))

for ic in range(len(controldata)):
if controldata[ic][0] == 'unit type': icunittype = ic
if controldata[ic][0] == 'old value': icoldvalue = ic
if controldata[ic][0] == 'old unit': icoldunit = ic
if controldata[ic][0] == 'new value': icnewvalue = ic
if controldata[ic][0] == 'new unit': icnewunit = ic

def convert(unittype=0, oldvalue=1.0, oldunit=0,


newunit=0):
# convert the current entry value from old to the new
units
conv = conversiondata[unittype][1]
newvalue = (float(oldvalue) * conv[oldunit][1] +
conv[oldunit][2] \
- conv[newunit][2])/ conv[newunit][1]
newvaluetext = '{:.5}'.format(newvalue)
if newvalue <= 0.001 or newvalue >= 1000.:
newvaluetext = '{:.5e}'.format(newvalue)
controls[icnewvalue].configure(text=newvaluetext)
return newvalue
# end of convert function

def entrytype(st='', ch=''):


# Returns 1 if st represents a complete valid floating
number with no non-numeric
# entries, no invalid repeats (like 12.4.), and no
incomplete end (like 12.0e).
# Returns 0 if no illegal characters but not yet a
complete number, like 1.0e-.
# Returns -1 if there are illegal characters or
repeats, or if ch = returnchar
# and entry is incomplete.
returnchar = '\r' # the return or enter key
noend = '+-Ee' # Illegal to end with one of these
noreps = '.+-Ee' # Illegal to repeat one of these
validchars = '0123456789' + returnchar + noreps # all
valid numeric characters
type = 0
if len(st) == 0: type = -1 # blank entry will fail
attempted conversion
if type == 0:
ic = 0
for c in st:
valid = c in validchars # valid = True if character
c is present in validchars
if not valid:
type = -1
break
if type == 0 and ic > 0:
norep = c in noreps and c in st[:ic] # True if c
is in both Lists
# list[n:m] is a 'slice' of list, a sublist with
all the elements from
# index n to index m. list[:m] has all the
elements from 0 to m.
if norep:
type = -1
break
ic = ic + 1 # This could also be written ic+=1, but
ic++ which is allowed in
# many other languages is NOT allowed
in Python.
if type == 0:
if st[-1] not in noend:
# st[-1] is the last element, [-2] the one
preceding it, ...
type = 1
else:
if ch == returnchar:
type = -1
return type
# end of entrytype function

def onkeypress(event):
# Event handler for entry key pressed passes source and
key to process
st = event.widget.get()
ch = event.char
process(icoldvalue, st, ch)

def onselection(event):
# Event handler for Combobox selected, calls
Converter.process with the control index
# Set up combobox lists if unit type changes
for controlindex in range(len(controls)):
if event.widget == controls[controlindex]: break
unittypeindex = icunittype
if controlindex == unittypeindex:
setupcomboboxes(1)
process(controlindex)
# end of onselection function

def process(controlindex=0, entrystring='', lastchar=''):


# Handle control event
# If event was key pressed, test the value and if
invalid, make the entry red and
# ignore it
if controlindex == icoldvalue:
col = 'light blue'
flag = entrytype(entrystring, lastchar)
if flag < 0:
col = 'red'
controls[icoldvalue].configure(bg=col)
# return without converting if entry is invalid or
incomplete
# configure (or config) sets properties of a control,
here the background color;
# fg would set the foreground, or text, color.
if flag <= 0:
return
# Get current settings of the controls
unittype = controls[icunittype].current()
oldunit = controls[icoldunit].current()
oldvalue = controls[icoldvalue].get()
newunit = controls[icnewunit].current()
conv = conversiondata[unittype][1]
# Do the conversion
newvalue = convert(unittype, oldvalue, oldunit,
newunit)
# end of process function

def setupcomboboxes(mode):
# Fill combobox lists
global controls
unittypeindex = icunittype
if mode == 0:
# set up unittype list.
units = []
#print 'conversiondata:', conversiondata
for item in conversiondata:
units.append(item[0])
#print 'item', item, ', units len =', len(units)
controls[unittypeindex]['values'] = units
controls[unittypeindex].current(0) # set the
current item selection
unittype = controls[unittypeindex].current()
convs = conversiondata[unittype][1]
units = []
for u in convs:
units.append(u[0])
cur = 0
for cindex in (icoldunit, icnewunit):
controls[cindex]['values'] = units
controls[cindex].current(cur)
cur += 1
# end of setupcomboboxes function

# Create control widgets


for c in controldata:
if c[1] == 'Combobox':
control = ttk.Combobox(root, height = 6, width = 14)
control.bind ('<<ComboboxSelected>>', onselection)
if c[1] == 'Entry':
control = tk.Entry(root, width=8, bd=3,
bg='lightblue')
control.insert (0, '1.0')
control.bind('<Key>', onkeypress)
if c[1] == 'Label':
w = 12
col = 'lightblue'
t = ' '
if c[0] != 'new value':
w = 2
col = 'white'
t = '='
control = tk.Label(root, width=w, bd=3, text=t,
bg=col)
controls.append(control)
control.pack(side = 'left') # Add the control to the
layout

setupcomboboxes(0) # 0 sets up unit types

# Set initial values


process()

# Run
root.mainloop()

# End of converter application

Exercises:
-- Add a new conversion, say from acres to square feet, modifying the 'conv' tables
accordingly.
-- Add time units: hours, minutes, seconds, days, years.
-- A difficult one: add currency conversions, such as dollars to euros. Why is this
difficult?

T h e M o d e l- V ie w - C o n t r o lle r o r g a n iz a t io n

We'll now study the converter rewritten in object-oriented language. At the same
time, we'll recast it into the Model-View-Controller (MVC) organization, not
changing the basic program but expressing it in the structure underlying most
contemporary object-oriented programs that create viewports or windows for a
Graphical User Interface (GUI). This organization separates the program into
three parts: a Model, a View, and a Controller. The Controller sets up timers and
actions to be performed when the user clicks a mouse button on a control, or
moves or resizes the viewport, that is, all the user interactions. But it shouldn't
need any information about what's actually being done. The View part handles
all the complicated details of painting, moving, and resizing the viewport; we need
only tell it what we want to display-- but not what we want to use it for. Together,
they constitute the GUI. The Model part, on the other hand, is what distinguishes
a particular application from others; it performs calculations and tells the View
what to draw, without needing any information about how the parameters
specifying the model's action were generated.

A major advantage of the MVC organization is that the Controller can often be
reused with many different applications with minimal changes to the Controller;
only the Model needs to be changed.

Note that MVC organization is not rigidly defined, and it can, but does not need to,
be used for programs written in any OOP programming language.

Download converterOM.py and run it. It will behave like the


previous version-- because they're the same program, just
organized in different ways, and they're both Object-Oriented.
(Actually, the new version has slightly improved error handling).
To distinguish the two versions, we'll call the original one non-MVC
and the reorganized one MVC. Also, the functions of the non-MVC
version will be called by the standard OOP name methods in the
MVC version, although, except for the syntactic changes, they are
functionally identical.

The full listing follows this discussion. There are several things to
note about it. Most obvious is the rearrangement of the function
definitions: the statement
class Control
begins the definition of the Control class, consisting of all the
methods at the subsequent level of indentation. They are
def __init__(self): creates the Entry, Comboboxes, and Label widgets,
binds them to the key actions that call them, sets default values for parameters,
and creates the layout. Note that the two underscores are required by Python;
def onkeypress(self, event): handles key presses in the Entry.
def onselection(self, event): handles changes in the Combobox
selections.

Each method has an added first argument called self, which identifies the class
that issued the callback. The initializer method __init__(self) is called by the
system before any other actions, which ensures that the program will create any
required controls and give them reasonable initial values. These methods are
called constructors in standard OOP terminology. Also, every reference to
objects and methods must define the class instance to which they belong. Thus,
the non-MVC statement
entry.bind('<Key>', onkeypress)
is replaced in the MVC version by
self.entry.bind('<Key>', self.onkeypress)
And the non-MVC statement
cbox[0]['values'] = units
is replaced by
self.cbox[0]['values'] = cv.units
where cv is defined below as a pointer to the instance of the Convert class.

The Model component is defined as a class we've called Convert. As with


Control, all statements at the same level of indentation following the definition
class Convert
are part of the Convert class, whose methods are the same as in the non-MVC
version, but with class identifiers when required, as well as the initializer
def __init__(self): creates lists of valid units and conversions;
def convert (self, oldvalue, oldunit, newunit, conv):
returns the converted value if the entry
information is valid and complete;
def entrytype(self, st='', ch=''): returns a value indicating
whether the current entry is
(1) valid and complete, (0) valid but incomplete, or (-1) invalid;
def process(self, source, char): tests the entry and if valid and
complete, converts the
value to the new unit, or, if invalid, turns the Entry background red.

After the class definitions, the statements


cv = Convert()
ct = Control()
create instances of the Convert and Control classes and give them
the short names cv and ct.

Here's the whole MVC version:

# converterOM.py: a Python application to convert amounts


between different units.
# The user specifies the value and units for the
conversion. The program checks the input
# for validity, then displays the value in the new units.
# This is an example of OOP programming with MVC
organization.
# There are several classes: Controller, Converter, and
subclasses of Converter for unit
# types Mass, Weight, Distance, Time, Temperature, ...
# 07-Apr-17: Robin Verdier, for the Belmont Programming
Courses

import sys # for Python version


pv3 = sys.version_info[0] > 2 # True for Python version
3, else 2
if pv3:
import tkinter as tk
from tkinter import ttk # for the Combobox
else:
import Tkinter as tk
import ttk
root = tk.Tk()
root.title('Converter')

class Controller:

# The Controller class is the MVC Controller component


that deals with the
# control widgets and the user interactions with them.

def __init__(self): # the initializer


# Create a list of controls from information in the
converter class
self.controls = []
self.cboxindices = []
for c in cv.controldata:
if c[1] == 'Combobox':
control = ttk.Combobox(root, height = 6, width =
14)
control.bind ('<<ComboboxSelected>>',
self.onselection)
if c[1] == 'Entry':
control = tk.Entry(root, width=8, bd=3,
bg='lightblue')
control.insert (0, '1.0')
control.bind('<Key>', self.onkeypress)
if c[1] == 'Label':
w = 12
col = 'lightblue'
t = ' '
if c[0] != 'new value':
w = 2
col = 'white'
t = '='
control = tk.Label(root, width=w, bd=3, text=t,
bg=col)
self.controls.append(control)
control.pack(side = 'left') # Add the control to
the layout
self.setupcomboboxes(0) # 0 sets up unit type
# end of __init__ function

def onkeypress(self, event):


# Event handler for entry key pressed passes source
and key to process
st = event.widget.get()
ch = event.char
cv.process(cv.icoldvalue, st, ch)

def onselection(self, event):


# Event handler for Combobox selected, calls
Converter.process with the control index
# Set up combobox lists if unit type changes
for controlindex in range(len(self.controls)):
if event.widget == self.controls[controlindex]:
break
unittypeindex = cv.icunittype
if controlindex == unittypeindex:
self.setupcomboboxes(1)
cv.process(controlindex)
# end of onselection function

def setupcomboboxes(self, mode):


# Fill combobox lists
unittypeindex = cv.icunittype
if mode == 0:
# set up unittype list.
units = []
for item in cv.conversiondata:
units.append(item[0])
self.controls[unittypeindex]['values'] = units
self.controls[unittypeindex].current(0) # set the
current item selection
unittype = self.controls[unittypeindex].current()
convs = cv.conversiondata[unittype][1].conversions
units = []
for u in convs:
units.append(u[0])
cur = 0
for cindex in (cv.icoldunit, cv.icnewunit):
self.controls[cindex]['values'] = units
self.controls[cindex].current(cur)
cur += 1
# end of setupcomboboxes function

# End of Controller class

class Converter:

# The Converter class is the Model component that carries


out the actions
# that are specific to the Converter application

def __init__(self): # the initializer


# Initializer: instantiate the unit classes
self.conversiondata = (('Mass', self.Mass()),
('Length', self.Length()),
('Weight', self.Force()), ('Force', self.Force()),
('Temperature', self.Temperature()))
self.controldata = (('unit type', 'Combobox'), ('old
value', 'Entry'),
('old unit', 'Combobox'), (' = ', 'Label'), ('new
value', 'Label'),
('new unit', 'Combobox'))
for ic in range(len(self.controldata)):
if self.controldata[ic][0] == 'unit type':
self.icunittype = ic
if self.controldata[ic][0] == 'old value':
self.icoldvalue = ic
if self.controldata[ic][0] == 'old unit':
self.icoldunit = ic
if self.controldata[ic][0] == 'new value':
self.icnewvalue = ic
if self.controldata[ic][0] == 'new unit':
self.icnewunit = ic
# end of converter __init__ function

def convert(self, unittype=0, oldvalue=1.0, oldunit=0,


newunit=0):
# convert the current entry value from old to the new
units
conv = self.conversiondata[unittype][1].conversions
newvalue = (float(oldvalue) * conv[oldunit][1] +
conv[oldunit][2] \
- conv[newunit][2])/ conv[newunit][1]
newvaluetext = '{:.5}'.format(newvalue)
if newvalue <= 0.001 or newvalue >= 1000.:
newvaluetext = '{:.5e}'.format(newvalue)

ct.controls[self.icnewvalue].configure(text=newvaluetext)
return newvalue

# end of convert function

def entrytype(self, st='', ch=''):


# Returns 1 if st represents a complete valid
floating number with no non-numeric
# entries, no invalid repeats (like 12.4.), and no
incomplete end (like 12.0e).
# Returns 0 if no illegal characters but not yet a
complete number, like 1.0e-.
# Returns -1 if there are illegal characters or
repeats, or if ch = returnchar
# and entry is incomplete.
returnchar = '\r' # the return or enter key
noend = '+-Ee' # Illegal to end with one of these
noreps = '.+-Ee' # Illegal to repeat one of these
validchars = '0123456789' + returnchar + noreps # all
valid numeric characters
type = 0
if len(st) == 0: type = -1 # blank entry will fail
attempted conversion
if type == 0:
ic = 0
for c in st:
valid = c in validchars # valid = True if
character c is present in validchars
if not valid:
type = -1
break
if type == 0 and ic > 0:
norep = c in noreps and c in st[:ic] # True if
c is in both Lists
# list[n:m] is a 'slice' of list, a sublist
with all the elements from
# index n to index m. list[:m] has all the
elements from 0 to m.
if norep:
type = -1
break
ic = ic + 1 # This could also be written ic+=1,
but ic++ which is allowed in
# many other languages is NOT allowed
in Python.
if type == 0:
if st[-1] not in noend:
# st[-1] is the last element, [-2] the one
preceding it, ...
type = 1
else:
if ch == returnchar:
type = -1
return type
# end of entrytype function

def process(self, controlindex=0, entrystring='',


lastchar=''):
# Handle control event
# If event was key pressed, test the value and if
invalid, make the entry red and
# ignore it
if controlindex == self.icoldvalue:
col = 'light blue'
flag = self.entrytype(entrystring, lastchar)
if flag < 0:
col = 'red'
ct.controls[self.icoldvalue].configure(bg=col)
# return without converting if entry is invalid or
incomplete
# configure (or config) sets properties of a
control, here the background color;
# fg would set the foreground, or text, color.
if flag <= 0:
return
# Get current settings of the controls
unittype = ct.controls[self.icunittype].current()
oldunit = ct.controls[self.icoldunit].current()
oldvalue = ct.controls[self.icoldvalue].get()
newunit = ct.controls[self.icnewunit].current()
conv = self.conversiondata[unittype][1].conversions
# Do the conversion
newvalue = self.convert(unittype, oldvalue, oldunit,
newunit)
# end of process function
# End of Converter class

class Force: # these subclasses do not need


initializers
conversions = (('Newtons', 1.0, 0.), ('pounds-force',
4.448222, 0.),
('dynes', 1.0e-5, 0.), ('kilogram-force', 9.80665,
0.))

class Length:
conversions = (('cms', 1.0, 0.), ('meters', 1.0e2,
0.), ('kilometers', 1.0e5, 0.),
('inches', 2.54, 0.), ('feet', 30.48, 0.), ('yards',
91.44, 0.),
('miles', 1.609344e5, 0.))

class Mass:
conversions = (('grams', 1.0, 0.), ('kilograms',
1.0e3, 0.),
('ounces', 28.34952, 0.), ('pounds', 453.59237, 0.),
('tons', 9.07185e5, 0.))

class Temperature:
conversions = (('Celsius', 1.0, 0.), ('Fahrenheit',
100./180, -160./9),
('Kelvin', 1.0, -274.15))

# End of Model component classes

# Create class instances


cv = Converter()
ct = Controller()
cv.process()

# Run
root.mainloop()

# End of converter application

Note that the classes Mass, Length, Force, ... are actually subclasses of the
Converter class, and that they don't have explicit initializers; their function here is
like that of structs in other OOP languages.

Time permitting, it's interesting to compare this Python OOP version with a
functionally identical Java program which also exposes its OOP construction and
uses the MVC organization. Download Converter.java to your desktop, run it (if
you have Java installed, which most Macs do; for Windows, you'll have to install it
yourself) by opening a new terminal window (Terminal > Shell > New Window)
and executing the instructions
javac Converter.java
java Converter
and note that it operates nearly identically. Now open both converterOM.py and
Converter.java in text editor windows and compare the way the two languages do
the same thing, in particular the initializer in Python vs. the Constructor in Java,
and the way the Comboboxes, Entry, and Labels are declared and connected to
triggering events.

Questions: should the subclasses have more functionality? For example, should
each of them be able to convert between items in its list, which is now done by the
convert function? Wouldn't this increase the amount of code?
Can you find any "magic cookies"-- unidentified constants in the code? When is it
difficult to avoid them?

Now we'll study some increasingly complex graphics applications.

Previous: Text Files Next: Drawing Random Shapes


The Belmont Python Computer Programming Course programs
Site map

Sign in | Recent Site Activity | Report Abus e | Print Page | P owered By Google Sites

Vous aimerez peut-être aussi