# Essential Notes on Object Oriented Programming (OOP) in Python. Part 1 # We've seen code using notation such as 'A.append(x)', A.pop(), etc. # etc... These are function calls, but they are slightly different from # the kind of functions that we have so far studied. Previously, all # the information that a function needs are passed in through its list of # parameters (arguments). But in an expression such as A.append(x), the # function 'append' is *oriented* towards a certain *data object*, namely A. # An object is a collection of data or attributes: a data structure. Functions that # that are oriented towards objects are also called 'methods'. # Functions allow us to structure large programs into self-contained # components. Objects serve this purpose at an even higher level. # A note of warning however: using objects in a program is not quite # the same as "object oriented programming," which requires a more # advanced understanding of objects and their use. # As a first example, suppose I want to write a program that manages # bank accounts. Each account contains several pieces of data or # *attributes*, such as who owns the account, the current account balance, # interest rate, etc... Each account is a data *object*. # The operations that I want to be able to define for each account object # are withdraw, deposit, balance-inquiry, etc... I want to be able to # *eventually* write the following code: ## create an account object for me with an initial balance of $1000: # youraccount = account("good student",2000) # create an account for you too. # myaccount = account("evil professor",1000) # myaccount.withdraw(400 # withdraw $400 from myaccount # youraccount.deposit(30) # deposit $30 into youraccount # print(myaccount.inquiry()) # balance inquiry on both accounts # print(youraccount.inquiry()) ## In this example, 'myaccount' and 'youraccount' are data objects. They're # also called *instances* of account. The code is almost self-explanatory, # which is the advantage that objects give us. But in order for the above # code to work, we'll need to define how to create an account, and what the # methods withdraw/depoist/inquiry actually do. This is done by # defining a *class*: class account: fee = 0.50 # this is a "class variable", applies to all accounts # The *constructor* function: def __init__(self,name,initbalance): self.owner = name # initialize data fields of the object self.balance = initbalance # owner, balance are "instance variables" # end of constructor function def deposit(self, amount): self.balance = self.balance + amount # add to balance # deposit method def withdraw(this, amount): # don't have to always call it 'self' if amount<=this.balance: this.balance = this.balance - amount - account.fee else: raise Exception("you don't have that much money!") # withdraw method def inquiry(me): # note "me" used instead of "self" return me.balance # balance inquiry method # end of class account # Just as writing a function does not cause anything to happen until you # *call* the function, creating a class does not create any objects until you # create *instances* of the class: myaccount = account("me",1000) # creates an account for me youraccount = account("you",2000) # and for you. myaccount.withdraw(400) # withdraw $400 from myaccount youraccount.deposit(30) # deposit $30 into youraccount print(myaccount.inquiry()) # balance inquiry on both accounts print(youraccount.inquiry()) youraccount.withdraw(-2000000) # this is a "vulnerability!" print(youraccount.owner, "has a balance of ", youraccount.inquiry()) account.deposit(myaccount,50) # equivalent to myaccount.deposit(50) account.fee = 0.75 # increase fee: one call works for all accounts # The account 'class' is a template or blueprint for creating account # objects. The class basically contains a series of function (method) # definitions. A function that stands out is __init__. This # function, which MUST be called __init__ (two underscores on each # side), is the **constructor** of the class. This function is # called when you say youraccount = account("your name",1000). It is the # job of this function to initialized the account object. Even though # there is no 'return' statement at the end of __init__, this special # function actually will return a pointer to the object that it has # just created. The first parameter of __init__, which I called # 'self' but really it can be called anything ('this', 'me', etc...) # has a special status. It is a pointer to the object that's being # constructed. The __init__ method then defines the *fields* or # *instance variables* of the object (owner and balance). You can # think of the object as just a collection of variables. Every # account object contains its own 'balance' variable and a 'owner' variable. # Objects can also share variables: the 'fee' variable is shared by all # account objects (they're called class variables as opposed to instance # variables). These variables should be referred to by the name of the class: # write account.fee instead of self.fee or this.fee. # Like arrays and hashmaps (dictionaries), objects are mutable (destructable). # This means that given an object like myaccount, we must think of it as # a *pointer*. We often need to distinguish the pointer from the object that # it points to. In fact, arrays and hashmaps are just special kinds of objects. # When I call account("myname",2000), the 'self' value is constructed # automatically. The constructor __init__ is called and the string # "myname" is passed to the parameter *name* and and 2000 is passed to # the parameter *initbalance*. These parameters are local variables # within __init__ and cannot be referred to outside. However, when the # constructor executes # self.owner = name # self.balance = initbalance # it uses the pointer 'self' to reference the object, and changes it. # Setting self.owner and self.balance does not change self, which is # still pointing to the same address in memory. It is the job of the # constructor to initialize the data fields of the object, which are # owner and balance. These variables are called "instance variables." # The class then defines the methods or functions that one wishes to call # on account objects, in this case deposit, withdraw and balance-inquiry. # Each of these methods also must have a distinguished first parameter: # This parameter points to the object that the function is operating on. # This is why deposit changes 'self.balance' and withdraw changes this.balance. # The first parameter is always a pointer to the data object: without it # we would not known *which* account we're depositing into. # To call a method on an object, we can use one of two forms: # "Functional Form": account.deposit(youraccount, 50) # calls the deposit method of class account, passing the pointer youraccount # to the functions's 'self' parameter, and 50 to the 'amount' parameter. # However, we will usually invoke the function as follows # "Object Oriented Form": youraccount.deposit(50) # This is the preferred form for object oriented programming. The # pointer to the left of the . is implicitly passed as the first # paramter of deposit. The class that youraccount belongs to is # inferred from the type of the youraccount object. That is, when I # make a call such as myaccount.withdraw(30), the parameter 'this' is # passed a pointer to myaccount, and the parameter 'amount' is passed # 30. ########### The Significance of Pointers: # Important concepts are worth repeating: objects can be # destructable/mutable: myaccount.withdraw(100) changes the account # object in place. Thus we have to distinguish between the pointer to # the object and the object itself: someaccount = myaccount # this only copies the pointer, not the object someaccount.withdraw(200) # this will also withdraw from myaccount print(myaccount.inquiry()) ## Another consequence of pointers is the following: bal = myaccount.inquiry() own = myaccount.owner accountcopy = account(own,bal) # creates real copy of myaccount print( accountcopy == myaccount ) # Will this print True or False? # The above will print False because accountcopy and myaccount are # pointers to different locations in memory (different account objects) # even though the contents of those locations are the same. ### To summarize some of the most important points about classes: # *** Remember: every method is defined with one extra parameter: the # *** first parameter is always the pointer to the object that the method # *** operates on. # Also note that you can have two different classes: A and B, and each # can define a function 'f'. But there's no confusion because the 'f' # is only called on different kinds of objects. # Remember: a class is a template for creating objects. The __init__ # method initializes the fields of the object. Do not confuse the class # with an object itself. The objects are 'myaccount' and 'youraccount', # which are two 'instances' of the class 'account'. # Remember: an object is referenced through a pointer. print( "--------- Another Example of a Class and Objects ---------" ) # This time I want to write a program to manage the win-loss records of # sports teams. Each team is a data object that records the number of # wins and the number of losses, as well as the number of games left on # the team's schedule (you can also imagine many other attributes). Thus # each team object contains the variables 'wins', 'losses' and 'gamesleft'. from random import random class team: def __init__(self,totalgames): self.wins = 0 self.losses = 0 self.gamesleft = totalgames # constructor def win(self): # method that records a win self.wins += 1 self.gamesleft -= 1 # win def lose(self): # method that records a loss self.losses += 1 self.gamesleft -=1 # lose def percentage(self): # calculate the team's winning percentage gamesplayed = self.wins + self.losses if gamesplayed==0: return 0 # don't divide by zero! return self.wins/(gamesplayed *1.0) # *1.0 to make it a float # percentage def project(self): # project the win-loss record of the entire seaon wp = self.percentage() # call percentage to get current percentage totalgames = self.wins + self.losses + self.gamesleft predictedwins = int(wp * totalgames) predictedlosses = totalgames - predictedwins return (predictedwins, predictedlosses) # project # note how percentage is called from within this function. def betterthan(myteam,yourteam): # see if myteam's percentage is better mp = myteam.percentage() yp = yourteam.percentage() return (mp > yp) # returns True or False # betterthan def play(myteam,yourteam): # self=myteam if random()<0.5: myteam.win() yourteam.lose() print("I win, you suck") else: myteam.lose() yourteam.win() print("You win, but you still suck") #play # end class team # Now we can create team objects (instances of class team): mets = team(162) # calls __init__, self = pointer to mets, totalgames = 16 yankees = team(162) yankees.win() # calls win, self = pointer to yankees yankees.lose() mets.lose() for i in range(20): yankees.play(mets) print("mets' winning percentage: ", mets.percentage()) print("yankees' winning percentage: %.2f" % yankees.percentage()) #formated print (w,l) = yankees.project() print("yankees' predicted record: ",w,"-",l) if yankees.betterthan(mets): print("yankees have better record") else: print("yankees are not better than mets") # Pay close attention to how the 'betterthan' method is defined and called. # I used 'myteam' instead of 'self' in the definition. What's important is # not the world "self" but the fact that 'myteam' is the first parameter # which always points to the object that the method operates on. In the # above call to betterthan, 'myteam' points to yankees and the remaining # parameter 'yourteam' points to mets. ### Another interesting point to notice: in the project method, I had to # calculate the value totolgames (total number of games in a season). # You cannot just use the totalgames variable from the __init__ function # because it's a local variable of that function. But then why isn't the # same true of the self variable? Isn't the self variable local to each # function? And if so, then wouldn't any changes made to it also be # in effect only locally? To understand this you need to remember what I # told you about pointers. Objects, like arrays (which are special kinds of # objects as well), are referred to using pointers (memory addresses). Yes, # each 'self' variable is local to each function, but they can all point to # the same object - the same collection of data - in memory. When I call # yankees.win() and then yankees.lose(), the 'self' variable in both the win # and lose functions both point to the object yankees. So changing self.wins # or self.losses will have an effect outside of the function. print("-------------------- An Abstract Example --------------------") # Examples of every-day objects such as bank accounts and baseball teams # may help you related to oop, but it's important to understand the # precise mechanisms that are driving the two programs above. So the # following example is completely abstract. class AA: def __init__(a,i): a.x = i a.y = 0 # constructor def f(s, j): s.x += j return s.x + s.y # method f def g(self): self.y = 1 # method g # class AA #### instances of class AA a1 = AA(0) # invokes constructor, a = a1, i = 0, a2 = AA(2) # creates an AA object (an instance of AA) a1.g() # invokes method g, self = a1 print(a1.f(1)) # invokes method f, s=a1, j = 1 print(a2.f(2)) # Each instance of the class AA contains two attributes: x and y. This # is indicated by the variables being instantiated inside the constructor # method. In each method, including the constructor, the first parameter # (a, s, self respectively) points to the object being operated on (the # "object in question"). The constructor takes one additional argument, # which must be passed in when the object is first created.