Lab Exercise 8:
Classes

The purpose of this lab is to give you practice in creating your own classes. In particular, we will convert both the lsystem and the interpreter modules we built last week into classes. Ultimately, classes make it easier to build a more complex system.

The other piece you will be implementing this week is handling multiple-rule L-systems. Adding this capability will enable you to draw more complex L-system structures.

If you have not already done so, take a look at the book The Algorithmic Beauty of Plants, which is the basis for this series of lab exercises. You can download the entire book from the algorithmic botany site.

Tasks

Most of the exercises in lab will involve building up an L-system class. This week it will be important for you to use the method names provided because the next several labs will expect the L-system and Interpreter classes to have certain methods with particular names.

You have been using objects throughout the semester (graphics objects, file objects, etc). Classes are type definitions that encapsulate both data (fields) and operations (methods). An object is an instance of a class, like a variable is an instance of a data type. You invoke (call) methods on objects using the form myObject.method().

When you define a class, the self keyword refers to the object instance on which a method has been invoked. Said another way, self is a reference to the object that is executing method code at runtime. Python highlights this by requiring self to be the first parameter to all methods defined for a class. You do not, however, specify this argument when calling a method. Instead, the object on which you are invoking the method is understood to be this first argument.

As we did last week, we will split the tasks up into building an L-system evaluator and an interpreter.

lsystem.py

  1. Create a new working folder and a new lsystem.py file. Label this one version 2 in the comments at the top of the file. The lsystem.py file will hold the Lsystem class and a test function for it.
  2. Begin a class called Lsystem. A class has a simple structure. It begins with the class declaration, and all methods are indented relative to the class definition, similar to a function. This example code begins the class definition and provides a template for what the function should do; it is explained in more detail below.
    class Lsystem:
        def __init__( self, filename=None ):
            # assign to the field base an empty string
            self.base = ''
            
            # assign to the field rules an empty list
            
            # if the filename variable is not None
                # call the read method of self with filename as the argument
    

    The __init__() method (function defined in a class) is executed when a Lsystem object is created. The init method should set up the fields of the object and give them reasonable initial values. Init methods often accept optional parameters. The Lsystem init function should have two arguments: self, and an optional filename. You can make a parameter optional by assigning a value to it in the parameter list of the function declaration).

    The Lsystem init should create two fields: base and rules. The field base should be initialized to the empty string. The field rules should be initialized to an empty list. To create a field of a class, put the prefix self. in front of the name of the field and assign something to it. The example code above creates the field base and assigns it the empty string.

    If the function receives a filename, it should read Lsystem information from the file by calling the read() method of self (which you'll create below), passing the filename as the argument: self.read( filename )

  3. Create mutator and accessor methods for the base string: setBase(self, newbase), getBase(self). The setBase function should assign to self's base field the value newbase. The getBase function should return self's base field.
  4. Create an accessor method getRule(self, index) that returns the single rule with the specified index from the rules field of self. Then create the method addRule(self, newrule), which should add a copy of newrule to the rules field of self. Look at the version 1 lsystem.py file if you need to remember how to write the method.
  5. Create a read(self, filename) method that opens the file, reads in the Lsystem information, resets the base and rules fields of self, and then stores the information from the file in the appropriate fields using the accessors self.setBase() and self.addRule(). You can refer to the createFromFile function code from the version 1 lsystem.py file, but it will require some modification. For examle, you don't need to create a new Lsystem (self already exists) and you will need to use the accessor methods. You can use the following template:
    def read( self, filename ):
        # assign to a variable (e.g. fp) the file object created with filename in read mode
        # assign to a variable (e.g. lines) the list of all lines in the file
        # close the file
    
        # for each element in the lines list
            # assign to a variable (e.g. words) the loop variable split on spaces
            # if the first item in words is equal to the string 'base'
                # call the setBase method of self with the new base string
            # else if the first item in words is equal to the string 'rule'
                # call the addRule method of self with the new rule
    
  6. To handle multiple rules we need to write our own replace method for an L-system. The indented algorithm template is below. We scan through the string, and for each character we test if there is a rule. If so, we add the replacement to a new string, otherwise we add the character itself to the new string.
    def replace(self, inString):
        # assign to a local variable (e.g. outString) the empty string
        # for each character c in the input string (inString)
            # set a local variable (e.g. found) to False
            
            # for each rule in the rules field of self
                # if the symbol in the rule is equal to the character
                    # add to outString the replacement from the rule
                    # set found to True
            
            # if not found
                # add to outString the character c
                
        # return outString
    

    Note that this method takes the place of the str.replace() method we used in the buildString() function last week. The approach from last week only handles single-rule L-systems.

  7. Ceate a buildString(self, iterations) function. It will be almost identical to the buildString function from version 1. The code outline is below.
    def buildString(self, iterations):
        # assign to a local variable (e.g. newString) value returned from the getBase() method of self
        # for the number of iterations
            # assign to newString the result of calling the replace() method of self passing in newString
        # return newString
    
  8. Use the following main() function to test your program so far. Be sure to import sys at the top of your file.
    def main(argv):
        if len(argv) < 4:
            print 'Usage: python lsystem.py filename iterations outputFile'
            exit()
    
        # parse command line arguments
        filename = argv[1]
        iterations = int(argv[2])
        outfile = argv[3]
    
        # create lsystem object
        lsys = Lsystem()
        lsys.read( filename )
        print lsys
    
        # build string from lsystem object
        lstr = lsys.buildString( iterations )
    
        # write result to a file
        fp = file( outfile, 'w' )
        fp.write(lstr + '\n')
        fp.close()
    
    if __name__ == "__main__":
        main(sys.argv)
    

    You can download any of the following examples and evaluate them as we did last week:

    $ python lsystem.py systemA.txt 3 output.txt
    

interpreter.py

  1. Create a new file called interpeter.py. Label it version 2 in the comments at the top of the file. You'll want to import the turtle package and probably the random and sys packages as well. Begin the class definition for a class called Interpreter:
    class Interpreter:
    
  2. Create an __init__ method with the definition below. The init method should call turtle.setup( width, height ) and then set the tracer to False (if you wish).
    def __init__(self, width=800, height=800):
    
  3. Create a drawString method for the Interpreter class. Except for the function definition and indentation, you can copy and paste it from the version 1 interpreter.py file. The new method definition just needs self as the first parameter.
    def drawString(self, dstring, distance, angle):
    
  4. Copy over the hold function from the version 1 interpreter.py file. Again, you just need to add self as the first parameter to the function definition and fix the indentation.
  5. One of the goals of building the interpreter is to encapsulate all of the turtle commands. We need to add some methods to the interpreter that let us position and orient the turtle and set its pen color. Add the following methods to your interpreter class.

    def goto( self, xpos, ypos )
    Pick up the pen, place the turtle at (xpos, ypos), and put the pen down.
    def orient( self, angle )
    Set the turtle's heading to the given angle.
    def place( self, xpos, ypos, angle=None )
    Pick up the pen, place the turtle at location (xpos, ypos), orient the turtle if angle is not None, and put down the pen.
    def color( self, color )
    Set the turtle's pen color.
    def width( self, width )
    Set the turtle's pen width.
  6. Use the test function below—which is almost identical to last week's—and test your interpreter.py file just like we did last week.
    def main(argv):
        if len(argv) < 4:
            print 'Usage: python interpreter.py filename distance angle'
            exit()
    
        # parse command line parameters
        filename = argv[1]
        distance = int(argv[2])
        angle = float(argv[3])
    
        # read lsystem output string from file
        fp = file( filename, 'r' )
        lstring = fp.readline()
        fp.close()
    
        # interpret string as turtle commands
        dev = Interpreter( 800, 800 )
        dev.drawString( lstring, distance, angle )
        dev.hold()
    
  7. If you run the pair of commands below, your programs should draw the a tree using a multiple rule L-system.
    $ python lsystem.py systemC.txt 3 output.txt
    $ python interpreter.py output.txt 10 25
    

When you are done with the lab exercises, get started on the project.