Using Classes Throughout out study and use of Python we have discussed and used objects. In this lecture we will learn more about reading and using classes (which are templates from which objects are created), and start to learn about writing classes; but barely, because writing classes is the main topic for the following two lectures. As we understand classes better, we will understand all aspects of Python better -those parts we have already learned and those parts we have yet to learn. When we started learning about Python, we started with simple objects storing int, float, str, and bool values. What do the names int, float, str, and bool refer to? They are names that refer to objects that represent the classes from which object values are constructed. When we write objects from these simple classes, we use special literals to describe their values. Note that all of these objects are immutable: we cannot change their state (but we can rebind names to different values: e.g., x += 1 which means x = x+1). Later we discussed, list, tuple, dictionary, set, and frozenset objects: these names are also bound to objects representing classes (some mutable, some immutable). When we write objects from these classes, we typically use special literals to describe their values (like lists in [] brackets), but we can also use the name of the class: e.g., x = list() or x = list(aset). We call this form using a constructor to construct the object bound to the name x. In all the classes that we write ourselves, we will use this constructor form: the name bound to the class object to create an object of the class. Classes serve as templates for constructing objects. Every object that we construct is initialized by a special method named __init__ in some class. Much more on this special method in the following two lectures. Let's review the difference between functions and methods. We can pass objects as arguments to functions: we have written many functions whose arguments are objects). But, we can also call methods on objects using a different syntax. For example if x is a list, we can call its append method as x.append(5) to append the value 5 to the list. We first saw methods used when processing strings: if s is a str, we can write method calls on s as s.upper() or s.find('#') or s.rstrip(). The fundamental equation of object-oriented programming tells us that Python translates the method call o.m(p) to the function call type(o).m(o,p). So, Python translates the method call s.find('#') into str.find(s,'#'): it usess the str class object and calls its find function, passing it the arguments s and '#'; likewise, Python translates x.append(5) into list.append(x,5): it finds the list class object and calls its find function, passing it the arguments x and 5. So, we use this special syntax to call methods (defined in classes) on objects (constructed from classes) and also pass along any other needed arguments. This syntax is the defining feature of using classes in object-oriented programming. ------------------------------------------------------------------------------ The Dice class With this as prologue, let's see the entire class definition for Dice (all classes we define will use upper-case letters; multiple words are separated by underscores). The Dice class models rolling any number of dice, each having any number of sides. We will not tackle understanding the code inside this class in this lecture, but instead just look at its the method headers. In the next two lectures we will study its method bodies, which contain familiar Python statements. Of course, dice are rolled randomly, for which we import the random module and use its randint and seed functions. ------------------------------------------------------------------------------ import random class Dice: def __init__(self,max_pips): assert len(max_pips) >= 1, 'Dice.__init__: max_pips is empty' for i in range(0,len(max_pips)): p = max_pips[i] assert p >= 1, 'Dice.__init__: max_pips['+str(i)+']='+str(p)+': must be an int >= 1' self._max_pips = max_pips[:] #Copy to avoid aliasing self._pips = [0]*len(max_pips) self._roll_count = 0 def roll(self): self._roll_count += 1 self._pips = [ random.randint(1,max_pips) for max_pips in self._max_pips ] return self def number_of_dice(self): return len(self._pips) def all_pip_maximums(self): return self._max_pips[:] def rolls(self): return self._roll_count def pips_on(self,i): assert self._roll_count > 0, 'Dice.pips_on: dice not rolled' assert 0<= i < len(self._pips), \ 'Dice.pips: die index i('+str(i)+') must be >= 0 and <'+str(len(self._pips)) return self._pips[i] def all_pips(self): return self._pips[:] def pip_sum(self): assert self._roll_count > 0, 'Dice.pip_sum: dice not rolled' return sum(self._pips) def pips_same(self): return all( [self._pips[0] == p for p in self._pips] ) def __str__(self): return 'Dice('+str(self._pips)+')' def __repr__(self): return 'Dice('+str(self._max_pips)+')' def standard_rolls_for_debugging(self): random.seed(12161949) ------------------------------------------------------------------------------ Again, in this lecture we will examine only the headers of the methods, which are highlighted here by removing their method bodies. ------------------------------------------------------------------------------ class Dice: def __init__(self,max_pips): def roll(self): def number_of_dice(self): def all_pip_maximums(self): def rolls(self): def pips_on(self,i): def all_pips(self): def pip_sum(self): def pips_same(self): def __str__(self): def standard_rolls_for_debugging(self): ------------------------------------------------------------------------------ The Dice class is the only definition in the dice.py module, which already is in the courselib folder. We can use Dice in any of our modules by just importing dice.py (as was done in the craps.py module used in the debugger handout). We can also impor this class in the interpreter to test its methods individually. The actual Dice class that I wrote includes comments that describe the class and each of its methods. We can see these headers and read their comments in the html documents produced by a program called pydoc. To view this document, go to the course home page, then click on the "Course Library Reference" link in the index; then click on the Dice link. Do this now, while you are reading this lecture (I will, when I discuss it in class). The methods in this document appear in alphabetical order (although the special __init__ method always appears first). Reading from the top down, in blue is the module in which this class is defined: dice.py. In purple are the modules that this one imports and uses: just random. In red is the class hierarchy for the Dice class (all classes inherit from builtins.object): which is just Dice; nested in red is the header of the class definition and a brief description of the class. Below that are the method headers (minus the keyword def) and documentation for each method that is not private (more on private in the next two lectures). The main idea behind objects constructed from the Dice class is that they model some number of dice, where each can have a different number of sides. We can execute a roll command on these dice (mutating the pips showing on each side) or query the rolled dice in various ways. Dice defines one command/mutator and many query/accessor methods. Each method has a parameter named self, which is the Dice object being operated on. For example, if we defined d = Dice([6,6]) d would refer to a Dice object representing two dice, each with six sides. The list's length is the number of dice and the values in the list specify how many sides each die has (the dice are indexed like lists: 0 and 1 in this case. We can also call methods like d.roll() and d.pip_sum()... Here are examples of calling all the Dice methods, summarizing in English what each method does. A Dice object stores this list of sides, a list of the number of pips showing on the dice (once it has been rolled) and a count of the number of times the time have been rolled (something that real dice cannot do). These method examine or update this date. d.roll() increasing the roll count and generate new random pips (returns d, so can cascade calls: d.roll().pip_sum()) d.number_of_dice() return the number of dice d.all_pip_maximums() return a list showing the maximum allowed pips d.rolls() return the number of times the dice have been rolled d.pips_on(i) return the number of pips on the ith (starting at 0) die d.all_pips() return a list of all the pips on all the dice d.pip_sum() return the sum of all the pips onall the dice d.pips_same() return whether or not all dice show the same pips d.__str__() return a string representation of the Dice object (same as calling the str functions: str(d)) d.standard_rolls_for_debugging() ensures the same pips are rolled each time Let's see how to perform various dice operations on a parameter to a function. The experiment function below rolls the dice (notice the lower-case d in the parameter name) the specified number of times, and records how often each pip sum is thrown. It creates an array that can be indexed between 0 and the maximum pip sum, then loops the required number of times, incrementing the index of the pip sum thrown, and finally returning the array. def experiment(dice : Dice,times : int) -> NoneType : histogram = (sum(dice.all_pip_maximums())+1)*[0] for i in range(times): histogram[dice.roll().pip_sum()] += 1 return histogram For the script below (using the print_histogram function) d = Dice([6,6,6]) print_histogram('1 million dice rolls', experiment(d,1000000)) The result it prints is as follows. Note that with 3 6-sided dice, the lowest possible pip sum is 3 and the highest 18. We would expect 3 and 18 to be thrown with probability 1/6**3 or 1/216 or about .46 percent (which is what the bins 3 and 18 show, to one significant digit). 1 million dice rolls 3[ 0.5%]|* 4[ 1.4%]|***** 5[ 2.8%]|*********** 6[ 4.6%]|****************** 7[ 6.9%]|*************************** 8[ 9.6%]|************************************** 9[ 11.6%]|********************************************** 10[ 12.5%]|************************************************* 11[ 12.5%]|************************************************** 12[ 11.6%]|********************************************** 13[ 9.8%]|*************************************** 14[ 6.9%]|*************************** 15[ 4.6%]|****************** 16[ 2.8%]|*********** 17[ 1.4%]|***** 18[ 0.5%]|* See the craps.py script for another use of the Dice class. ------------------------------------------------------------------------------ The Stopwatch Class: The Stopwatch class is the only definition in the topwatch.py module, which is also already in the courselib folder. We can use Stopwatch in any of our modules by just importing stopwatch.py. This class models a stopwatch that is either stopped or running (forward -adding time- or backwards -subtracting time). We can read the time (in seconds) on the stopwatch (while it is stopped or running), and reset the stopwatch. You can examine the documentation for this class at the same location as the Dice class, and read it the same way. Here are all the methods that we should call (there are a few more, but those are private helper methods, which we will discuss in the next two lectures). This class is the opposite of Dice in that it defines many more commands/mutators (the first four) than queries/accessors (the second to last one; the last one is called only for debugging purposes). class Stopwatch: def reset(self): reset the stopwatch: stopped, 0 accumulated time def start(self): run the stopwatch forward def start_backwards(self): run the stopwatch backward def stop(self): stop the stopwatch def read(self): return the time elapsed (a float in seconds) def status(self): return a tuple with the state of the stopwatch We can easily use this class to determine how long it takes to execute some code, including how long it takes the user to respond to some prompt. timer = Stopwatch() # with no arguments, the stopwatch is stopped at 0 seconds timer.start() code-to-time timer.stop() print(timer.read(),'seconds elapsed time') In fact, we can shorten this code to the following, although the code above can be more easily changed to do slightly different tasks. When we construct the stopwatch we can construct it to be running, and we can read a running stopwatch (we don't have to stop it to read it). timer = Stopwatch(running_now=True) # running from 0 seconds code-to-time print(timer.read(),'seconds elapsed time') # can read a running stopwatch The code below times how long it takes to do a loop calling function f, and then subtracts the amount of time it takes to run the same loop without calling f. timer1 = Stopwatch() # with no arguments, the stopwatch is stopped at 0 seconds timer1.start() for i in range(1000000): f(i) timer1.stop() timer2 = Stopwatch() timer2.start() for i in range(1000000): pass timer2.stop() print(timer2.read()-timer1.read(),'seconds elapsed net time') We can use a single stopwatch to do both jobs, running it backwards after the first loop. timer = Stopwatch() # with no arguments, the stopwatch is stopped at 0 seconds timer.start() for i in range(1000000): f(i) timer.start_backwards() for i in range(1000000): pass timer.stop() print(timer.read(),'seconds elapsed net time') ------------------------------------------------------------------------------ Other classes in the courselib Examine the documentation for all the modules in courselib: goody, predicate, and prompt are modules that define functions. All the other modules define a single class: dice and stopwatch model data in the physical world; modular models a special kind of number; equivalence, graph,, queue, and stack model data types (e.g., they each store data, like lists/tuple/dict/set/frozenset do). All programming languages come with large libraries. For a programmer to become proficient in a language he/she must learn the language itself but also have a good familiarity with the language's library (and be able to create library modules of his/her own). Python Module Description dice A class modeling an ensemble of dice equivalence A class implementing the equivalence class data type goody A module defining miscellaneous useful functions (goodies) graph A class implementing the graph data type (using nodes and edges) modular A class implementing the modular number data type predicate A module defining predicate functions: i.e., def p(x:int)->bool prompt A module defining functions that prompt the user for values queue A class implementing the queue data type stack A class implementing the stack data type stopwatch A class modeling a stopwatch (useful for timing code) ------------------------------------------------------------------------------ Preview of the netxt 2 lectures on classes (2) Implementing classes: __init__, self, and methods (3) Advance classes: privacy conventions; translating operators to method calls