Python: String Formatting and Enumerate

Posted on February 21, 2012 - category: python

As a mostly self-taught Python scripter, I try to keep on top of best-practices and constantly learn, because I realize how easily bad habits can slip in. I’ve recently learned these bad habits are known as anti-patterns.

That said, I learned a couple new Python tricks this week.

1. Better String Formatting Using Format()

If you’ve used string formatting before, this is very similar, except the replacement fields are inside {} brackets, and you can use variables as keywords or an index.

examples:

print 'We are the {0} who say "{1}!"'.format('knights', 'Ni')
print 'We are the {1} who say "{0}!"'.format('Ni', 'knights')
print 'We are the {people} who say "{quote}!"'.format(people='knights', quote='Ni')

There are also lots of examples of how to format numbers or create precise column spacing. The ” old string formatting” is eventually going to be removed from Python. There is likely no rush to switch, but it is less flexible and a bit harder to read:

print 'We are the %s who say %s!' % ('knights', 'Ni')

source: http://docs.python.org/tutorial/inputoutput.html

2. Looping With Enumerate()

One of the common things you want when looping through things is an index counter. You can do this a few various ways: counter += 1 for i in range(): or you can use each.index() on array items. There is a better way:

Enumerate() is nice PEP-friendly way to get a “a compact, readable, reliable index notation” in loops. It works even when you don’t have an array to count, or a known length (for doing a range()). You use two variables in your for loop, and the first is your index. It is much nicer than having a separate counter or measuring the length of something which may change inside the loop.

(This is a PyMEL example in Maya and assumes you have a selection of objects.)

import pymel.core as pm

for i, each in enumerate(pm.selected()):
    print i, each

for i, each in enumerate(['some', 'array', 'here']):
    print i, each

3. Put Them Together

Let’s do a simple example where we also use .format() to rename a series of objects. I’ll use {1:02d} to automatically add frame-padding. {1:__} is the replacement field index. {__:02d} is the frame-padding. (You can also convert to percentages, hexadecimal, dates, change the column spacing, etc. etc. This is just one small example.)

for i, each in enumerate(pm.selected()):
    newName = '{0}_{1:02d}'.format('right_whisker', i)
    pm.rename(each, newName)

The nice thing about .format() is how easy it is to read and edit, since the replacement fields are clearly separated by brackets and you can pass any variables to them. Again this isn’t new, but it is easier to read than the old string formatting. So let’s play with this a bit and expand the example to automatically rename the object as “left” or “right”, depending on its translation in X.

for i, each in enumerate(pm.selected()):
    if each.tx.get() > 0:
        side = 'left'
    else:
        side = 'right'
    newName = '{0}_{1}_{2:02d}'.format(side, 'whisker', i)
    pm.rename(each, newName)

In the past, one of the most common ways I’ve created an index count is by measuring a length of a list or array or by using .index() to return the index of an array item. In comparison, this looks very ugly now. Plus, if you were to delete some objects inside the loop, the length of your range might change.

objects = pm.selected()
for i in range(0,len(objects)):
    print i, objects[i]

Two useful new tools for the arsenal. If you have any tips, suggestions or improvements, leave a comment. Next up, I’ll be getting back to the Mini Mammoth rigging after a bit of a hiatus.


Comments (closed)

Alexander Morano: How does this work with the log_formatter strings? E.G.: _log_format = ‘%(asctime)-9s %(levelname)-7s %(name)-40s %(message)s’ since that is a template, are the formatters for the logger class updated in 2.6-> ? Cheers.

Chris: @Keir: Ah thanks. Yes it seems it very similar. I suppose the only advantage is the readability. @Alexander: That’s a good question, but I’m not sure since I use pymel’s plogging module. There doesn’t seem to be any word on format() in regards to log formatting does there?

Keir: The % based string substitution also has a couple of cool tricks. [code] print ‘We are the %s who say “%s!”’ % (‘knights’, ‘Ni’) print ‘We are the %(people)s who say “%(quote)!”’ % {people:’knights’, quote:’Ni’} [\code] [code] people = ‘knights’ quote = ‘Ni’ print ‘We are the %(people)s who say “%(quote)!”’ % locals() [\code]

Rob Galanakis: Good post but two problems: First is the for/if/else antipattern. You have a known case (each.tx.get() == 0), why are you testing? Pull the loop code into an inner function, call it for the explicit case, then call it for the loop case. Not only does for/if/else not perform well, it is difficult to read because it introduces an extra branch for every loop iteration, even though you should only have to think about the ‘0 case’ logic once. Second is mutating a collection as you loop over it. Not only is this actually illegal with many structures (python dicts) and languages (like C#), it is a great way to write code that is incredibly difficult to understand, especially if, say, your collection is pymel nodes. There is never ever a reason to mutate within a loop. Assign things ‘transactionally’ instead- that’s a really loose usage of the word transaction, but I mean, just build up new structures and assign them to what you need to assign them to when done.

Chris: Awesome Rob, thanks for the great feedback. That makes total sense. I always find your blog and TAO posts very valuable to read and re-read. 2 years into Python I consider myself a beginner and you very often introduce concepts, rules and guidelines to which I was completely unaware. (Actually my real autorig code does explicitly call functions. This was just a quick contrived example.)

gr: for i, item in enumerate(L): # … compute some result based on item … L[i] = result