"""
 barnyard.py

 An example illustrating python objects including the concepts of

   * class variables             Animal.population, one for whole class
   * instance variables          self.name, self.sound for each animal
   * "special" __str__ method    str() output ; how the class prints
   * class inheritance           "isa" relationship : "A Dog is a Animal."
   * a container class           a class with instances of another within

 tests
 -----

   >>> spot = Dog(name='Spot')
   >>> spot.speak()
   "A dog named 'Spot' says woof."
   >>> print(spot)
   Dog(name='Spot')

   >>> Cat().speak()
   'A cat says meow.'

   $ python --version
   python 3.5.2

 running it
 ----------

   $ python barnyard.py 
   The barnyard has 5 animals :
   A dog says woof.
   A cat says meow.
   A dog says woof.
   A dog says woof.
   A cat named 'Mr. Mistoffelees' says meow.

 Jim Mahoney | cs.marlboro.college | Oct 2018 | MIT License
"""

class Animal:
    """ The parent class for any sort of animal. """

    # population is class variable - its full name is Animal.population.
    # Note that there is only one of these for the whole class,
    # not one per animal as is the case with instance variables.
    population = 0

    def __init__(self, name=''):
        self.name = name
        Animal.population += 1
        self.initialize()

    def initialize(self):
        """ Change this method for each particular animal ! """
        self.sound = '?'
        
    def __str__(self):
        """ Return the string representation of this animal. """
        if self.name:
            naming = "name='{}'".format(self.name)
        else:
            naming = ''
        return '{}({})'.format(self.__class__.__name__, naming)

    def speak(self):
        """ Return a string describing what this animal says. """
        if self.name:
            named = "named '{}' ".format(self.name)
        else:
            named = ''
        return 'A {} {}says {}.'.format(self.__class__.__name__.lower(),
                                        named, self.sound)

class Dog(Animal):
    """ A dog says woof. """
    def initialize(self):
        self.sound = 'woof'

class Cat(Animal):
    """ A cat says meow. """
    def initialize(self):
        self.sound = 'meow'

class Zoo:
    """ A bunch of animals that can each speak in turn. """
    
    def __init__(self):
        Animal.population = 0    # The doc tests add extra animals ...
        self.animals = []
        
    def add(self, animal):
        self.animals.append(animal)
        
    def print_chorus(self):
        """ Let the animals speak ... """
        for animal in self.animals:
            print(animal.speak())
        
def main():
    barnyard = Zoo()
    
    for creature in (Dog, Cat, Dog, Dog):          # creature is a class ...
        barnyard.add( creature() )                 # ... so this creates one!
        
    barnyard.add( Cat(name='Mr. Mistoffelees') )   # And one more, with a name.
    
    print('The barnyard has {} animals :'.format(Animal.population))
    barnyard.print_chorus()

if __name__ == '__main__':
    import doctest
    doctest.testmod()
    main()