Mimsy Were the Borogoves

Hacks: Articles about programming in Python, Perl, PHP, and whatever else I happen to feel like hacking at.

Learning Python

Jerry Stratton, March 1, 2006

I rarely follow “learning” books sequentially through from start to finish. I generally read the first chapter or two and then skip around while writing some software that I need to write. Learning Python had me all the way through to the end, mostly because Python has significant differences in style and philosophy than C, Perl and PHP.

I originally wanted to learn Python because I manage the Mailman mailing list software at the University. Mailman is written in Python and we occasionally need to customize it. I bought Programming Python several years ago, and it was useful enough for that purpose, but it didn’t help me learn Python enough to write my own scripts and software from scratch. Nor did it tell me why I might want to.

Python’s advantages

I tend to look at scripting languages very practically: what can they do for me now? I currently use Perl and PHP most of the time. The advantages of Python for me are clearly defined scope, easy objects, named variables in function calls, and to some extent partial pre-compilation.

In Python, everything that gets defined in a file is only available in that (or from that) file. There is none of the namespace pollution that you see in languages such as PHP or Perl where everything has access to everything else.

When I wrote this journal software several years ago, I used PHP because it was the best choice at the time. Were I to write it today, I would seriously consider Python’s scope against PHP’s ability to be easily embedded inside text documents such as HTML pages.

For example, when I add plug-ins to this software, I always prefix function names with a code to ensure that the functions do not conflict with other functions. You see this in plug-ins for other journal/CMS software as well, and even then you are hoping that nobody else uses the same code. Since it is usually a two-letter abbreviation it is a serious issue.

In Python having two functions with the same name in different files doesn’t matter. If I have a plug-in called “forum” and a plug-in called ”shoutback” and each has a function called “displayMessage”, the journal’s engine will call the one as “forum.displayMessage()” and the other as “shoutback.displayMessage()”.

The same applies to incidental globals: globals are per file, not per application. You control which globals are available by which files you import, and globals are referenced in the same way that functions are: by prefixing them with the file’s name.

For large projects, such as, I suspect, Mailman, Python’s scope rules are a huge advantage.

Python also pre-compiles as needed. Whenever a module (file) is used, Python compiles it to “byte code”, which is much faster to parse than the source code on demand. As long as Python has write access to the folder that contains the module, that byte code version is then written out. From now on, until the original file is modified, Python will use the byte code version rather than the source code version. “The net effect is that pure Python code runs somewhere between a traditional compiled language, and a traditional interpreted language.”

As far as objects go, just about everything in Python is an object, in the sense that they contain methods and properties. If you want the length of an array, you use “arrayname.length”, not “length(arrayname)”. If you want to convert a string to title case, you’ll use “stringname.title()” rather than, say, “ucwords(stringname)”.

Sometimes the choice of what contains which methods is a little strange; if you want to implode an array into a string separated by a comma, you’ll use “','.join(arrayname)” not “arrayname.join(',')”. But in general, the heavy objectification in Python works well.

An easy tutorial

I purchased Learning Python during the 2005 Emerging Technologies conference here in San Diego, and was able to follow it easily during free time at the conference. I rarely go through tutorials as deeply as I went through this one. I’ll usually give up after a few chapters and just start some project. Part of it was that Python does so many things differently that it is necessary to keep following the tutorial just to know what questions to ask the index. But part of it is that the tutorials are well written; each step builds on the previous step, but it doesn’t go so slowly as to bore. It isn’t only easy, it’s interesting.

After going through the tutorials and after starting to use Python for some projects, I’ve found that the book continues to be useful as a reference.

Overloading

Strings are, in a way, used as an example of how to overload basic functions on variables. Many of the basic mathematical expressions are overloaded for strings, for example. Besides using the “+” to concatenate strings, you can multiply a string by 3 to repeat the string three times. You can overload these things yourself, for your classes.

Unique data types

Besides the standard data types (data objects) such as numbers, strings, lists, and dictionaries (associative arrays), Python has a few unique ones as well, such as complex numbers and tuples.

Tuples are a sort of immutable group of objects. They work very much like lists, but have their own special uses, some of them, according to the book, historical.

Complex numbers are those square roots of negative numbers that you might recall from high school algebra. For example, “print 4j*4j” results in “-16”. More specifically, it results in “-16+0j”: the real portion is -16, and the imaginary portion is 0.

Even functions and methods can be used as data objects: they can be placed in lists, assigned to variables, and passed around in pretty much the same way any other data type can.

Loops

Python does away with the standard C-style for counter. Python’s “for” is like PHP’s “foreach”: it loops through items in a list. There is a “range()” function to provide numbers for a counter. It does so by providing a list of numbers that are then iterated through by “for” the same as for any other list of items.

Example: Role-playing game assistant

So what might you do with Python? In my case, I use it for scripting simple applications that are easier to script than write a program for, but which I want Python’s scope and other application-useful features.

For example, I might want to make a script that rolls ability scores for a role-playing game. So I could create a class like this:

[toggle code]

  • import random
  • abilitynames = ['agility', 'charisma', 'endurance', 'intelligence', 'strength', 'wisdom']
  • numericsuffixes = {1: 'st', 2: 'nd', 3: 'rd'}
  • class ability:
    • def __init__(self):
      • #roll four six-sided dice and remove the lowest
      • rolls = [random.randint(1,6), random.randint(1,6), random.randint(1,6), random.randint(1,6)]
      • self.score = sum(rolls)-min(rolls)
    • def __str__(self):
      • score = str(self.score)
      • score = score.rjust(2)
      • return score
    • def __int__(self):
      • return self.score
    • def setScore(self, newScore):
      • self.score = newScore

This is a fairly simple class. It has four methods. One of them is the Python initialization method: “__init__”. The other two create a means of turning instances of this class into a string (“__str__”) or an integer (“__int__”). The __int__ method simply returns the score as a number. The __str__ method returns the score right-justified.

All of the methods that handle a built-in function are surrounded by double underscores.

The last method is “setScore” which is a way to set this score to something other than what was originally rolled in the initialization sequence.

If I save this class in a file and call it “rpg.py”, I can use the class in other scripts.

[toggle code]

  • #!/usr/bin/python
  • import rpg
  • stats = []
  • for i in range(6):
    • stat = rpg.ability()
    • stats.append(int(stat))
  • stats.sort()
  • print "Rolls: ", stats

This script will import “rpg”, create an empty list called “stats”, and then fill that list with six items, each of them an ability score. It then sorts the list and prints it. It will produce results like:

  • 27: ./game
  • Rolls: [11, 13, 13, 14, 15, 15]

Quick character generator

One way I might use that ability class is to create a character using those abilities. Here is a very simple character generator:

[toggle code]

  • #set up some defaults for characters
  • abilitynames = ['agility', 'charisma', 'endurance', 'intelligence', 'strength', 'wisdom']
  • numericsuffixes = {1: 'st', 2: 'nd', 3: 'rd'}
  • firstnameparts = ['ji', 'di', 'po', 'an', 'on', 'la', 'el']
  • lastnameparts = ['red', 'blue', 'beard', 'bird', 'leaf', 'stick', 'sky']
  • class character:
    • def __init__(self, name=None, level=1):
      • if name == None:
        • self.name = self.makeName()
      • else:
        • self.name = name
      • self.level = level
      • self.abilities = {}
      • for name in abilitynames:
        • self.abilities[name] = ability()
    • def __str__(self):
      • lines = []
      • lines.append(self.title().center(80))
      • lines.append("")
      • lines.append("ABILITIES")
      • maxlength = 1
      • for ability in self.abilities:
        • if maxlength < len(ability):
          • maxlength = len(ability)
      • maxlength += 2
      • for ability in self.abilities:
        • abilityName = ability.title()+ ':'
        • abilityName = abilityName.ljust(maxlength)
        • lines.append(abilityName + str(self.abilities[ability]))
      • sheet = "\n".join(lines)
      • return sheet
    • def makeName(self):
      • firstName = random.choice(firstnameparts) + random.choice(firstnameparts)
      • lastName = random.choice(lastnameparts) + random.choice(lastnameparts)
      • return firstName + ' ' + lastName
    • def title(self):
      • myTitle = self.name + ', ' + self.levelTitle() + ' level'
      • myTitle = myTitle.upper()
      • return myTitle
    • def levelTitle(self):
      • suffix = numericsuffixes.get(self.level, 'th')
      • return str(self.level) + suffix

This should go into the same rpg.py that we placed the ability class in. It creates a class called “character”. This class has only five methods at the moment: an initialization method, a method for turning instances of the class into a string (in this case, by creating a character sheet), a method for creating a simple random name for the character, and two methods for dealing with the character sheet’s title.

One interesting line, in the levelTitle() method, is:

  • suffix = numericsuffixes.get(self.level, 'th')

Python doesn’t have a switch statement. Learning Python recommends either using a series of “if” and “elif” statements, or using a dictionary and indexing into it. If you look up towards the top, we created a dictionary called “numericsuffixes”. One of the methods that dictionaries have is the “get” method. It gets the value from the dictionary that matches the key we give it; it returns a default value that we can also supply.

Here, we pass the level of the character and the default value of “th”. If the character’s level appears in the numericsuffixes dictionary, the appropriate suffix is returned. Otherwise we get “th”. This gives us “1st”, “2nd”, “3rd”, and then “4th”, “5th”, and so on.

Dictionaries (like lists and tuples) can contain functions, methods, and classes as well as more normal data values, making the get() method a useful alternative to “switch”-style statements.

If we start up Python and import rpg.py, we can use this to create quick stats for characters:

  • >>> import rpg
  • >>> jack = rpg.character("Jack Redbeard")
  • >>> print jack
                            JACK REDBEARD, 1ST LEVEL
ABILITIES
Agility:      11
Strength:     15
Intelligence: 13
Endurance:    11
Wisdom:       15
Charisma:     13

Extending classes

Of course, one of the benefits of classes is the ability to extend them. We can extend our “character” into a quick way of making specific characters such as warriors.

Add this method to the character class (finally got to that pun):

[toggle code]

  • def ensurePrime(self, prime):
    • #make sure that this character has the stat to be a this archetype
    • #nine is the minimum for any character archetype
    • if int(self.abilities[prime]) < 9:
      • maxAbility = prime
      • #first, determine which ability is the largest
      • for ability in self.abilities:
        • if int(self.abilities[ability]) > int(self.abilities[maxAbility]):
          • maxAbility = ability
      • #then, if the largest ability isn't our too-low prime ability
      • #switch the two around
      • if maxAbility != prime:
        • newStrength = self.abilities[maxAbility]
        • if int(newStrength) > 8:
          • self.abilities[maxAbility] = self.abilities[prime]
          • self.abilities[prime] = newStrength
          • return True
    • else:
      • return True
    • #if we haven't returned yet, we were unable to find a score at least 9
    • return False

This method, “ensurePrime()”, ensures that a character has at least an ability score of nine in a specified ability. If the character doesn’t, the method switches with another ability score, assuming it can find one greater than nine.

So now we can make a warrior class and ensure that any characters created using it have at least a nine strength:

[toggle code]

  • class warrior(character):
    • def __init__(self, name, level=1):
      • character.__init__(self, name, level)
      • if not self.ensurePrime('strength'):
        • print "Unable to make this character a warrior."
    • def title(self):
      • myTitle = character.title(self) + " WARRIOR"
      • return myTitle

Notice that we override both __init__() and title(), but in each case we first call the parent class’s version of those methods.

  • >>> import rpg
  • >>> jill = rpg.warrior("Jill Bluebird", 3)
  • >>>print jill
                        JILL BLUEBIRD, 3RD LEVEL WARRIOR                        

ABILITIES
Agility:      11
Strength:     13
Intelligence: 14
Endurance:    13
Wisdom:       12
Charisma:     14

Matching arguments by name

I mentioned earlier that one of the useful things in Python is named arguments in function (or method) calls. Here, we can create a character with or without a name and with or without a level. We might simply use:

  • >>> print rpg.warrior()

to produce:

                         ELON REDRED, 1ST LEVEL WARRIOR                         

ABILITIES
Agility:      16
Strength:     10
Intelligence:  6
Endurance:    11
Wisdom:       11
Charisma:      5

But we might also use:

  • >>> print rpg.warrior(level=9)

to specify the level but not the name, producing something like:

                        ONDI LEAFSKY, 9TH LEVEL WARRIOR                         

ABILITIES
Agility:      12
Strength:     14
Intelligence: 15
Endurance:    17
Wisdom:        9
Charisma:     11

When you’re making a complex application with functions that can take many arguments, this ability to bypass arguments and specify only the ones you need is extremely useful.

If these examples intrigue you, I recommend this book. If you normally use PHP and/or Perl for your scripting tasks, and occasionally feel the need for a more strict environment or one that facilitates large, more complex applications, I definitely recommend this book.

Throughout creating this example, I found the book useful when looking up the syntax for creating random numbers, for handling switch-style choices, how to make the class instances convert themselves to strings “automatically” for printing, and the syntax for extending classes. It’s really a great book if you want to learn Python.

  1. <- Automatic viruses
  2. Perls Before Swine ->