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
- 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.
- 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 argumentThe __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 )
- 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.
- 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.
- 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 - 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 outStringNote 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.
- 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 - 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
- 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:
- 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):
- 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):
- 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.
- 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.
- 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() - 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.