Boston Python workshop 2/Python classes

From OpenHatch wiki

Staff member Jessica Hamrick wrote up an excellent document to motivate and explain Python classes. Read through it here or copy it into a file to run and experiment with it.

This tutorial also exists in blog-post form.


#############################################
## INTRODUCTION TO CLASSES AND INHERITANCE ##
#############################################

# ---> For questions, feel free to email jhamrick@mit.edu <---

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

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

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

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

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

# Another way to think about this is to think about a form you need to
# fill out for the government.  Let's say that there is a particular
# tax form that everybody has to fill out.  Everybody fills out the
# same type of form, but the content that people put into the form
# differs from person to person.  A class is like the form: it
# specifies what content should exist.  Your copy of the form with
# your specific information is like an instance of a class: it
# 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)


# 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"

# Sometimes just defining a single class (like Pet) is not enough.
# For example, some pets are dogs and most dogs like to chase cats.
# Birds are also pets but they generally don't like to chase cats.  We
# can make another class that is Pet but is also specifically a Dog,
# for example: this gives us the structure from Pet but also any
# 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

# 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)

# isinstance is a special function that checks to see if an instance
# is an instance of a certain type of class.  Here we can see that
# mister_pet is an instance of Pet, but not Dog, while mister_dog is
# 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

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

# AttributeError: 'Pet' object has no attribute 'chasesCats'
print mister_pet.chasesCats()

# because the Pet class has no chasesCats() method.  We can, however,
# call chasesCats() on mister_dog, because it is defined for the Dog
# class:

print mister_dog.chasesCats() # prints True


# 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"

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

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

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

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


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

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

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