Boston Python workshop 2/Python classes


 * 1) INTRODUCTION TO CLASSES AND INHERITANCE ##
 * 1) INTRODUCTION TO CLASSES AND INHERITANCE ##


 * 1) ---> For questions, feel free to email jhamrick@mit.edu <---


 * 1) NOTE: This file intentionally contains some statements that will
 * 2) generate errors.  This is used to illustrate what things are and are
 * 3) not allowed.  If you try to import this file it will throw an
 * 4) AttributeError, so don't be alarmed!


 * 1) Data structures like lists or strings are extremely useful, but
 * 2) sometimes they aren't enough to represent something you're trying to
 * 3) implement in Python.  For example, let's say we needed to keep track
 * 4) of a bunch of pets.  We could represent a pet using a list, for
 * 5) example, by specifying the first element of the list as the pet's
 * 6) name and the second element of the list as the pet's species.  This
 * 7) is very arbitrary and nonintuitive, however -- how do you know which
 * 8) element is supposed to be which?


 * 1) Classes give us the ability to create more complicated data
 * 2) structures that contain arbitrary content.  We can create a Pet
 * 3) class that keeps track of the name and species of the pet in
 * 4) usefully named attributes 'name' and 'species', respectively.


 * 1) Before we get into creating a class itself, we need to understand an
 * 2) important distinction.  A class is something that just contains
 * 3) structure -- it defines how something should be laid out or
 * 4) structured, but doesn't actually fill in the content.  For example,
 * 5) a Pet class may say that a Pet needs to have a name and a species,
 * 6) but it will not actually say what the pet's name or species is.


 * 1) This is where instances come in.  An instance is a specific copy of
 * 2) the class that does contain all of the content.  For example, if I
 * 3) create a pet, 'polly', with name Polly and species Parrot, then
 * 4) 'polly' is an instance of 'Pet'.


 * 1) Another way to think about this is to think about a form you need to
 * 2) fill out for the government.  Let's say that there is a particular
 * 3) tax form that everybody has to fill out.  Everybody fills out the
 * 4) same type of form, but the content that people put into the form
 * 5) differs from person to person.  A class is like the form: it
 * 6) specifies what content should exist.  Your copy of the form with
 * 7) your specific information is like an instance of a class: it
 * 8) specifies what the content actually is.

class Pet(object):

# When we create a new pet, we need to specify what it's name is   # and what it's species is. 'self' is the instance of the class. # Remember that instances have the structure of the class but that # the values within an instance may vary from instance to   # instance. So, we want to specify that our instance (self) has # different values in it than some other possible instace. That # is why we say 'self.name = name' instead of 'Pet.name = name'. def __init__(self, name, species): self.name = name self.species = species

# We can also define methods to get the contents of the instance. # This method, getName, takes an instance of a Pet as a parameter # and looks up the pet's name. Again, we have to pass in the # 'self' parameter so that the function knows which instance of   # Pet to operate on: it needs to be able to find out the content.

# You'll see below that when we call the getName function we don't   # actually pass anything to it: we just do polly.getName or    # polly.getSpecies. Why don't we have to pass in the 'self' # parameter? This phenomena is a special behavior of Python: when # you call a method on an instance, Python automatically figures # out what 'self' should be (from the instance) and passes it to   # the function. An equivalent way of doing this would be:

# Pet.getName(polly)

# In this case, we don't do 'instance.getName', so Python can't   # automatically figure out what 'self' should be. Instead, we   # have to manually pass in the instance so that getName can # operate correctly.

def getName(self): return self.name

# getSpecies is similar to getName.

def getSpecies(self): return self.species

# This is a special function that is defined for all classes in   # Python. You can specify your own version of the function (known   # as "overriding" the function). By overriding the __str__ # function, we can define the behavior when we try to print the # instance using the 'print' keyword. For example, if we try to   # print polly:

# >>> print polly # Polly is a Parrot

def __str__(self): return "%s is a %s" % (self.name, self.species)


 * 1) Let's create some pets!

polly = Pet("Polly", "Parrot") ginger = Pet("Ginger", "Cat") clifford = Pet("Clifford", "Dog")

print polly.getName      # prints "Polly" print polly.getSpecies   # prints "Parrot" print polly                # prints "Polly is a Parrot"

print ginger.getName     # prints "Ginger" print ginger.getSpecies  # prints "Cat" print ginger               # prints "Ginger is a Cat"

print clifford.getName   # prints "Clifford" print clifford.getSpecies # prints "Dog" print clifford             # prints "Clifford is a Dog"


 * 1) Sometimes just defining a single class (like Pet) is not enough.
 * 2) For example, some pets are dogs and most dogs like to chase cats.
 * 3) Birds are also pets but they generally don't like to chase cats.  We
 * 4) can make another class that is Pet but is also specifically a Dog,
 * 5) for example: this gives us the structure from Pet but also any
 * 6) structure we specify for Dog.

class Dog(Pet):

# We want to specify that all Dogs have species "Dog", and also # whether or not the dog like to chase cats. To do this, we need # to define our own initialization function. We also need to call # the parent class initialization function, though, because we   # still want the 'name' and 'species' fields to be initialized. # If we did not have the 'Pet.__init__(self, name, "Dog")' line, # then we could still call the methods getName and getSpecies. # However, because Pet.__init__ was never called, the 'name' and # 'species' fields were never created, so calling getName or   # getSpecies would throw an error. def __init__(self, name, chases_cats): Pet.__init__(self, name, "Dog") self.chases_cats = chases_cats

def chasesCats(self): return self.chases_cats


 * 1) And similarly for cats...

class Cat(Pet): def __init__(self, name, hates_dogs): Pet.__init__(self, name, "Cat") self.hates_dogs = hates_dogs

def hatesDogs(self): return self.hates_dogs

mister_pet = Pet("Mister", "Dog") mister_dog = Dog("Mister", True)


 * 1) isinstance is a special function that checks to see if an instance
 * 2) is an instance of a certain type of class.  Here we can see that
 * 3) mister_pet is an instance of Pet, but not Dog, while mister_dog is
 * 4) an instance of both Pet and Dog.

print isinstance(mister_pet, Pet) # prints True print isinstance(mister_pet, Dog) # prints False print isinstance(mister_dog, Pet) # prints True print isinstance(mister_dog, Dog) # prints True


 * 1) Because mister_pet is a Pet, but not a Dog, we can't do this:

print mister_pet.chasesCats
 * 1) AttributeError: 'Pet' object has no attribute 'chasesCats'


 * 1) because the Pet class has no chasesCats method.  We can, however,
 * 2) call chasesCats on mister_dog, because it is defined for the Dog
 * 3) class:

print mister_dog.chasesCats # prints True


 * 1) Now let's create some cats and dogs.

fido = Dog("Fido", True) rover = Dog("Rover", False) mittens = Cat("Mittens", True) fluffy = Cat("Fluffy", False)

print fido   # prints "Fido is a Dog" print rover  # prints "Rover is a Dog" print mittens # prints "Mittens is a Cat" print fluffy # prints "Fluffy is a Cat"

print "%s chases cats: %s" % (fido.getName, fido.chasesCats)
 * 1) prints "Fido chases cats: True"

print "%s chases cats: %s" % (rover.getName, rover.chasesCats)
 * 1) prints "Rover chases cats: False"

print "%s hates dogs: %s" % (mittens.getName, mittens.hatesDogs)
 * 1) prints "Mittens hates dogs: True"

print "%s hates dogs: %s" % (fluffy.getName, fluffy.hatesDogs)
 * 1) prints "Fluffy hates dogs: False"


 * 1) To make the difference between classes and instances a little bit
 * 2) clearer, we can try to call a method on a class instead of on an
 * 3) instance: it doesn't work!  This is because these methods only know
 * 4) how to operate on an instance of the class, because only the
 * 5) instance contains the actual content.

print "All pets are %ss" % Pet.getSpecies
 * 1) TypeError: unbound method getSpecies must be called with Pet
 * 2) instance as first argument (got nothing instead)

print "All dogs are named %s" % Dog.getName
 * 1) TypeError: unbound method getName must be called with Dog instance
 * 2) as first argument (got nothing instead)