## Essential Notes on Classes and Objects Continued ... # The following is one of my favorite examples of a class: # class representing a fraction class fraction: def __init__(self,n,d): # initialize numerator and denominator self.n = n if d==0: raise "zero denominator error" self.d = d self.simplify() # can reference now? yes # constructor # method to return string representation of class: def tostring(self): return str(self.n)+"/"+str(self.d) # tostring # method to simplify fraction by calculating the gcd def simplify(self): a = self.n # rename vars for convenience b = self.d while (a!=0 and b!=0): #find gcd of a and b if (a>b): a = a%b else: b = b%a # while gcd = a+b # gcd is the number that's not zero self.n = self.n / gcd # simplify numerator and denominator self.d = self.d /gcd #simplify def multiply(self,B): # multiply this fraction with another fraction B n = self.n * B.n d = self.d * B.d newobject = fraction(n,d) return newobject # multiply: # is multiply a destructive or non-destructive function? (non-destructive) def add(self,B): # non-destructive addition of fractions return fraction(self.n*B.d+B.n*self.d, self.d*B.d) # The following is a destructive version of add: it mutates the object # that self points to: def addto(self,B): # destructive addition to self n,d = self.n, self.d self.n = n*B.d+B.n*d self.d = d*B.d # destructive operations usually don't return anything # ----------- operator overloading ---------------- def __mul__(self,B): # overrides the * operator return self.multiply(B) # __mul__ def dmult(self,B): # destructive version of multiply: changes self self.n = self.n * B.n self.d = self.d * B.d self.simplify() # destructive multiplication def __add__(A,B): n = A.n*B.d + B.n*A.d d = A.d * B.d return fraction(n,d) # add def __eq__(A,B): return A.n*B.d == A.d*B.n # equals # class fraction ##### f1 = fraction(1,2) f2 = fraction(2,4) f3 = fraction(2,8) print(f1) # don't expect to see 1/2 print(f1.tostring()) print(f1 == f2) # calls __eq__ method f4 = f1 + f3 # calls f1.add(f3) print(f4.tostring()) print(f4.tostring()) f4.addto(f1) # destructive change to f4 print(f4.tostring()) ############# # The above class illustrates a method called operator overloading: # The special names __eq__, __add__ overrides the meaning of == and + # on objects that are instances of fraction. Such operators for # most built-in classes including strings and arrays/lists, are already # overloaded. ### Summary: # In order to create a class of objects, I must first be clear as to: # 1. The attributes or "fields" of the object. That is, what are the # pieces of data that will make up the object. In the case of fractions, # these are the numerator and the denominator. # 2. What functions (methods) do I wish to be able to call on each object. # # Once I'm clear as to what needs to be done. I can now write a class # structure in Python. The first method I should write (usually) is the # "constructor" function, which must be called __init__. It is the job # of this function to assign initial values to the variables (fields) # representing each object. print("-------------- Class Excercise - Pun Intended -------------") # I want to have objects representing collections of coins (i.e., piggybanks). # The coin denominations are nickels, dimes, and quarters (we're snobs and # don't collect pennies). When the a coin-collection # object is first created, it will contain 0 nickels, 0 dimes and 0 quarters. # The methods that I want to call are # addquarter(), adddime(), addnickel(), totalworth(), # That is, the following code needs to be valid with your class: class piggybank: def __init__(self): # self.dimes = 0 # self.nickels = 0 # self.quarters = 0 self.total = 0 def addime(self): #self.dimes +=1 self.total += 10 # .. def totalworth(self): (n,d,q) = self.nckels, self.dimes, self.quarters return n*5 = d*10 + q*25 #class piggybank # mybank = piggybank() # mybank.adddime() # mybank.addquarter() # mybank.addnickel() # print(mybank.totalworth()) # should print 40 cents ## Now you're going to add another method to the class. You want to # *merge* the contents of two piggybanks into one: # yourbank = piggybank() # yourbank.addnickel() # yourbank.addquarter() # yourbank.merge(mybank) # add mybank's contents to yourbank # print(yourbank.totalworth()) # should print 70 cents # print(mybank.totalworth()) # should print 0 cents: you stole my money. ############################ ONE MORE TIME! ################################ #You were first introduced to the concept of pointers with respect to arrays. #Arrays are special kinds of objects. Objects in Python are also referenced #through pointers. This means that if I do: # mybank = yourbank # it does not duplicate the yourbank object, rather it would just set mybank # to the memory address of where yourbank is stored. It doesn't copy the # object. So remember, when you assign a variable to an object, the variable # is actually only assigned to the memory address of the object. class demo: def __init__(this,a): this.x = a #init #demo def f(x,D): x = 2 D.x = 2 #f x = 1 D = demo(1) f(x,D) print(x, D.x) # prints 1, then 2 # x stays 1 because the x inside the function f is a local x. The D inside # the function f is also local to f (because it's a parameter), but it # points to the same object that the D outside points to. So when D.x is # changed to 2, it affects the same object that the other D points to as well. # Had I tried to do the following inside f: D = demo(2). Then the D inside # f will be set to a different object, but the D outside will still point to # the original object, which hasn't been changed. In this case it will still # print 1 for D.x