Boston Python workshop 2/Python classes

From OpenHatch wiki
Jump to navigation Jump to search

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