# Object Oriented Programming (OOP) in Python. # We've seen code such as 'A.append(x)', brush.draw_line(..), fd.close(), # 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. And 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 relies on a technique # called dynamic dispatch. # Time for an 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: # myaccount = account("prof essor",1000) # youraccount = account("stu dent",2000) # create an account for you too. # 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: # The *constructor* function: def __init__(self,name,initbalance): self.owner = name # initialize data fields of the object self.balance = initbalance # 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 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 myaccount = account("chuck liang",1000) youraccount = account("stu dent",2000) # create an account for you too. 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) # he he he ... print youraccount.owner, "has a balance of ", youraccount.inquiry() account.deposit(myaccount,50) # equivalent to myaccount.deposit(50) # The account 'class' is a template or blueprint for creating account objects. # The class basically contains a series of function (method) # definitions. The most important of these functions is __init__. This # function, which MUST be called __init__ (two underscores on each side), # is called the "constructor" of the class. This the function that's called # when you say myaccount = account("my 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* (variables) of the object (owner and # balance). You can think of the object as just a collection of variables. # Every account object contains a 'balance' variable and a 'owner' variable. # When I call account("stu dent",2000), the 'self' value is constructed # automatically. The string "stu dent" is passed to the parameter 'name' and # and 2000 is passed to the parameter 'initbalance'. # 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 correct 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 infered 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. # *** 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'. print "--------- Another Example of a Class and Objects ---------" # Recall that earlier in the semester we wrote a program that added # minutes and seconds. Now we can encapsulate both values inside a # class. class time: def __init__(self,m,s): # constructor sets intial time self.min = m self.sec = s # __init__ def reset(self): self.min, self.sec = 0,0 #reset # destructive addition of minutes and seconds: def add(self,m,s): tsec = self.sec + s self.min += m + (tsec/60) self.sec = tsec % 60 #time # version of add that accepts one additional parameter: def sum(self,other): tsec = self.sec + other.sec self.min += other.min + (tsec/60) self.sec = tsec % 60 # sum # non-destructive version of add, returns a new time object: def cadd(self,other): tsec = self.sec + other.sec m = self.min + other.min + (tsec/60) s = tsec % 60 newtime = time(m,s) return newtime # constructive addition ### defining a function in terms of other functions def tick(self): # destructive increase of one second self.add(0,1) # tick def equals(self,other): return self.min == other.min and self.sec == other.sec # == : recommend against overloading __eq__ ### OPERATOR OVERLOADING: def __add__(self,other): # overloads + tsec = self.sec + other.sec m = self.min + other.min + (tsec/60) s = tsec % 60 newtime = time(m,s) return newtime # constructive addition def __cmp__(self,other): # overloads ==, <, >=, etc ... sec1 = self.min*60 + self.sec sec2 = other.min*60 + other.sec return sec1-sec2 # cmp returns negative if selfother, 0 otherwise def tostring(self): return str(self.min)+"m"+str(self.sec)+"s" # tostring # time t1 = time(3,0) # 3 minutes and 0 seconds t2 = time(2,59) # 2 minutes and 59 seconds t1.sum(t2) # t1 becomes 5 minues and 59 seconds t3 = t1+t2 # '+' here actually refers to the __add__ function # same as t3 = t1.cadd(t2), t1, t2 not changed. print t3.tostring() t2.tick() print t2 < t3 # prints true, '<' refers to __cmp__ #### # 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 giants and the remaining # parameter 'yourteam' points to jets. ### 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 # giants.win() and then giants.lose(), the 'self' variable in both the win # and lose functions both point to the object giants. 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 football teams # may help motivate why we use 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.