Iterator Generators and Maya

Iteration is a fundamental concept of computer science, and Python’s iterator generators make it a snap to organize and re-use iteration. I find that I am frequently iterating through large hierarchical structures, both ‘up’ and ‘down’ them looking for nodes with specific qualities.

In Maya, this is often iterating through the DAG, which is Maya’s version of a scengraph. Each level of the DAG can have one parent (for transforms, not shapes) and multiple children. If you are traversing the DAG looking for items, say in a character’s skeleton, Python iteration could be what you’re looking for.

In PyMel, you can grab a PyNode and ask for its parent with getParent(). I find, however, that I often need to iterate farther up the DAG looking for an ancestor that has a particular name or attribute. This could be accomplished by multiple calls to the getParent method of each node, but that can be cumbersome to do in code, and is harder to do in when using list comprehensions. Fortunately, a simple iterator generator can be whipped up in no time:


def parentIter(pnode):
    p = pnode.getParent()
    while p:
        yield p
        p = p.getParent()

Now we can hand this a PyNode (or anything with a getParent method) and it’ll run all the way up the DAG. This is handy if you have a schema where you are parenting a character’s skeleton under a root transform.

for t in parentIter(PyNode('someJoint')):
    if t.type() == 'transform':
    # do something here

This is pretty useful – I often will add a bit of sugar to make life easier. Consider a function that expects to operate on a character – it takes any node that’s part of the character, but needs to get some info off of the root node. Often, you’d need to check to see if the caller passed in the root node itself, or any of the joints. You would need something like this:


def getRoot(node):
    if node.type() == 'transform' and node.hasAttr('info'):
        return node
    for nd in parentIter(node):
        if node.type() == 'transform' and node.hasAttr('info'):
            return node

In order to prevent duplicating the testing part (seeing if a node is a transform and has an attribute called info), the parentIter function can be modified as follows:


def parentIter(pnode, inclusive=False):
    if inclusive:
        yield pnode
    p = pnode.getParent()
    while p:
        yield p
        p = p.getParent()

Now, the iteration can be a bit simpler:


def getRoot(node):
    for nd in parentIter(node, inclusive=True):
        if node.type() == 'transform' and node.hasAttr('info'):
            return node

Eliminating those extra lines can reduce visual clutter and potential error. Also, if you need to update the line that does the filtering, you only have to change it in one place.

Child iteration can be just as simple if you don’t care about the order and you don’t need to skip branches of a hierarchy:


def childIter(pnode, inclusive=False):
    if inclusive:
        yield pnode
    for ch in pnode.getChildren():
        yield ch
        for gch in childIter(ch):
            yield gch

Again, the inclusive keyword is used to simplify the iteration. I usually keep the keyword optional as it seems to blur the line between what you might expect ‘Give me all the nodes under x‘ as opposed to ‘Give me x and all the nodes underneath it‘. I find in my work plenty of use cases for either, so keeping the arg as a convenience seems efficient and clear.

One thing to note about these generators – if you need a list of everything the generator would return, you need to use a list constructor to capture it, i.e.:


# get all the nodes that are ancestors in a list
list(parentIter(node))

Failure to do so will often result in the calling code erroring out with the generator object:


# Error: AttributeError: file line 1: 'generator' object has no attribute 'append' #

 

Advertisements
Iterator Generators and Maya