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. Classes provide a way to encapsulate data and operations associated with that data. 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 Lsystem and Interpreter classes to have methods (functions) with particular names.
You have been using objects throughout the semester (PIL Image objects, file objects, strings, lists, 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. This is frequently a source of confusion when Python produces an error message about the number of operands used when calling a method.
The self keyword needs to precede any field access within a class definition. For example, assigning a value to a field has the form: self.field = value. Each instance of a class has its own copy, and therefore different values, for each field.
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 for project8 and a new lsystem.py file. Label this one version 2 in the comments at the top of the file. This file will hold the Lsystem class and a test function for it.
- Define a class named Lsystem. A class definition has a simple
structure. It begins with the class declaration, and all methods are indented
relative to this class declaration, similar to a function. This example code
begins the class definition and provides a template for what the
__init__ method should do, explained in more detail below.
class Lsystem(object): 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 parameter 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 method 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__ method 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 for a class, put the prefix self. in front of the name of the field and assign a value to it. The example code above creates the field base and assigns to it an empty string.
If the __init__ function receives a filename, it should read L-system information from the file by calling the read() method of self (which you will 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 the base field the value newbase. The getBase function should return the base field.
- Create an accessor method getRule(self, index) that returns the single rule with the specified index from the rules field. Then create the method addRule(self, newrule), which should add a copy of newrule to the rules field. Look at the version 1 lsystem.py file if you need to remember how to write these methods.
- Create a read(self, filename) method that opens the file specified
by filename, reads in the Lsystem information, resets the
base and rules fields, and then stores the information from the file
in the appropriate fields using the methods 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 do not need to create a new Lsystem data
structure (self already exists) and you will need to use the accessor
methods. You can use the following template:
def read( self, filename ): # open the file specified by filename # read the contents of the file into a list of strings, one string per line # close the file # for each line in lines # split the line into words using the split() function # if the first word is 'base' # call the setBase method, passing the new base string # else if the first word is 'rule' # call the addRule method, passing 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 input
string, testing if there is a replacement rule for each character. If there is
a rule, we add the replacement string onto the output string. If there is not a
rule, we add the character unchanged to the output string.
def replace( self, inString ): # assign to a local variable (e.g. outString) the empty string # for each character in the input string # set a local variable (e.g. found) to False # for each rule in the rules field # if the symbol for the rule matches 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.
- Create a buildString(self, iterations) function.It will be very similar to the buildString function from version 1.
- Use the following main() function to test your program so far.
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 named interpeter.py. Label it version 2 in the
comments at the top of the file. Import the turtle package and
probably the random and sys packages as well. Begin the class
definition for a class named Interpreter:
class Interpreter(object):
- Create the __init__ method for the Interpreter class
starting 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, this will be identical to
the function 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.read() 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 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.