#!/usr/bin/env python # coding: utf-8 #
# 
# 
# 
# 
# # # Object Oriented Programming (II) #
# 
# 
# 
# 
# 
# # # Classes have a bunch of special methods ### # the mirror of `__init__` is `__del__` # (it is the tear down during clean up) # In[ ]: class Bear: def __init__(self,name): self.name = name print(" made a bear called %s" % (name)) def __del__(self): print("Bang! %s is no longer." % self.name) # In[ ]: y = Bear("Yogi") ; c = Bear("Cal") # In[ ]: del y; del c # In[ ]: ## note that I'm assigning y twice here y = Bear("Yogi") ; y = Bear("Cal") # If we quit (not from the notebook but the interpreter or ipython) # # ```python # >>> f = Bear("Fuzzy") # ` made a bear called Fuzzy` # >>> exit() # `Bang! Fuzzy is no longer.` # BootCamp> # ``` #
# neither `__init__` or `__del__` are allowed to return anything #
# In[ ]: get_ipython().run_cell_magic('file', 'bear.py', 'import datetime\nclass Bear:\n logfile_name = "bear.log"\n bear_num = 0\n all_bear_names = []\n def __init__(self,name):\n self.name = name\n print((" made a bear called %s" % (name)))\n self.logf = open(Bear.logfile_name,"a")\n Bear.bear_num += 1\n self.my_num = Bear.bear_num\n self.logf.write("[%s] created bear #%i named %s\\n" % \\\n (datetime.datetime.now(),Bear.bear_num,self.name))\n self.logf.flush()\n \n def growl(self,nbeep=5):\n print(("\\a"*nbeep))\n\n def __del__(self):\n print(("Bang! %s is no longer." % self.name))\n self.logf.write("[%s] deleted bear #%i named %s\\n" % \\\n (datetime.datetime.now(),self.my_num,self.name))\n self.logf.flush()\n # decrement the number of bears in the population\n Bear.bear_num -= 1\n # dont really need to close because Python will do the garbage collection\n # for us. but it cannot hurt to be graceful here.\n self.logf.close()\n\n def __str__(self):\n return " name = %s bear number = %i (population %i)" % \\\n (self.name, self.my_num,Bear.bear_num)\n') # In[ ]: get_ipython().system('rm bear.log') get_ipython().run_line_magic('run', 'bear') # In[ ]: a = Bear("Yogi") # In[ ]: b = Bear("Yogi") # In[ ]: b = a # In[ ]: b = Bear("Fuzzy") # In[ ]: Bear.bear_num # In[ ]: del a; del b # In[ ]: Bear.bear_num # In[ ]: get_ipython().system('cat bear.log') # ### Classes have a bunch of special methods ### # # `__str__` is a method that defines how a Class should represent itself as a string # # it takes only self as an arg, must return a string # In[ ]: run bear # In[ ]: b = Bear("Fuzzy") # In[ ]: print(b) # In[ ]: a = Bear("Yogi") # In[ ]: print(a) # this is the kind of formatting that datetime() is doing in it's own `__str__` # In[ ]: get_ipython().run_cell_magic('file', 'bear2.py', 'import datetime\nclass Bear:\n logfile_name = "bear.log"\n bear_num = 0\n def __init__(self,name):\n self.name = name\n print(" made a bear called %s" % (name))\n self.logf = open(Bear.logfile_name,"a")\n Bear.bear_num += 1\n self.created = datetime.datetime.now()\n self.my_num = Bear.bear_num\n self.logf.write("[%s] created bear #%i named %s\\n" % \\\n (datetime.datetime.now(),Bear.bear_num,self.name))\n self.logf.flush()\n \n def growl(self,nbeep=5):\n print("\\a"*nbeep)\n\n def __del__(self):\n print("Bang! %s is no longer." % self.name)\n self.logf.write("[%s] deleted bear #%i named %s\\n" % \\\n (datetime.datetime.now(),self.my_num,self.name))\n self.logf.flush()\n # decrement the number of bears in the population\n Bear.bear_num -= 1\n # dont really need to close because Python will do the garbage collection\n # for us. but it cannot hurt to be graceful here.\n self.logf.close()\n\n def __str__(self):\n age = datetime.datetime.now() - self.created\n return " name = %s bear (age %s) number = %i (population %i)" % \\\n (self.name, age, self.my_num,Bear.bear_num)\n') # In[ ]: # add some dynamic aging to the bears from bear2 import Bear as Bear2 # In[ ]: a = Bear2("Yogi") # In[ ]: print(a) # In[ ]: print(a) # ### Emulating Numeric operations ### # you can define a whole bunch of ways that instances behave upon numerical operation # # (e.g., `__add__` is what gets called when you type instance_1 + instance_2) #
# 
# __add__(self, other)
# 
# __sub__(self, other)
# 
# __mul__(self, other)
# 
# __div__(self, other)
# 
# __mod__(self, other)
# 
# __divmod__(self, other)
# 
# __pow__(self, other[, modulo])
# 
# __lshift__(self, other)
# 
# __rshift__(self, other)
# 
# __and__(self, other)
# 
# __xor__(self, other)
# 
# 
# In[ ]: get_ipython().run_cell_magic('file', 'bear1.py', '\nclass Bear:\n """\n class to show off addition (and multiplication)\n """\n bear_num = 0\n def __init__(self,name):\n self.name = name\n print(" made a bear called %s" % (name))\n Bear.bear_num += 1\n self.my_num = Bear.bear_num\n\n def __add__(self,other):\n ## spawn a little tike\n cub = Bear("progeny_of_%s_and_%s" % (self.name,other.name))\n cub.parents = (self,other)\n return cub\n\n def __mul__(self,other):\n ## multiply (as in "go forth and multiply") is really the same as adding\n return self.__add__(other)\n') # In[ ]: from bear1 import Bear as Bear1 # In[ ]: y = Bear1("Yogi") ; c = Bear1("Fuzzy") # In[ ]: our_kid = y + c # In[ ]: our_kid. # In[ ]: our_kid.parents # In[ ]: our_kid.parents[0].name # In[ ]: our_kid.parents[1].name # In[ ]: y.broken_limb = True # In[ ]: our_kid1 = y * c # ### Other Useful Specials ### #
# __dict__ : Dictionary containing the class's namespace.
# __doc__ : Class documentation string, or None if undefined. 
# __name__: Class name. 
# __module__: Module name in which the class is defined. This attribute is "__main__" in interactive mode. 
# __bases__ : A possibly empty tuple containing the base classes, in the order of their occurrence in the base class list. 
# 
# In[ ]: print(Bear1.__doc__) print(Bear1.__name__) print(Bear1.__module__) print(Bear1.__bases__) print(Bear1.__dict__) # ## "Hiding" class data attributes # In[ ]: class JustCounter: __secretCount = 0 x = 0 def count(self): self.__secretCount += 1 self.x += 1 print(self.__secretCount) # In[ ]: counter = JustCounter() # In[ ]: counter.count() ; counter.count() # In[ ]: print(counter.x) # In[ ]: print(counter.__secretCount) # no attribute is ever precisely private # # double underscore attributes are exposed as `object._className__attrName` # In[ ]: counter._JustCounter__secretCount # ### A note on referencing... ### # In[ ]: a = Bear("Yogi") # In[ ]: a # In[ ]: b = a # In[ ]: b # In[ ]: a.name = "Fuzzy" # In[ ]: b.name # In[ ]: Bear.bear_num # In[ ]: import copy # In[ ]: c = copy.copy(a) # In[ ]: c # new memory location # In[ ]: Bear.bear_num # In[ ]: c.name # In[ ]: c.name = "Smelly" # In[ ]: a.name # In[ ]: a.mylist = [1,2,3] # In[ ]: b.mylist # In[ ]: c.mylist # In[ ]: d = copy.copy(a) d.mylist # In[ ]: d.name # In[ ]: d.name = "Yogi" # In[ ]: a.name # In[ ]: a.mylist[0] = -1 # In[ ]: d.mylist # In[ ]: e = copy.deepcopy(a) # In[ ]: a.__dict__ # In[ ]: del a.logf # In[ ]: e = copy.deepcopy(a) # In[ ]: a.mylist[0] = "a" # In[ ]: e.mylist # In[ ]: Bear.bear_num # `deepcopy`: copies all attributes pointed to internally # # Subclassing & Inheritance # # `class classname(baseclass):` # # For example, # `class Flower(Plant):` # # Here we say that # "the class Flower is a subclass of the base class Plant." # Plant may itself be a subclass of LivingThing # # # attributes of the baseclass are inherited by the subclass # # In[ ]: class Plant: num_known = 0 def __init__(self,common_name,latin_name=None): self.latin_name = latin_name self.common_name = common_name Plant.num_known += 1 # In[ ]: class Flower(Plant): has_pedals = True # In[ ]: p = Plant("poison ivy") e = Flower("poppy") # In[ ]: e.has_pedals # In[ ]: Plant.num_known # In[ ]: Flower.__bases__[0].__name__ # instantiation of a Flower reuses the `__init__` from the Plant class. It also sets has pedals = `True` # In[ ]: class Plant: num_known = 0 def __init__(self,common_name,latin_name=None): self.latin_name = latin_name self.common_name = common_name Plant.num_known += 1 def __str__(self): return "I am a plant (%s)!" % self.common_name class Flower(Plant): has_pedals = True def __str__(self): return "I am a flower (%s)!" % self.common_name # now the `__str__` method of Flower takes precedence over the `__str__` method of the parent class # In[ ]: f = Flower("rose") ; print(f) # In[ ]: p = Plant("oak"); print(p) # In[ ]: class Flower(Plant): has_pedals = True def __init__(self,common_name,npedals=5,pedal_color="red",latin_name=None): ## call the __init__ of the parent class Plant.__init__(self,common_name,latin_name=latin_name) self.npedals = npedals self.pedal_color = pedal_color def __str__(self): return "I am a flower (%s)!" % self.common_name # we can still use the parent class' `__init__` # In[ ]: f = Flower("rose") ; print(f) # In[ ]: class A: def __init__(self): print("A") class B(A): def __init__(self): A.__init__(self) print("B") # In[ ]: a = A() # In[ ]: b = B() # ## Multiple Inheritances # # `class Flower1(Plant,EdibleFood,SmellyStuff)` # # when executing a method the namespace of: # # - `Flower1` is searched first # - `Plant` second (and it's baseclasses...and their baseclasses) # - `EdibleFood` second (and it's baseclasses...and their baseclasses) # - `SmellyStuff` second (and it's baseclasses...and their baseclasses) # ## Errors (& Handling) # # #### There are many different kinds of exceptions that can be raised by an error and each of them can be handled differently... # In[ ]: 3.1415/0 # In[ ]: def this_fails(): x = 3.1415/0 try: this_fails() except ZeroDivisionError as detail: print('Handling run-time error:',detail ) # `import exceptions` # http://docs.python.org/library/exceptions.html # #
# exception BaseException
# exception Exception
# exception StandardError
# exception ArithmeticError
# exception LookupError
# exception EnvironmentError
# exception AssertionError
# exception AttributeError
# exception EOFError
# exception GeneratorExit
# exception IOError
# exception ImportError
# exception IndexError
# exception KeyError
# exception KeyboardInterrupt
# exception MemoryError
# exception NameError
# exception OverflowError
# exception ReferenceError
# exception RuntimeError
# exception StopIteration
# exception SyntaxError
# exception SystemError
# exception SystemExit
# exception TypeError
# exception ValueError
# exception VMSError
# exception WindowsError
# exception ZeroDivisionError
# exception Warning
# exception UserWarning
# exception DeprecationWarning
# exception PendingDeprecationWarning
# exception SyntaxWarning
# exception RuntimeWarning
# exception FutureWarning
# exception ImportWarning
# 
# In[ ]: def divide(x, y): try: result = x / y except ZeroDivisionError: print("division by zero!") else: print("result is", result) finally: print("executing finally clause") # In[ ]: divide(2,1) # In[ ]: divide(2,0) # In[ ]: divide("2","1") # ## Catch Multiple Error Types # In[ ]: get_ipython().run_cell_magic('file', 'catcherr.py', 'import sys\ntry:\n f = open(\'myfile.txt\')\n s = f.readline()\n i = int(s.strip())\nexcept IOError as err:\n print("I/O error: %s" % err)\nexcept ValueError:\n print("Could not convert data to an integer.")\nexcept:\n print("Unexpected error:", sys.exc_info()[0])\n raise\n') # > exc_info(...) # # > exc_info() -> (type, value, traceback) # # Return information about the most recent exception caught by an except # clause in the current stack frame or in an older stack frame. # In[ ]: run catcherr # ## Raising Errors # # we can raise errors in our codes (which themselves might be caught upstream) # In[ ]: a = "cat food" if a != "spam": raise NameError("anything that isn't spam breaks my code") # ## Errors are a family of classes (and subclasses)! #
# BaseException
#  +-- SystemExit
#  +-- KeyboardInterrupt
#  +-- GeneratorExit
#  +-- Exception
#       +-- StopIteration
#       +-- StandardError
#       |    +-- BufferError
#       |    +-- ArithmeticError
#       |    |    +-- FloatingPointError
#       |    |    +-- OverflowError
#       |    |    +-- ZeroDivisionError
#       |    +-- AssertionError
#       |    +-- AttributeError
#       |    +-- EnvironmentError
#       |    |    +-- IOError
#       |    |    +-- OSError
#       |    |         +-- WindowsError (Windows)
#       |    |         +-- VMSError (VMS)
#       |    +-- EOFError
#       |    +-- ImportError
#       |    +-- LookupError
#       |    |    +-- IndexError
#       |    |    +-- KeyError
#       |    +-- MemoryError
#       |    +-- NameError
#       |    |    +-- UnboundLocalError
#       |    +-- ReferenceError
#       |    +-- RuntimeError
#       |    |    +-- NotImplementedError
#       |    +-- SyntaxError
#       |    |    +-- IndentationError
#       |    |         +-- TabError
#       |    +-- SystemError
#       |    +-- TypeError
#       |    +-- ValueError
#       |         +-- UnicodeError
#       |              +-- UnicodeDecodeError
#       |              +-- UnicodeEncodeError
#       |              +-- UnicodeTranslateError
#       +-- Warning
#            +-- DeprecationWarning
#            +-- PendingDeprecationWarning
#            +-- RuntimeWarning
#            +-- SyntaxWarning
#            +-- UserWarning
#            +-- FutureWarning
# 	   +-- ImportWarning
# 	   +-- UnicodeWarning
# 	   +-- BytesWarning 
# 
# ## We can create our own exception classes by subclassing others (e.g., Exception) # In[ ]: import datetime # In[ ]: class MyError(StopIteration): def __init__(self,value=None): ## call the baseclass Exception __init__ Exception.__init__(self) self.value = value print("exception with %s at time %s" % (self.value,datetime.datetime.now())) def __str__(self): return "you said %s" % self.value # In[ ]: raise MyError("darnit") # In[ ]: get_ipython().run_line_magic('debug', '') # In[ ]: get_ipython().run_line_magic('pdb', '') # In[ ]: