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)

>>> 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 [ ]:
%%file bear.py
import datetime
class Bear:
    logfile_name = "bear.log"
    bear_num     = 0
    all_bear_names = []
    def __init__(self,name):
        self.name = name
        print((" made a bear called %s" % (name)))
        self.logf  = open(Bear.logfile_name,"a")
        Bear.bear_num += 1
        self.my_num = Bear.bear_num
        self.logf.write("[%s] created bear #%i named %s\n" % \
                        (datetime.datetime.now(),Bear.bear_num,self.name))
        self.logf.flush()
    
    def growl(self,nbeep=5):
        print(("\a"*nbeep))

    def __del__(self):
        print(("Bang! %s is no longer." % self.name))
        self.logf.write("[%s] deleted bear #%i named %s\n" % \
                        (datetime.datetime.now(),self.my_num,self.name))
        self.logf.flush()
        # decrement the number of bears in the population
        Bear.bear_num -= 1
        # dont really need to close because Python will do the garbage collection
        #  for us. but it cannot hurt to be graceful here.
        self.logf.close()

    def __str__(self):
        return " name = %s bear number = %i (population %i)" % \
              (self.name, self.my_num,Bear.bear_num)
In [ ]:
!rm bear.log
%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 [ ]:
!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 [ ]:
%%file bear2.py
import datetime
class Bear:
    logfile_name = "bear.log"
    bear_num     = 0
    def __init__(self,name):
        self.name = name
        print(" made a bear called %s" % (name))
        self.logf  = open(Bear.logfile_name,"a")
        Bear.bear_num += 1
        self.created = datetime.datetime.now()
        self.my_num = Bear.bear_num
        self.logf.write("[%s] created bear #%i named %s\n" % \
                        (datetime.datetime.now(),Bear.bear_num,self.name))
        self.logf.flush()
    
    def growl(self,nbeep=5):
        print("\a"*nbeep)

    def __del__(self):
        print("Bang! %s is no longer." % self.name)
        self.logf.write("[%s] deleted bear #%i named %s\n" % \
                        (datetime.datetime.now(),self.my_num,self.name))
        self.logf.flush()
        # decrement the number of bears in the population
        Bear.bear_num -= 1
        # dont really need to close because Python will do the garbage collection
        #  for us. but it cannot hurt to be graceful here.
        self.logf.close()

    def __str__(self):
        age = datetime.datetime.now() - self.created
        return " name = %s bear (age %s) number = %i (population %i)" % \
                (self.name, age, self.my_num,Bear.bear_num)
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 [ ]:
%%file bear1.py

class Bear:
    """
    class to show off addition (and multiplication)
    """
    bear_num = 0
    def __init__(self,name):
        self.name = name
        print(" made a bear called %s" % (name))
        Bear.bear_num += 1
        self.my_num = Bear.bear_num

    def __add__(self,other):
        ## spawn a little tike
        cub = Bear("progeny_of_%s_and_%s" % (self.name,other.name))
        cub.parents = (self,other)
        return cub

    def __mul__(self,other):
        ## multiply (as in "go forth and multiply") is really the same as adding
        return self.__add__(other)
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 [ ]:
%%file catcherr.py 
import sys
try:
    f = open('myfile.txt')
    s = f.readline()
    i = int(s.strip())
except IOError as err:
    print("I/O error: %s" % err)
except ValueError:
    print("Could not convert data to an integer.")
except:
    print("Unexpected error:", sys.exc_info()[0])
    raise

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 [ ]:
%debug
In [ ]:
%pdb
In [ ]: