Poetic form. Classes and objects.

Poetic forms

“A poem is a small (or large) machine made of words.” — William Carlos Williams

A poetic form is halfway between a set of constraints and a set of instructions. Some of these constraints/instructions are formal (e.g., rhymes, number of syllables) and some are semantic (e.g., what the poem has to be “about”).

Some examples:

… and so forth.

Compare and contrast form with techniques such as N+7, cut-ups.

Our question: Which of these forms could be composed by a computer? Which would be impossible to compose with a computer?

Reading discussion: Oulipo, Cut-ups

Some points on Oulipo:

  • Literature as constraints and procedures
  • “The really inspired person is never inspired, but always inspired.” What does this mean? How was the Oulipo positioning itself with regard to Romantic ideals of literature?
  • Literature is subject to “exploration” by procedural means
  • If language is a concrete object, how do we manipulate it?
  • Forms as potential

Points on Gysin:

  • Do you agree that “all writing is in fact cut-ups”?

Object-oriented programming

Everything in Python is an object. But what is an object, really? In the most abstract sense, an object has some kind of data and provides a means to manipulate that data—to change the data, store new data, or retrieve the data in a different format. A good example is the list:

>>> foo = list() # create the object
>>> foo.append(1) # manipulate its data
>>> foo.append(2)
>>> foo.append(3)
>>> foo.pop() # get data: pop returns and removes last list element
3

The list foo has the ability to store data (a bunch of integers, in this case), and provides methods (append, pop, among others) to access that data.

There are two key benefits to this arrangement:

  • The data itself is hidden from view: we can only access it through methods. This means that even if Python decides to change the underlying code that implements append, our programs will still work. (This is called encapsulation.)
  • The close association of data and code that operates on that data provides a helpful abstraction: we can cognitively treat the combination of the two as a unit. This is generally considered more helpful than non-object-oriented code, wherein the functions that operate on data and the data itself are separate.

Defining our own classes

The idea of abstraction—of creating something in code that behaves like whatever it is we’re trying to model in the real world—is the main reason we might want to make our own kinds of objects. Python provides lists, dictionaries, sets, strings—but what if my program is about sentences, or poems, or people, or space rockets? It would be nice to be able to make our own objects, with data appropriate for those objects and methods that operate on them in an intuitive way. Something like:

>>> rocket = Rocket()
>>> rocket.power = 4
>>> rocket.awesomeness = 5000
>>> rocket.launch()
KABOOM!

Fortunately, Python provides a mechanism for us to define our own types of objects: the class.

If an object is a cookie, then the class is the cookie cutter. If an object is a car, then the class is the factory. If an object is a piece of Ikea furniture, then the class is the instructions that tell you how to put it together. Building, blueprint; vineyard, wine. Etc. Insert your own metaphor here.

Let’s make the simplest class possible:

>>> class Rocket(object):
...     pass
... 
>>>

As you can see, class definitions in Python begin with the keyword class, followed by the name of the class, followed (in parentheses) by the name of the class that this class should “inherit” from (see below for more details on inheritance). We’re not inheriting from any class we’ve already defined; Python requires in this case that you inherit from the built-in class object.

That’s all you need to make a class. (The pass keyword just means “nothing’s here”—it’s like empty curly brackets in other languages.) It won’t be a very interesting class—it has no properties or methods—but it’s a fully-fledged class nonetheless.

Now that we’ve defined a class, we can instantiate an object of that class. This is done by calling the class as though it were a function. The resulting object can’t do much, but it will respond to the type function (which returns the type of the object, i.e., the class from which it was instantiated), and the isinstance function (which returns true if the first parameter is an instance of the class specified as the second parameter):

>>> rocket = Rocket()
>>> type(rocket)
<class '__main__.Rocket'>
>>> isinstance(rocket, Rocket)
True
>>> isinstance(rocket, list)
False

Attributes, methods, and __init__

Class definitions can include more than just pass, of course. For the most part, when you define a class, you’ll be defining class methods: a special kind of function that is designed to work with objects of a particular class. Let’s redefine our Rocket class with a bit more functionality:

>>> class Rocket(object):
...     def __init__(self, power, awesomeness):
...             self.power = power
...             self.awesomeness = awesomeness
...     def launch(self):
...             output = "kab" + ('o' * self.power) + "m!"
...             if self.awesomeness >= 5000:
...                     output = output.upper()
...             return output
... 
>>> rocket = Rocket(5, 7000)
>>> rocket.power
5
>>> rocket.awesomeness
7000
>>> rocket.launch()
'KABOOOOOM!'

Here we’ve defined a class with two methods. The first thing you’ll notice is that both of these methods take self as a first parameter. You can think of self as analogous to this in Processing/Java: it simply means “whichever object this method was called on.” Python automatically passes this parameter to the method, so while you don’t have to include it when calling the method, you do have to include it when you define the method.

The __init__ (short for “initialize”) method is a special method: it’s called automatically whenever an object is instantiated. Any arguments that are passed when the object is instantiated (e.g., 5, 7000 in the transcript above) are passed along to __init__.

Attributes

Inside __init__, we use the parameters to set attributes on the object. You can set and access attributes by putting a dot (.) between the object and some name (e.g., rocket.power retrieves the power attribute of the rocket object). You can think of an object’s attributes as a strange-looking dictionary that holds the data associated with that object.

Methods

The launch method then accesses those attributes to determine what it should output. Here we make a string that varies depending on the rocket’s power and awesomeness.

Example in context: sentence.py

import conjunctions

class Sentence(object):

  # initialize function provides default arguments for subject and verb
  def __init__(self, subj="No one", verb="ignored", direct_obj="",
      prep_phrase=""):
    self.subj = subj
    self.verb = verb
    self.direct_obj = direct_obj
    self.prep_phrase = prep_phrase

  # render puts all the parts of the sentence together, only adding
  # direct object and prepositional phrase if present
  def render(self):
    elems = [self.subj, self.verb]
    if len(self.direct_obj) > 0:
      elems.append(self.direct_obj)
    if len(self.prep_phrase) > 0:
      elems.append(self.prep_phrase)
    output = ' '.join(elems)
    return output

  # statement: renders this sentence as a statement
  def statement(self):
    output = self.render()
    output += "."
    return output

  # uses the conjunctions module to randomly conjoin this sentence with
  # another
  def statement_conjoined_with(self, other_sentence):
    output = conjunctions.random_conjoin(self.render(), other_sentence.render())
    output += "."
    return output

if __name__ == '__main__':
  sentence1 = Sentence('John', 'ate', 'cheese', 'in a sack')
  print sentence1.statement()
  sentence2 = Sentence('George', 'slept')
  print sentence2.statement()
  print sentence1.statement_conjoined_with(sentence2)

- explain if __name__ == ‘__main__’

(notes forthcoming)

Inheritance, adding functionality and overriding methods

Another benefit of object-oriented programming is the concept of inheritance. Inheritance is a way to create a new class that behaves in all ways like another class, except for whatever differences that we define. Here’s how to do it, using sentence.py as the class we’re inheriting from (the “base class”):

>>> import sentence
>>> class Sentence2(sentence.Sentence):
...     pass
... 
>>> s = Sentence2('Joe', 'shrank', 'his sweater')
>>> s.statement()
'Joe shrank his sweater.'

The Sentence2 class here inherits from the Sentence class in sentence.py. That means that objects of class Sentence2 behave exactly like objects of class Sentence, without our having to define all of the same methods in class Sentence2.

We can easily add new functionality to our Sentence2 class by defining a function that isn’t present in the base class:

>>> class Sentence3(sentence.Sentence):
...     def hedge(self):
...             output = self.render() + ", or at least that's what I heard."
...             return output
... 
>>> s = Sentence3('Joe', 'shrank', 'his sweater')
>>> s.statement()
'Joe shrank his sweater.'
>>> s.hedge()
"Joe shrank his sweater, or at least that's what I heard."

The Sentence3 class supports all methods that the Sentence class supports, in addition to the new hedge method.

But the real power of inheritance becomes manifest when we override functions in the original class. This lets us replace parts of the functionality of the base class, while leaving the rest of the functionality intact. Behold, weird_sentence.py:

import conjunctions
import sentence
import random

class WeirdSentence(sentence.Sentence):

  def render(self):
    elems = [self.subj, self.verb]
    if len(self.direct_obj) > 0:
      elems.append(self.direct_obj)
    if len(self.prep_phrase) > 0:
      elems.append(self.prep_phrase)
    random.shuffle(elems)
    output = ' '.join(elems)
    return output

if __name__ == '__main__':
  weird = WeirdSentence('John', 'ate', 'a sandwich', 'yesterday')
  print weird.statement()
  normal = sentence.Sentence('George', 'bought', 'flowers')
  print normal.statement()
  print weird.statement_conjoined_with(normal)

Exercises

  1. Make a class that models a person. Specifically, instances of this class should have two attributes: first_name and last_name. The class should also define a full_name function which, when called, returns the first name and last name concatenated.
  2. Modify (or extend) the Sentence class in sentence.py to print out sentences in the form of a question. You can either define a new method (e.g., render_as_question) or override the existing render method.
  3. Take one of your homework assignments and re-envision it as an object-oriented program.

Helpful resources

Midterm project (due July 22nd)

This project has two steps. You must:

  1. Devise a new poetic form.
  2. Create a computer program that generates texts that conform to new poetic form you devised.

Your poetic form could be something as simple as “Each line must begin with the letter ‘A’” or something as sophisticated as Mac Low’s diastics.

Your computer program should take the form of a Python class that extends the PoetryGenerator class introduced in session 6. Your class file should be named yourusername.py and be located in the /var/www/generators directory on sandbox.decontextualize.com. (More details below.)

Your presentation and documentation for this project should include the following:

  • The name of your poetic form, and a thorough description of how it works
  • The source code for the program you used to generate poems that follow your form
  • A number of “poems” that your program generated (at least three), one of which you will read aloud during your presentation

Consider the following when evaluating your work:

  • How well does the output of your computer program conform to your invented poetic form? Could a human do it better?
  • How does your choice of source text (your “raw material”) affect the character and quality of the poems that your program generates?

Using sandbox.decontextualize.com to create your midterm project

If you’re using ssh, then ssh into the server as you usually do. At the prompt, type the following:

$ cd /var/www/generators
$ cp template.py yourusername.py

(Replace yourusername with your username on the server.) You can now use a text editor to make modifications to the file. You can see your program’s output on the web by visiting the following URL:


http://sandbox.decontextualize.com/generators/yourusername.py

For example, my poem generator is available at http://sandbox.decontextualize.com/generators/aparrish.py

If you’re using an SFTP client, make sure that you change which path you’re looking at. By default, you’ll be looking at your home directory; you’ll need to change the directory that your client is looking at to /var/www/generators. Make a copy of template.py, naming your copy yourusername.py. Make your modifications to this file as normal. Your poem generator will be available on the web at the same URL as above.

How the PoemGenerator class works

The PoemGenerator class is a generic class for web-based poetry generators. It takes care of interacting with the browser (gathering input and displaying output). You must make a class that inherits from this class, overriding certain methods in order to make your poem generator your own.

The PoemGenerator class has several methods which your classes must override. These are:

  1. description: this method should return a string containing a description your poetry generator. This string will be displayed beneath the title and author areas on the
  2. title: this method should return a string containing the title of your poem generator (the name of your invented poetic form)
  3. author: this method should return a string containing the name of the program’s author (you!)
  4. fields: the PoemGenerator class uses the result of this method to determine what fields to present to the user when they’re interacting with your generator. The keys of this dictionary should be a unique identifying string (like a variable name), and the values should be the human-readable labels for those fields. If the user supplies input to these fields, they’ll be passed to the generate function. (If you don’t want any user input, have this function return an empty dictionary.)
  5. generate: the most important function to override. This is where the code to generate your poem should go. The string returned from this function will be displayed to the user after the “Generate” button is clicked. If the user supplied any input, the second parameter passed to the method will contain a dictionary whose keys are the field names (as specified in fields) and whose values are what the user supplied for that field. (Both aparrish.py and template.py provide good examples of how this works.

You’ll be starting with a copy of template.py, so these methods will already be defined. Your job will be to replace the generic boilerplate code with your own code.

Notes:

  • Any new line characters in the string that generate returns will automatically be translated to <br>.
  • You’re free to use HTML in the string returned from generate.
  • When you’re editing your program, be sure to retain the first line of the program (#!/usr/bin/python) and the lines including and following if __name__ == '__main__':! Your program won’t work without these lines.

If you’re having any problems getting this to work, let me know ASAP and we’ll get you fixed up. In certain instances, I may have to make configuration changes on the server to get your program to work.

Reply