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