Lecture 1 - DePaul University



CSC 401 NotesIntroduction to Programmer-Defined ClassesIntroduction: We are about to embark on a discussion of a fundamental tenet of “object oriented programming”, namely, class creation. This is one of the most important concepts in modern programming and therefore, we will be spending a fair amount of time on it. Before beginning, however, we must review a few concepts. Initializers / Constructors Those of you who covered the basics of class creation in other languages surely learned about constructors. In Python, constructors are a slightly more exoctic construct. Instead, the construct that languages such as Java refer to as a constructor is called an initializer in Python.THAT BEING SAID: Even many (probably most) Python progarmmers still call it the intiializer. Because this is the case, and because many othe rlanguages such as Java refer to it as the constructor, we too, will refer to it as the constructor. That being said, to recognize that technically speaking, a Python constructor is a different construct, but one that is rarely used. The constructor is a special and very important method to understand. It is a method that is invoked automatically every time you instantiate (i.e. create) a new object. x = -3Behind the scenes, the constructor has been invoked by the Python interpreter.This constructor method has a rather unusual identifier: __init__(). We will see that the classes we have been using (as well as the new classes we will be creating) should ALL have this method. Let's begin by reiterating the point that there is an alternate way to create new objects from what we have (mostly) been doing so far. This technique involves the use of the constructor. It is very important to become familiar with this way of creating new objects. Example: We are very familiar with creating new int objects by typing:x = -17Here is the "constructor " version of creating the same object: >>> x = int(-17)Example: Here we use a constructor to create an object of type list:>>> someList = list( [3,4,5] )>>> someList[3, 4, 5]And here we use a constructor to create an object of type str (i.e. string):>>> s = str(‘Hello’)NOTE: While the constructor method is named __init__(), when we invoke the constructor, we do not type out that identifier. Instead, we type the name of the class.>>> x = __init__(-17) #Nope!!>>> x = int(-17) #Yes!A constructor is the most fundamental technique for instantiating (i.e. creating) new objects.InstantiationDon't get thrown off by this term. By "instantiating" we simply mean that we are "creating a new instance of". This is very commonly used terminology when we talk about creating new objects. But again, all it means is that "we are creating a new instance of". Example: x = int(-17)can be stated as: "We have instantiated an int."flag = bool(True)#We have instantiated a booleanDefault Constructors: When we instantiate using a constructor, but don't pass any arguments to that constructor, the method assigns a default value to the object. A well designed constructor should always do this. (Even if it just assigns the null value: None).Here is an example where we invoke the constructor and pass it an argument:>>> x = int(-17) #here we DO pass an argument to the constructor>>> x-17Here is an example where we invoke the constructor and do NOT pass it an argument:>>> x = int() >>> x0 #The int constructor assigns a default value of 0>>> lst = list() >>> lst[]#Note that the list() class' constructor#assigns a default value of emtpy listMany default constructors involving collections simply create empty versions of their object e.g. lists, tuples, strings, etc. Many default constructors involving numbers assign a default value of 0. Why Create Our Own ClassesConsider the classes (i.e. data types) of objects we have dealt with so far: Classes in which the object holds a single value class ‘int’class ‘string’class ‘boolean’etcClasses of objects that are collections of values:class ‘list’class ‘set’class ‘tuple’ etcClasses that are collections with named items class ‘dict’ (dictionaries) We will now learn how to create our own classes (data types). In the process of doing so, we will also:Learn to create some very specialized objects as necessary for our programming needsDevelop a deeper and better understanding of how to use our familiar classes (int, boolean, dict, etc, etc) more effectivelyOur First User-Defined Class (Data Type)Aside: By "user-defined" we mean that it is not a built-in class (such as string, int, list, etc). User-defined means that a programmer has created the class based on their particular needs.Aside: “Class” v.s. “Data Type”: Throughout our discussions of classes, you will ocasionally hear me refer to them as “data types”. There is a subtle distinction, but we will not get into this issue here. I like to use the terms interchangably for teaching purposes. Let’s jump right in by looking at a situation in which we might want to create our own class (i.e. data type). Imagine a teacher wanted to represent a student in a particular course in which there is only a midterm exam (making up 40% of the total grade), and a final exam (making up 60% of the total grade). Therefore, for every student object, the teacher would need to keep track of the student's:nameid (since two students might have the same name)midterm exam gradefinal exam gradepercentage gradeletter gradeEach of the above items will be created as something called instance variables. Aside: You will hear various terms used for these instance variables. Most commonly, I will refer to them as either "instance variables", or “fields” or “members”. You’ll get used to hearing all of these terms over time. We will begin by creating a new class called Student. We will start with only two of the above instance variables (the name and ID), and will gradually add the rest of the fields as we progress through our exploration of creating classes. In Python, we create a class (i.e. data type) by typing: class <Class Name>(inherited_class):For now, this “inherited class” will always be a class called object. So to create our student class we would start with:class Student(object):Inside this class, we will have a collection of methods. It is inside these methods that we will create our instance variables. There are various different ways of creating instance variables. As mentioned above, we pretty much only create them inside methods of our class. Most commonly we will create them inside a particular– and very important – method called the “constructor”. In Python, the constructor method is always given the identifier: __init__() i.e. two underscore characters, the word ‘init’ and two more underscores. Here is our first, rather primitive version of our Student class. This early version contains only one method, the constructor. It is inside this constructor that we define two of our instance variables: class Student(object): 'a class to represent a Student' def __init__(self): 'the constructor for the Student class' self.name = ''#create an instance variable: name self.idNum = ''#create an instance variable: idNumA few important notes:The word object in the class declaration line MUST be there.The word self as the first parameter of the constructor MUST be there. (In fact, the parameter self must be the first parameter of pretty much every single method in the class).Note that self is also used before each of the two instance variables. That is also required! We will, of course explain the reasons for all of this as we proceed. Now that we have a new data type, let’s create a couple of new objects of this type. To do this, we MUST use the constructor technique:s1 = Student()s2 = Student()s3 = Student()Each of these objects (a.k.a. “instances”) has its own copy of ‘name’ and ‘idNum’. In fact, that’s why we call name and idNum “instance variables”. We can assign values to these instance variables as follows:s1.name = "Phoebe"s1.idNum = "1111"s2.name = "Monica"s2.idNum = 2222s3.name = "Chandler"s3.idNum = 3333COMPARE: Here is what the code might look like if we wanted to start creating a class to represent an Animal with instance variables for ‘species’ and ‘language’. (For brevity, I am leaving out docstrings).class Animal(object):#the class declaration def __init__(self):#the constructor self.species = '' self.language = ''Now let’s instantiate a couple of Animal objects and set values for the instance variabes:a1 = Animal()a1.species = "Dog"a1.language = "Bark"a2 = Animal()a2.species = "Cat"a2.language = "Meow"Okay, let’s return to our Student class.Recall how we have been using constructors in the past. For example, to create an integer using the constructor for the int class we would write:n = int(3)This allows us to instantiate an integer, and assign it a value of 3. In other words, as we instantiate (create) the integer object, we assign it its value at the same time. At the moment we can’t do this with our Student class. With our Student class, we must first instantiate using:s3 = Student()and then subsequently assign values, e.g. s3.name = "Jenny"Let’s modify our constructor method in the Student class to allow us to pass arguments which will assign values to the instance variables name, and idNum: def __init__(self, theName, theId): 'constructor' self.name = theName self.idNum = theIdWe can now instantiate as follows:s1 = Student('Mary',4444)If we typed: print( s1.name ) it would output ‘Mary’However, now try instantiating a new Student object without any arguments like we did originally:s1 = Student()We will get an error because the constructor expects two arguments (one for the name and one for the id number). Again: I realize it looks like the constructor expects 3 arguments, but for now, we can ignore that first self parameter.But what if we don’t yet know the value for one or more of those parameters? For example, what if we want to create a new Student object, but don’t yet know the values for the name or id number? The answer is to create default values for those parameters. Having default values for parameters should not be unfamiliar to you. Here is how it might look:def __init__(self, theName='unknown', theId=-1): 'Constructor. Defaults name to unknown, and idNum to -1' self.name = theName self.idNum = theIdNow we have more options!s1 = Student('Mary',4444)print(s1.name) #outputs "Mary"s2 = Student() #the default constructorprint(s2.name) #outputs "Unknown"Thought Question: Why did I choose -1 as the default value for the ID number? Answer: This way, if we see a value of -1 for idNum, we know that this isn’t really a valid ID number. In other words, it tells us that the idNum instance variable has not yet been assigned a legitimate value yet. It’s the exact same principle as assigning a default value of 'unknown' to name. Okay, now let’s add in two more instance variables, the ones for the midterm exam grade, and the final exam grade. Again, recall that we typically create instance variables by placing them inside the constructor. We should also probably add in two more parameters to our constructor for those two new instance variables:class Student(object): def __init__(self, theName='unknown', theId=-1, mt=-1, fe=-1): 'constructor method' self.name = theName self.idNum = theId self.midtermScore = mt self.finalScore = feWe can now instantiate as follows:s1 = Student('Mary',4444,87,92)print( s1.midtermScore ) #would output 87Thought Question: What would get output if we had the following code? s1 = Student('Mary',4444)print( s1. midtermScore ) Answer: Because no value was provided for the 'mt' parameter, the default value of -1 will be assigned. So it would output -1.Now let’s tack on the final two instance variables, the ones representing the percentage grade and the final exam grade. However, let’s take a moment to analyze the scenario for these two fields. (Aside: Remember that ‘fields’ is another term for ‘instance variables’). The percentage grade is calculated from the current values of midtermScore and finalScore. Therefore, we don’t need to assign it ourselves. Rather, let’s create a method that examines the values of midtermScore and finalScore, does the calculation and assigns the resulting value to percentageGrade. def calcPercentageGrade(self): '''calculates percentageGrade as 40% of midtermGrade plus 60% of finalGrade''' self.percentageGrade = (0.4*self.midtermScore + 0.6*self.finalScore) self.percentGrade = round(self.percentGrade,1) #let's round it to 1 decimalImportant: This is another example of where we have created a new instance variable inside a method. Recall that we typically create our instance variables inside of methods. Most instance variables are created from inside the constructor method. However, we can and will create instance variables inside other methods as well, as is demonstrated here.Let's create one final method that creates an instance variable for the letter grade. This method will assign a letter based on the percentage grade. def setLetterGrade(self): 'assigns letter grade based on the value of the percentage grade' self.letterGrade = 'U' #create the instance variable #'U' is meant to represent "Unknown" if self.percentageGrade>=90: self.letterGrade='A' elif self.percentageGrade>=80: self.letterGrade='B' elif self.percentageGrade>=70: self.letterGrade='C' elif self.percentageGrade>=60: self.letterGrade='D' elif self.percentageGrade>-1: self.letterGrade='F' #If percentageGrade is -1, it means we don't yet #have a valid value for percentageGrade, so we should #leave it as 'U'Okay, so we have now created a class that has all of our desired instance variables. Let's return to the constructor for a moment. In addition to allowing us to create a new object and create new instance variables, another very important use of the constructor is to set up default values for many – and often all – of our instance variables. Question: However, as we have seen, some of our instance variables were created inside other methods. What to do?Answer: Let's invoke those methods from inside our constructor! Here is the final version of our constructor method. Note that inside this constructor, we invoke the methods that create our last two instance variables (percentageGrade and letterGrade).def __init__(self, theName='unknown', theId=-1, mt=-1, fe=-1): self.name = theName self.idNum = theId self.midtermScore = mt self.finalScore = fe self.calcPercentageGrade() self.setLetterGrade()Here are some examples of using our new class:>>> s1 = Student()>>> s1.letterGrade'U'>>> s2 = Student("Archie",3333,93,86)>>> s2.percentageGrade88.8>>> s2.letterGrade'B'NOTE: The value for s1’s letter grade is U because the default constructor had assigned -1 for the midterm and -1 for the final exam. The constructor then invoked the calcPercentageGrade() method which assigned a calculated grade of -1 as well. Because s1's percentageGrade is -1, when the constructor then invoked setLetterGrade(), the assigned value was 'U' (meaning "Unknown").The same process occurred for s2, except that this time, because we had meaningful values for the midterm and final exam scores, when the constructor invoked the calcPercentageGrade() and subsequently setLetterGrade(), the values were set appropriately based on the exam scores.All of the decimal places seen for s2's percentage grade and that "1" at the end results from something called a "floating point error". We will ignore this for now. The easiest solution is to limit the number of decimal places when we output.Now suppose that the course has proceeded, and we have meaningful values for s1's exam scores. Let's assign those values:>>> s1.midtermScore=90>>> s1.finalScore=92Now let's see what we get for s1's percentage grade:>>> s1.percentageGrade-1.0What's going on? Answer: The instance variable percentageGrade is still holding it's original value of -1 that was calculated from inside the constructor. The percentageGrade variable will not "magically" change just because we have updated the midterm and final exam scores. In other words, it is now up to us to recalculate s1's percentage score based on the new information (i.e. s1's exam scores):>>> s1.midtermScore=90>>> s1.finalScore=92>>> s1.calcPercentageGrade()>>> s1.percentageGrade91.2In order to determine the letter grade, we must again tell the program to recalculate the letter grade: >>> s1.setLetterGrade()>>> s1.letterGrade'A'Now suppose you wanted to see all of the information for our Student object. We could type:print('Name: ' + s1.name)print('ID: ' + s1.idNum)print('Midterm: ' + s1.midtermScore)etcetcWe'd have to do this for every single instance variable. This would be tedious (and imagine if we had 10,20,30 instance variables?!). In addition, now suppose you wanted to do it again for an object s2, and then perhaps another object s3. Or even a list of a few hundred Student objects!Instead, let's create a method that returns a string showing all of the Student information in some "neat" form, all of the information for a Student object. For now we will call this method: getStudentInfo(). Shortly, we will learn a better way to name this method. def getStudentInfo(self): '''returns a string containing all info of the Student object in a nicely formatted string''' s = 'Name:\t{}\n\ID Number:\t{}\n\Midterm Exam:\t{}\n\Final Exam:\t{}\n\Percentage Score:\t{}\n\Letter Grade:\t{}'.format(self.name,self.idNum, self.midtermScore,self.finalScore, self.percentageGrade,self.letterGrade) return sAside: In case you have forgotten, recall that we use the \ character if we want to break up our code into multiple lines while inside a string.Aside: Recall the odd indentation situation inside the string above. This full left indentation is necessary due to the fact that I wanted to break up the long string into multiple lines. When using the \ character inside of a string, the string will include all whitespace as part of the string – inclduing indentations. Therefore, on all broken lines (inside the string), I needed to eliminate any extraneous white space – or else it would have shown up in the eventual output string. At the same time, the spacing inside the format() method is not left-justified since it is not inside the string. In this case, I have just indented in such as way as to make the code look a little neater, and therefore, easier to read. The quirks of whitespace and how it works in Python does take a little getting used to!What's Up With 'self'?In order to create classes effectively (and we do need to!), it is vital to understand what this self parameter that we've been seeing all over the place is used for. Briefly, it refers to the "calling object". What does that mean?Consider this bit of code:l1 = [3,4,5]l2 = ['hi','bye','later']Now suppose that we wish to append a number into our list l1. To do so, we will use a method that exists in the list class called append(). Can we invoke it as follows?append(6)Hopefully you realize that this would not work. The reason is that the append() method is intended to work on a specific list object. In other words, we need a list object to call (invoke) the method:l1.append(6)In this case, l1 is the "calling object". Similarly, suppose we wanted to invoke the isupper() method of the string class. Hopefully you recognize that typing:isupper()By itself would do nothing – and wouldn't even make sense. We need a calling object in order to invoke this method:s = 'hello's.isupper()In other words, all “class methods” of a class (there are other kinds of methods) require a calling object. Consider the calcPercentageGrade() method: def calcPercentageGrade(self): self.percentageGrade = (0.4*self.midtermScore + 0.6*self.finalScore)I hope you agree that this method requires a calling object. Simply invoking it like so:calcPercentageGrade()would make no sense! Do we want to invoke this method on the object s1? Or a different object such as s23? In other words, we would want to invoke it with a particular object:s1.calcPercentageGrade()In this case, s1 is the calling object. While in this case:s24.calcPercentageGrade()In this case, s24 is the calling object. So to come back to our original discussion, inside a method, the parameter self refers to the calling object. Returning to the method:def calcPercentageGrade(self): self.percentageGrade = (0.4*self.midtermScore + 0.6*self.finalScore)To reiterate: The self in this method refers to whichever object invoked calcPercentageGrade. If the method was called by an object s3, s3.calcPercentageGrade()then self refers to s3. If the method was called by an object stud42stud42.calcPercentageGrade()then self would refer to stud42. NOTE: We do not have to name this parameter "self". We could call it madame_tussaud if we wanted to. However, by convention, Python programmers always use the identifier self.NOTE: We still haven't explained why we must include the self parameter that I said must be present as the first parameter inside every instance method of the class. def __init__(self, theName='unknown', theId=-1, mt=-1, fe=-1):This topic, too, will be explained soon.Version 1 of our Student ClassYou can find this version in the file Student_v1.pyTerminology for classesWhen we create a new object (typically using a constructor), we say that we are instantiating an object. (i.e. Creating an instance of an object).Every class is essentially a list of class attributes (also called members), meaning class variables and class methods. The available methods and attributes of the type can be seen using the help function.>>> help(int)Help on class int in module builtins:class int(object) | int(x[, base]) -> integer | | Convert a string or number to an integer, if possible. A floating | point argument will be truncated towards zero (this does not include a | string representation of a floating point number!) When converting a | string, use the optional base. It is an error to supply a base when | converting a non-string. | | Methods defined here: | | __abs__(...) | x.__abs__() <==> abs(x) | | __add__(...) | x.__add__(y) <==> x+y | | __and__(...) | x.__and__(y) <==> x&yEtc.Doc Strings are Important! Note the importance of doc strings. Whenever someone uses your class, they will definitely be using the help() function to see which methods are available. Compare: class Account Let's look at another class. This will represent a bank account, and has instance variables for the customer name, the customer ID number, the balance, and the interest rate. As before, we will create most (in fact, in this case all) of the instnace variables inside the constructor. You can find the complete code in BankAccount_v1.pyclass Account(object): 'a bank account class' def __init__(self,custName='unknown', idNum=-1, bal=None,intRate=None): '''constructor for Account class, balance must be in decimal form''' self.name = custName self.custId = idNum self.balance = bal self.rate = intRate def withdraw(self,withdrawalAmount): '''deducts withdrawalAmount from balance requires that balance >= withdrawalAmount returns -1 if not, and does not adjust balance''' if self.balance>=withdrawalAmount: self.balance = self.balance - withdrawalAmount else: #perhaps print error message to log return -1 def setInterest(self,newRate): '''changes the instance variable rate to newRate rate must be in decimal form (e.g. 0.05)''' self.rate = newRate def addInterest(self): '''updates balance by adding interest to it, requires rate to be set''' self.balance = self.balance + (self.balance*self.rate) def getInfo(self): 'prints all of the information for the object in friendly form' s = 'Name:\t{}\nID:\t{}\nBalance:{}'.format(self.name, self.custId, self.balance) return sA few additional random but important notesMost of this Account class will closely resemble what we have done in the Student class. However, there are several important additional things to note here. So do not "skim" the notes below. Please review them carefully, as we will be implementing these types of details as we proceed.Use of None in the constructor. None is the way Python refers to null values. Remember that 0 is a valid number, so you should be careful about initializing a variable to 0 as a "default" value. For example, an interest rate of 0 is not a good default value. Perhaps -1 could be used, as that would clearly let the programmer know that the particular variable has not yet been assigned a meaningful value. However, None clearly indicates that the variable in question does not yet have any value. Some programmers feel – and there are good reasons for this – that using None for default values is not always a good idea. We will not delve into this for now. Suppose our business logic dictates that overdrafts should never be allowed. Therefore, in our withdraw() method, we first make sure that the balance is greater than the withdrawal amount.In our withdraw() method, if the user attempts to withdraw more than the available balance, we might output to a log. However, note that the method returns a -1. This means that someone using our method can make sure that the method does not return -1 in order to confirm that everything worked as desired.a3 = Account('Smith',323354,1500,0.015)if ( a3.withdraw(2500) == -1 ): #code to let user know we can't do this withdrawalWe also make a point of indicating this in the doc string. This means that when a programmer who is using our class invokes this method, they understand how they might use it as in the code snippet above.Similarly, note how in setInterest(), our doc string indicates how the programmer expects rate to be provide (i.e. in decimal form)Very Important: In our withdraw() method, note that we do NOT precede withdrawalAmount by self. This is because withdrawalAmount is NOT an instance variable. It is simply a parameter to this method indicating how much to withdraw. The complete class definition can be found in: BankAccount_v1.py ................
................

In order to avoid copyright disputes, this page is only a partial summary.

Google Online Preview   Download