Inheritance

The purpose of this lab is to give you practice in creating a base class and many child (derived) classes that inherit the methods and fields of a base class. In addition, we'll be modifying the L-system class to enable it to use rules that include multiple possible replacement strings and choose them stochastically (randomly).

Adding the capability to incorporate different replacements will enable you to draw yet more complex L-system structures where each tree is unique. We'll continue to use material from the ABOP book.


Tasks

This week we'll modify the lsystem.py and turtle_interpreter.py files and create a new shapes.py file that implements shapes other than trees. Please follow the naming conventions for classes and methods. We'll provide some test code that expects particular classes, methods, and parameter orderings.

  1. Create a new working folder. Copy your lsystem.py and turtle_interpreter.py files from your prior assignment (version 2). This week we are writing version 3 of both files. Make sure a comment at the top of the file indicates they are version 3.
  2. The change to the Lsystem class is to enable it to use rules with multiple possible replacements. During replacement, the particular replacement string is chosen randomly. There are a number of methods requiring modification: __init__, read, replace, and addRule. In addition, you'll need to import the random package.

    An example of an L-system with multiple possible replacements is given below. Note that the rule has the symbol F followed by three separate replacement strings, separated by spaces.

    base F
    rule F F[+F]F[-F]F F[+F]F F[-F]F
    

    To make handling multiple rules easier, we're going to make use of Python dictionaries. A dictionary is an implementation of a lookup table. Each entry in a dictionary has a key (like a word) and a value (like a definition). To create an empty dictionary we use curly brackets instead of square brackets like a list.

    myDict = {}

    To add a new entry to a dictionary, we index the dictionary with the key and make an assignment. The following creates an entry in the dictionary for the string 'F'. The value of the entry is a list containing the string 'FF'.

    myDict['F'] = [ 'FF' ]

    In the __init__ method, change the initialization of the self.rules field to be an empty dictionary instead of an empty list.

  3. Remove the getRule method. It no longer makes sense because rules are not stored via indices.
  4. In the addRule method we need to redo the function. We no longer want to append a 2-element list to self.rules. Instead, we want to make an assignment into the dictionary with a new key. The value should be a list of all the possible replacments for a symbol.

    Consider the process from file to dictionary. For example, the line below is a possible rule for a tree.

    rule F F[+F] F[-F] F[+F][-F]

    After reading the line and breaking it into strings using split, the words variable in the read method would be the following.

    [ 'rule', 'F', 'F[+F]', 'F[-F]', 'F[+F][-F]' ]

    If we pass words[1:] into addRule as the rule argument, then the rule variable will be:

    [ 'F', 'F[+F]', 'F[-F]', 'F[+F][-F]' ]

    The symbol is the first item in the list (rule[0]) and the set of replacements is the remainder of the list (rule[1:]). Modify the addRule method so that it assigns the list of replacements (rule[1:]) to the self.rules dictionary with the symbol (rule[0]) as the key.

  5. In the applyRules function we need to make use of the dictionary to build up the string. Using a dictionary is much easier than using a list of rules because we don't have to search for a rule matching the key. Instead, we just ask if the key exists in the dictionary. If it does, we pick a random replacement from the list of replacements and add it to the output string. Otherwise, we add the character itself to the output string.
        def applyRules( self, inputString ):
            # assign to a local variable (e.g. resultString) the empty string
            # for each character, char, in the input
                # if char is in the self.rules dictionary
                    # add to resultString a random choice from the dictionary entry self.rules[char]
                # else
                    # add to resultString the character char
            # return resultString
    
  6. Because we are using mutators to update the information in the object, the read method should not change, so long as you are passing to self.addRule the argument words[1:], which passes everything but the first item in the words list.

    When you're done with these changes, try running one of the following L-systems with just one or two iterations. Do it twice and see if you get the same string both times.

  7. Make three changes to the TurtleInterpreter class.

    First, just after the class TurtleInterpreter: line, and inside the class, assign False to a variable called initialized. This variable is what is called a class global. Only one copy of it exists for all of the objects in the class. We're going to use it to keep track of whether the turtle window has been initialized.

    After created the class variable, add the following three lines to the beginning of the __init__ method. Note that we have to use the class name to access a class variable.

            if TurtleInterpreter.initialized:
                return
            TurtleInterpreter.initialized = True
    

    Now the init function will not recreate the turtle window if it already exists.

    Second, add two cases to draw string. For the character '{' call turtle.fill(True), and for the character '}' call turtle.fill(False).

    Third, if you don't already have it, create a method color(self, c) that sets the turtle's color to the value in c.

    When you're done, download and run the following test function. You should get something like the following, but with randomly selected colors.

  8. We're now going to start building a system that makes use of our turtle interpreter class to build shapes that are not necessarily the output of L-systems. Just as we made functions in the second project to draw shapes, now we're going to make classes that create shape objects.

    Create a new file shapes.py. In it, create a class called Shape. It is a base class, from which other classes will be derived. Then define an init method with the following definition.

        def __init__(self, distance = 100, angle = 90, color = (0, 0, 0), istring = '' ):
            
            # create a field of self called distance and assign it distance
            # create a field of self called angle and assign it angle
            # create a field of self called color and assign it color
            # create a field of self called string and assign it istring
    
  9. Create the following mutator methods.
  10. Create a method draw that executes the following algorithm.
        def draw(self, xpos, ypos, scale=1.0, orientation=0):
            # create a TurtleInterpreter object
            # have the TurtleInterpreter object place the turtle at (xpos, ypos, orientation)
            # have the TurtleInterpreter object set the turtle color to self.color
            # have the TurtleInterpreter object draw the string
            #    Note: use the distance, angle, and string fields of self
            #    Note: multiply the distance by the scale parameter of the function
    
  11. In the same file, but below the Shape class definition, begin a new class Square that is derived from Shape.

    class Square(Shape):

    Have the __init__() function of the Square class take two optional arguments: distance and color. The __init__() function should do the following.
        def __init__(self, distance=100, color=(0, 0, 0) ):
            # call the parent's __init__ function with distance, 
            #      an angle of 90, color, and the string 'F-F-F-F-'
    
  12. In the same file, below the Square class, create a new class called Triangle that is the same as the Square class, but sets the string to 'F-F-F-' and sets the angle to 120.
  13. Once you have completed the Square and Triangle classes, download the second test function and run it. You should get the output below.