CS 1321X - Lecture 28 - November 25, 2003

CS 1321X - Lecture 28

More Python Please


I.  Not quite lists

There's a related data structure that's sort of like a list, but isn't.
It's called a tuple, and you create them pretty much the same way
that you create lists, except you use parentheses instead of square 
brackets.

>>> x = [0, 1, 2, 3]  # list
>>> y = (0, 1, 2, 3)  # tuple

>>> x
[0, 1, 2, 3]

>>> y
(0, 1, 2, 3)

>>> x[1]   # indexing works the same
1
>>> y[1]
1

>>> x[1] = 72  # I can change a list
>>> x
[0, 72, 2, 3]


>>> y[1] = 72  # I can't change a tuple
Traceback (most recent call last):
  File "input", line 1, in ?
TypeError: object doesn't support item assignment
>>> 

So tuples are very static.  You can't overwrite elements, you can't
append or concatenate with tuples...you can't do anything but 
create them and then look at them.  If you have lots of information
that doesn't need to change, then it's a more efficient way of storing
that info than with lists.


II.  Fun with scoping

Python is lexically scoped, just like Scheme.  Variables that are 
created within a function definition are accessible within the
execution of that function, but are no longer accessible when
that function has ceased execution.  Here's some test code to
type into our editor window and run:

test1 = [10, 20, 30, 40, 50]  # the function will work
                              # on both lists and tuples
test2 = (10, 20, 30, 40, 50)

def average (sequence):
    count = 0
    total = 0
    for i in sequence:
        print i
        count = count + 1
        total = total + i
    avg = total/count
    print avg
    return avg

print "into loop"

average (test1)

average (test2)

print "out of loop"

print avg


Here's what shows up in the output window:

into loop
10
20
30
40
50
30
10
20
30
40
50
30
out of loop

And here's the error message we get in yet another window:

NameError: name 'avg' is not defined

...bla bla bla...


III.  Parameter Passing

However, just like Scheme, parameter passing in Python is pass-by-reference 
or call-by-reference.  So if you pass a mutable object like a list as a 
parameter to some function, and that function does something "destructive" 
on that object, then the results will be felt outside the function as well.  
For example, here's a little function that adds the numbers passed through 
two lists and puts the results back in the first of those lists:

test1 = [10, 20, 30, 40, 50]

test3 = [40, 50, 60, 70, 80]

def addlists (list1, list2):
    for i in range (0, len(list1)):
        list1[i] = list1[i] + list2[i]
        print list1[i]


print test1
print test3
print "adding lists"
addlists (test1, test3)
print "done adding"
print test1
print test3

And here's the output:

[10, 20, 30, 40, 50]
[40, 50, 60, 70, 80]
adding lists
50
70
90
110
130
done adding
[50, 70, 90, 110, 130]
[40, 50, 60, 70, 80]

If you don't want to do that, here's a way to create a third list
on the fly, but the appending might be expensive:

def addlists (list1, list2):
    list3 = []
    for i in range (0, len(list1)):
        list3.append (list1[i] + list2[i])
    print "here's the result"
    print list3
    return list3

And here's another way, that makes a separate copy of the first
list, not just another pointer to the first list (which isn't what you want):

def addlists (list1, list2):
    list3 = list1[:]
    for i in range (0, len(list1)):
        list3[i] = (list1[i] + list2[i])
    print "here's the result"
    print list3
    return list3


IV.  Welcome to Object-Oriented Programming

"Object-oriented programming" is one of the hot computing buzzwords (or 
buzzphrases) of recent history, right up there with the "Web" and 
"e-commerce". For advocates of the object-oriented programming (OOP) paradigm, 
there's a promise of clarity of design because of the ability to make a more 
direct map of the software solution to the real-world problem to be modeled. 
There's also the promise of greater programmer productivity because OOP 
encourages the reuse of existing code, whether that code comes from 
pre-existing libraries or is newly written for the project at hand. So OOP 
offers, at its best, faster and cheaper development and, maybe more 
importantly, maintenance.

At the same time, the OOP style of thinking may not be all that easy to 
learn, especially if you've spent a lot of time thinking procedurally or 
imperatively, as many of you already have. OOP encourages you to design your 
programs around objects (we'll discuss what an object is in awhile...for now 
think of it as a specific instance of an abstract data type) instead of 
procedures. Designing those objects well, so that they might be reused 
elsewhere, can be even more challenging. But hey, designing anything well is 
challenging, so let's jump in.

Remember that we talked briefly about different programming paradigms way 
back when. We told you that the procedural or imperative paradigm focussed on 
mapping the problem being solved onto a model of the computer itself. So 
instead of thinking about the problem in terms appropriate to the problem, 
you have to think about it in terms appropriate to the computer. That 
translation makes more work for you, and more complicated programs are the 
result. An alternative is to employ the functional paradigm, but that
too has its drawbacks. This paradigm asks you to think about your problem in 
ways similar to the ways in which you think about (or don't?) math functions. 
This frees you from the underlying machine model, which should simplify things 
for you, but it still requires a translation from the given problem domain to 
the functional domain...that's good for some problems, and not so good for 
others.

The OOP paradigm provides tools for the programmer to represent elements in 
the problem (or perhaps more appropriately, its solution) in terms that are 
much more appropriate to the problem itself.  The representation is general 
enough that the programmer is not constrained to any particular type of 
solution.

In OOP-land, the elements in the problem space (i.e., the real world) and 
their representation in the solution space (i.e., your program) are called 
"objects". Your program can be adapted to the language of the problem by 
adding new types of objects, so when you read the code describing the 
solution, you're reading words that also express the problem. (You may also 
need additional objects that don't have problem-space equivalents, but don't 
worry about that for now.)

So OOP should be a more flexible and powerful language abstraction than 
you've had before...but you'll have to form your own opinions through lots of 
practice. This course won't give you enough of that kind of practice...all we 
can hope to do is introduce you to the basics of OOP at the highest levels so 
that you won't drown when they throw you into the deep end of the swimming 
pool, so to speak, in CS 1322. (And for those of you who are contemplating 
1322X, bring a life raft.)

In short, OOP allows you to describe the problem in terms of the problem 
instead of in terms of some constraints on the solution as imposed by the 
computer. But there's still a connection back to the computer: each object 
behaves like a little computer, each object has a state, and each object has 
operations you can ask it to perform.

According to Bruce Eckel, who wrote a really good Java book and who sort of 
quotes Alan Kay, one of the godfathers of OOP, there are five characteristics 
of the OOP language Smalltalk, the first successful OOP language, and these 
characteristics apply to OOP languages in general (or should apply) and to 
Python in particular:

1.  Everything is an object. Think of an object as a fancy variable; it 
    stores data, but you can also ask it to perform operations on itself by 
    making requests. In theory, you can take any conceptual component in the 
    problem you're trying to solve (dogs, buildings, services, etc.) and 
    represent it as an object in the program. [Note: the hard part here is 
    that you have to be able to determine what the "conceptual components" 
    are in your problem, and which ones are important, and how to represent 
    them...that's not necessarily easy. But the thinking process involved 
    could lead to a really great solution.]

2.  A program is a bunch of objects telling each other what to do by sending 
    messages. To make a request of an object, you 'send a message' to that 
    object. More concretely, you can think of a message as a request to call 
    a function that belongs to a particular object.

3.  Each object has its own memory made up of other objects. Or, you make a 
    new kind of object by making a package containing existing objects. Thus, 
    you can build up complexity in a program while hiding it behind the 
    simplicity of objects.

4.  Every object has a type. Using the parlance, each object is an instance 
    of a class, where 'class' is synonymous with 'type'. The most important 
    distinguishing characteristic of a class is 'what messages can you send 
    to it?'

5.  All objects of a particular type can receive the same messages.... Because 
    an object of type circle is also an object of type shape, a circle is 
    guaranteed to receive shape messages. This means you can write code that 
    talks to shapes and automatically handle anything that fits the
    description of a shape. This substituitability is one of the most 
    powerful concepts in OOP.

Yet another way of looking at OO programming is this:  you're creating
solution elements that are nothing more than data structures and their
associated operations.  You're always asking "What information do I need
to keep?" and "What operations do I need to be able to perform on that
information?"  In a very real sense, you're programming by creating
abstract data types (or using existing ones) that are appropriate to
the problem at hand and then figuring out how the instances of those
ADTs communicate to get the job done.  The instances of those ADTs
are called objects.


V. Underlying principles

Another way of looking at OOP is through its underlying principles. There's a 
lot of different terminology that floats through OOP land, but there are just 
a few common-sense fundamentals that bring it all together. Those four 
principles are data abstraction, encapsulation, inheritance, and polymorphism. 
We'll discuss the first three principles today, and we'll let you learn about 
polymorphism in CS 1322.

You're already familiar with data abstraction. Data abstraction is the process 
of discarding unimportant details about your data structures and retaining 
only the stuff necessary to solve your problem, while at the same time putting 
some distance between those data structures and your higher-level algorithms 
for using those data structures. Together with the operations that work on the 
data structure, this all forms an abstract data type. You've been working with 
abstract data types and data abstraction for awhile, so there's nothing here 
that should make you crazy.

As for encapsulation, you've seen a little bit of that concept recently too. 
Encapsulation says that the operations to be performed on the data are just as 
important as the data, and (here's the real meat) there should be a way to 
associate the data and operations closely together and treat them as a 
single "encapsulated" unit of organization. So data and related procedures 
should be bundled together so that you can look at something and say "this is 
a foo and these are the only operations that can be performed on foos".

Most non-OOP languages support encapsulation well for built-in types (think 
of car and cdr for lists in Scheme), but not at all well for user-defined 
data types. We managed to pull off the encapsulation idea in Scheme (the Coke 
machine example) but we had to go down some not very intuitive roads to get 
there...in fact, we had to take advantage of some subtle stuff and write 
functions that generate other functions...yikes! Like I said earlier, 
wouldn't it be nice if there were a programming language that helped you 
do that same sort of thing in explicit ways?

Well, that's what happens in OOP languages. These languages let you bundle 
types and procedures that operate on those types while restricting access to 
the internal representation for all user-defined data types. It's built in to 
the language...it's a large part of why the language exists.

Why the importance on restricting access to the internal representation? 
That's a way of enforcing the integrity of a type by preventing programmers 
from accessing the data structure in inappropriate ways. That makes for 
better, more reliable programs. For example, say you implement some important 
part of a problem as a stack data type. You want stuff to be added and removed 
from the same end. But some other programmer on the project thinks it would be 
really handy if he could suck some stack element out of the middle of the 
stack at various times. So now your stack isn't working the way you expect 
it, and you can no longer count on its reliability. OOP languages give you 
the ability to say "Hey, this data type works this way, and you can't get at 
the internal representation. Go make your own @#$% data type." (As we'll see, 
Python doesn't currently restrict access to the degree that other OO languages 
do.)

As for inheritance, we'll talk about it at the end of this lecture.


VI. Some OOP terminology

Everybody talks about object-oriented this and object-oriented that, so you'd 
think that it's all about objects. It's not. It's really about an entity 
called a class. When you encapsulate a data type and its related operations 
and its access restrictions together, you've created a class. The individual 
variable or constant data items defined within a class are called fields or 
attributes. The operations defined within a class are called methods (they're 
just procedures). A class is a template for what specific instances of that 
class should look like. A specific instance of a class is given a name (by 
declaring a variable of that class type) and binding it to a fleshed-out copy 
of the class template. That instance is what we give the name "object" to (or 
sometimes the books just call it an "instance").

That last part is really important, so let me say it again. It's something 
that CS 1322 instructors and TAs keep telling me that students coming from 
CS 1321 keep getting wrong: an object is an instance of a class. From one 
class you may make countless objects that all have the same (or similar...more 
about that wrinkle later) behavior. When you write your program, you're 
actually writing code that describes classes, and you write code that 
describes how specific instances of those classes will be created, and then 
those instances or objects do the work. So in general, classes don't do 
anything except serve as templates for objects. That doesn't mean classes 
aren't important, because they are...they're what you as a programmer are 
creating when you're doing object-oriented programming. And if you get your 
classes wrong, the objects won't be right. But maybe they should have called 
it "class-oriented programming" instead of "object-oriented programming".

Maybe it would help to go back to our Scheme-based Coke machine example. The 
function "make-coke-machine" would take the role of a class: it serves as a 
template for specific instances of our vending machine. Calling 
"make-coke-machine" created a new instance of a vending machine, and we gave 
each of those instances separate names by binding the result of 
"make-coke-machine" to "cs-machine" and "ee-machine". In that sense, we 
created two unique instances of the abstract data type "coke machine"... each 
of those things could be called an object. Within the class template for 
"make-coke-machine" we had a variable called "number-of-cans", and that could 
be called a "field" in the OOP paradigm.  And finally, when we had some 
instructions in the class definition that either let us buy a Coke or reload 
the machine with more Cokes, we had things that you could call "methods".

Wasn't Scheme wonderful?


VII. I Have A Dream, revisited...

Remember back a few days (weeks?) when I said that what I really wanted to do 
was own a string of Coke machines? Well, I still have that dream, and we'll 
talk about simulating my vending machine empire in Python today. But first, 
as a little reminder, here's what my Coke machine template looked like in 
Scheme:

(define (make-coke-machine-v2)
  (let ((number-of-cans 0))
      (lambda (dowhat)
        (cond ((equal? dowhat 'load)
               (set! number-of-cans 20)
               (display number-of-cans)
               (display " frosty cans waiting for you") (newline))
              ((and (equal? dowhat 'buy) (> number-of-cans 0))
               (set! number-of-cans (- number-of-cans 1))
               (display "have a Coke") (newline)
               (display "cans remaining: ")
               (display number-of-cans)
               (newline))
              ((equal? dowhat 'buy)
               (display "sorry, out of Coke"))
              (else (print "please use correct change"))))))

What I want to do now is to approximate the same thing in Python. In other 
words, I'll want to make a class that is a template for a Coke machine.

So let's build that class:

class CokeMachine:
   def __init__(self):
      self.numcans = 20
      print "Adding another Coke machine to your empire"

The class "CokeMachine" currently consists of a single method (a function or 
procedure associated with the class CokeMachine) called __init__.  The 
__init__ method is special in that, when it's defined for a class, it's 
executed every time an instance of that class (i.e., an object) is created.  
In OO terminology, this kind of method is often called a constructor.
Every method you define needs a parameter list with at least one parameter, 
even if you don't intend to explicitly pass any parameters, and by convention 
that first parameter is called "self" (you could call it anything, but you'd 
be annoying all the other Python programmers).

In our case, the __init__ method creates an object variable or instance 
variable that's associated with the object/instance being created.  The 
variable is called numcans, and it's bound to 20.  Then the __init__ method 
then prints a message intended solely to feed my ego. It's not exciting, but 
I'm not looking for excitement in the land of vending machines. I just want 
quarters.

I also want a main program to drive the simulated operation of my Coke 
machines, and I'll make that a class as well.  I'll call it "SimCoke" (maybe 
I'll package it up and sell it on the shelf next to "The Sims" who drink 
SimCoke, of course).

class SimCoke:
   def __init__(self):
      print " "
      print "Here's the Coke Machine Simulator"
      cs = CokeMachine()
      ee = CokeMachine()
      print "The cs machine has %3d frosty cans of Coke waiting for you." \
            % (cs.numcans)

(I don't have to make SimCoke a class.  I could make it a function.  And
if I don't care if the main program has name, I could just write some
nameless code like I did in class for a little bit.  I'm just making
it a class because it's an OO thing to do.)

The only thing that's new here is this stuff:

   cs = CokeMachine()
   ee = CokeMachine()

What's that? Well, that's how you create objects from classes.  Once you've 
defined the class CokeMachine, invoking CokeMachine() creates a new instance 
of the class...an object.  In SimCoke, we create two different CokeMachine 
objects and give each a unique name.  Each instance has its own fields or 
methods or variables (in this case there's just one variable for each 
instance:  it's called cs.numcans for the cs machine and ee.numcans for the 
ee machine), and each instance has its own methods (again only one in this 
simple case: cs.__init__ and ee.__init__ respectively).

In more detail, that cs.numcans construct tells Python I want the value 
that's stored in numcans, but if there are lots of objects of type CokeMachine,
then there are lots of fields called numcans floating around. Which one do I 
want? I want the one associated with the object called cs. And I tell Python 
that much by composing the name of the object with the name of the field 
using that dot operator:

            cs.numcans

That wasn't so hard, was it?

Now we create a SimCoke object just to make stuff happen:

            SimCoke()

and then we run the whole thing and here's what comes out:

Here's the Coke Machine Simulator
Adding another Coke machine to your empire
Adding another Coke machine to your empire
The cs machine has  20 frosty cans of Coke waiting for you.
The ee machine has  20 frosty cans of Coke waiting for you.

As you'll see in the next lecture, there's a problem here
waiting to happen.  But you can wait to find out, can't you?



Copyright (c) 2003 by Kurt Eiselt.  All rights reserved, with 
the exception of stuff that belongs to somebody else.

Last revised: December 2, 2003