|
|
CS 1110 - Introduction to Programming
Starting Out with Python
Chapter 10, Classes and Object-Oriented Programming, part 1
Objectives:
This lesson covers the first part of chapter 10, which
discusses more ways to
reuse program structures. Objectives important to this lesson:
- Procedural and object-oriented programming
- Classes
- Working with instances
Concepts:
Chapter 10Before we begin, the chapter may not be clear to you. I will try to make it clearer, as always. If you need something else, try this web page, on which another practitioner gives us his thoughts on the subject.
Procedural and Object-Oriented Programming
The chapter begins, once again, with a formal definition that is not very
enlightening. The
author wants to compare and contrast two programming methods,
procedural programming and object-oriented programming (OOP). We might
argue that all programs are procedural, in that they have code sections
that perform procedures: they do something. Modularized programming, as
we have seen it in the previous chapters, encourages us to create a function for each separate procedure
we can imagine in our programs, such as gathering input, processing the
data, and displaying output. The author presents the idea that
procedural programming makes a separation between data and program, but
this it is not made clear at this point that object-oriented programs
are different.
Moving into the object-oriented discussion, the author tells
us that OOP is concerned with objects. Objects can have built-in
methods, and can contain data attributes to store data. Still sounds
like the functions we made that received and returned data. There is
more vocabulary in this section as well.
- encapsulation - combining data and program code in objects
- data hiding - when an object hides the data it holds from code that is not its own code
- method access - allowing code outside an object to access the methods of that object
- object reusability - making it possible for other programs to use objects that we create
Having confused many readers, the text changes tactics to begin talking about an example. We are told that a software clock
would have data attributes for the current second, minute, and hour.
This is something we would do with variables holding values that are
constantly ready to change. If this is an alarm clock, there would be
one or more alarm_time attributes, and On_Off attributes for each one.
The discussion continues, listing a few methods the clock would need to
have: set_time, set_alarm_time, set_alarm_on, set_alarm_off. These
methods would interact with the data attributes. These methods would
give a user the ability to change values held by the attributes. The
text tells us that the methods are necessary because the data itself is private
to the clock object, and the user is not allowed to access or change
the data directly. These methods are available to the user or users,
making them public methods. (This is still not different from our programs. We have asked the user for choices, and the programs have used the choices.)
If we allow that the program object has private data, we should have no problem with the idea that it can have private methods,
methods that are used only by the object itself. Examples would include
any internal function of the clock. The text lists a few examples:
increment_current_second, increment_current_minute, and sound_alarm.
This completes the first section of the chapter.
Classes
A class is a template, a pattern, or a blueprint for
something. It is useful to create classes for complex objects so that
we can quickly make more instances of them. Think of a class as being a recipe.
Suppose we have a class for apple pie. The class tells us what to do to
make one, what its ingredients are, how to bake it, and what it is good for. Every time we follow
that recipe and bake an apple pie, we are creating a new instance of
apple pie, an actual, individual example of one, a real object. In a
sense, a class
is a theoretical concept. It gives us the power to make instances of
that class. An instance is a real, functional object, not just an idea,
that contains all the parts that the class definition describes.
Like functions, classes are created with definitions. You begin a class definition with the keyword class, instead of the word def. The example in program 10-1 defines a class called Coin. Let's examine it.
class Coin:
def __init__(self):
self.sideup = 'Heads'
def toss(self):
if random.randint(0, 1) == 0:
self.sideup = 'Heads'
else:
self.sideup = 'Tails'
def get_sideup(self):
return self.sideup
In the code fragment above, the class called Coin is defined, and three methods
are defined in it. The text points out that it is common for class
names to begin with a capital letter. This makes them look different
from other types of "variables". The text mentions that the three
methods inside the class definition look like any other function
definitions. Well, not exactly.
- First, each of these methods expects a parameter that is called self. This is to tell the methods that they are to work with the object that they are part of. This is apparently a standard
for methods in a class. The thing is, when they are eventually called
they will not be passed an argument for this parameter. More on this in
a minute.
- The method with the really weird name, __init__(), is called that by another standard practice. This is a method that is supposed to be included in classes. It runs automatically. init stands for initializer. It is supposed to be the first method you create in a class. In this case, it only initializes the value of one attribute, self.sideup.
The text takes a moment at the end of the discussion to mention how the
method's name is spelled: two underscores before and another two after
the name init. Wow, guy, think you took long enough to bring that up?
- The toss() method simulates a coin toss, randomly assigning Heads or Tails to the sideup attribute.
- The get_sideup() method returns the current value of the sideup attribute.
What
the text does not mention is that toss() and get_sideup() are methods
that are going to be called by the main() function. This code is added
in program 10-2. The program
creates the class called Coin, then creates an instance of this class
in the main() function. The line that does this is
my_coin = Coin()
This is a little misleading. We are not calling a function called Coin.
We are invoking the class called Coin, which is expecting arguments, but
not really. No argument is passed to the class. It already knows to include
the self argument where it is needed. Notice that the program does not
actually pass any arguments to the methods of the object either, as I
warned you in the bullets above. They are only expecting values for self.
The self parameter is a built-in reference for the object that contains
the code that is being called. In other words, toss() is a method of an
object, and we want to assign its calculated value to an attribute of
that same object. It's really simpler than it looks.
The text runs through this example program for a few pages
before mentioning the next item: hiding attributes. Example program 10-3
demonstrates that code in a program can write to attributes in an
instance of a class, if we only write the code as the text has shown it
to us so far. This is bad programming, if we are at all concerned about
the honesty of our coin tossing program, and about its vulnerability to
hacking by a person who can inject a new line of code into it. This
concern is more valid if the class definition is not actually in our
program file, but this problem will be addressed in a minute. For now,
the problem is that code outside the object itself can rewrite the
value of an attribute. The text overcomes this problem with a simple
solution. The name of the sideup attribute is changed to __sideup. This
is apparently enough. When the interpreter sees an attribute with two underscores as the first characters of its name, that attribute becomes private to the methods inside the instance/object it is part of.
Around page 433, the book tells us about a better security tool: storing
classes in modules. If your user sees the objects that are being
created as mysterious black boxes, they are less likely to have an inspiration
about circumventing your code. In example program 10-5, the author creates
a file to be saved as coin.py, which will be a new importable module.
This file imports random, then it defines the class Coin. It is the file
we have been working on so far in the chapter. Example program 10-6 imports
the coin module, creates a Coin object by referencing the class the same
way you would reference a function in a module, then runs the rest of
the main() function we have seen in the other recent examples.
The next example starts with the idea that we can and should
create a new module to hold our new class definition. This file will
hold the definition of a class called BankAccount, which will have several methods, some of which will use parameters in addition to the required self
parameter. This file will be stored as bankaccount.py, which is
imported by example program 10-8. You can think of this program as an
interface to an ATM. It uses the techniques discussed so far in the
chapter. When an object of this class is created, its __init__()
method expects to be given an opening balance, which is assigned as the
value of the object's __balance attribute. This is an example of
passing an argument to a class when creating an instance of that class.
The __balance attribute is initialized when the object is created,
incremented when the deposit() method is used, decremented when the
withdraw() method is used (unless the value of __balance is not
sufficient), and reported to the user when the get_balance() method is
used.
As it is, program 10-8 is only good for setting up a new
account. In a real ATM program, we would have to check the available
balance in an existing account. We could combine the two ideas to
create a temporary account in the memory of the ATM which would
retrieve the balance information from an account in the cloud (on our
bank's network), This account would be modified by the user, then
updates would be sent to the official database.
Program 10-9 adds a new method that the text recommends as a standard: the __str__()
method is intended to return the "state" of a program. For the bank
account program, the author defines the state as the value of the
current balance. This is obtained by reading the value of
self.__balance. This may seem like duplication to you, since the
get_balance() method returns the same value. The difference is that the
__str__() method is not called by a user. It is called automatically
when you pass the name of the bank account object to a function. In
example program 10-10, this object is passed to the print() function on
line 19. This seems like magic, and it is, really. As the text
explains, the __str__ method is called automatically, because Python
looks for it, and knows to use it as the method that generates the
string that represents an object. Makes perfect sense...
Working with Instances
The text opens this section with an observation that should be
self-evident: each instance of a class has its own set of attributes.
Well, that is as it should be: each instance is a separate object. That
is what the self parameter tells a method: do the following with
respect to the attributes and methods of this object. The text presents
a short program that creates three instances of the Coin class. The
program then calls the toss() method of each instance, then reports the
state of each instance, showing that they are not all the same (well,
they could be, but it's
unlikely). The thing to learn from this example is the syntax to access
the methods and attributes of the separate objects. The key is to keep
track of which object you should be accessing.
The text begins a section about accessors and mutators
on page 445 in the banana edition. These are methods that provide the
ability to read and change date in an object's attributes.
- An accessor is a method that can read and return a value from an attribute, but cannot change that value. Accessors are also called getters, because they can get values.
- A mutator is a method that can store or change a value in an attribute. Mutators are also called setters, because they can set values. A mutator method should validate data before trying to write it to an attribute.
The text goes on at length about this concept, making sure
that we understand the necessity of these methods. If the user is not
given what we might call curated access to the data, running reports
and making changes to data in attributes would require the help of a
programmer. I have worked with systems like this in the past, and I can
tell you from experience that users are never happy with them. Being
able to ask for what they want when they want it, and being able to
change what they are allowed to change, are empowering for an
employee/user.
On page 448, the author discusses passing an object as an argument. He creates a new object:
my_coin = coin.Coin()
This code is different from the code example above because the
author is now calling the Coin class from the coin library module where
he stored it. I think his naming convention could be a little more
verbose. In the next line, he passes the name of the new object to a
function:
show_coin_status(my_coin)
The text explains that passing the object as an argument does
not pass a copy of the object. What actually happens is that the
function being called will receive a reference (also called a pointer) to the real object. Anything the function does will affect the actual object, not a copy.
That is enough for this lesson.
|