Book Image

Practical Maya Programming with Python

By : Robert Galanakis
Book Image

Practical Maya Programming with Python

By: Robert Galanakis

Overview of this book

Table of Contents (17 chapters)
Practical Maya Programming with Python
Credits
About the Author
About the Reviewers
www.PacktPub.com
Preface
Index

Exploring Maya and PyMEL


Now we will start digging into Maya and PyMEL. Let's begin by initializing Maya in the mayapy interpreter so we can use more than just standard Python functionality. We do this by calling maya.standalone.initialize, as shown in the following code:

>>> import maya.standalone
>>> maya.standalone.initialize()
>>> import pymel.core as pmc
>>> xform, shape = pmc.polySphere()

The import of pymel.core will implicitly call maya.standalone.initialize automatically, but I do it explicitly here so it's clear what's going on. In the future, you can generally skip the call to maya.standalone.initialize and just import pymel.core.

There is a lot we can discover about these PyMEL objects, which represent Maya nodes, using basic Python. For example, to see the type of either of our objects, we can use the built-in type function (we will dig much deeper into types later in this chapter).

>>> type(xform)
<class 'pymel.core.nodetypes.Transform'>
>>> type(shape)
<class 'pymel.core.nodetypes.PolySphere'>

To see the names of all the attributes on our shape, we can use the built-in dir function:

>>> dir(xform)
['LimitType', 'MAttrClass', ..., 'zeroTransformPivots']

We can use the built-in getattr function with an object and a name of one of its attributes to get the value of the attribute.

>>> getattr(xform, 'getShape')
<bound method Transform.getShape of nt.Transform(u'pSphere1')>
>>> getattr(xform, 'translate')
Attribute(u'pSphere1.translate')

Tip

Note that we are using the getattr built-in Python function, not to be confused with the maya.cmds.getAttr function or its equivalent PyMEL version. We will not use the getAttr function at all in this book. While using getAttr may be more familiar if you are coming from the world of MEL and maya.cmds, using getattr is familiar if you are coming from the much saner world of Python. We should take full advantage of Python and do things the Python, not MEL, way.

We can combine all of these techniques and use the inspect module from Python's standard library (commonly referred to as the stdlib) to filter for interesting information about our object.

>>> import inspect
>>> methods = []
>>> for a in dir(xform):
...     attr = getattr(xform, a)
...     if inspect.ismethod(attr):
...         methods.append(attr)
>>> attrs = xform.listAttr()
>>> methods
[<bound method Transform.__add__ of nt.Transform(u'pSphere1')>, ...]
>>> attrs
[Attribute(u'pSphere1.message'), ...]

In the preceding code, we use the dir function to get every Python attribute from our PyMEL transform instance, and then filter them into a list of methods. We then use the listAttr method to get a list of Maya attributes on the transform. Based on this data, we can begin to see how the node is structured. Maya attributes are represented by instances of Attribute, and methods are usually helpers.

Tip

Using for loops and if statements that append to a list is not a good practice. We'll replace them with a much nicer syntax called list comprehensions in Chapter 2, Writing Composable Code.

Creating an introspection function

We'll create a new function in minspect.py that will print out interesting information about the object we pass it. Open C:\mayapybook\pylib\minspect.py in your IDE or text editor. First, we will add an import to the top of the file (import sys should already be there). As a matter of style, imports should always go at the top of the file, one per line, divided into groups. Refer to Python's style guide, called PEP8, at http://www.python.org/dev/peps/pep-0008/#imports. We will refer back to PEP8 several times throughout this book.

import pymel.core as pmc
import sys

Now let's use our earlier techniques and our understanding of Maya to print some information about an object. We can use the name method on PyNode to provide a simple string representation for a node.

def info(obj):
    """Prints information about the object."""

    lines = ['Info for %s' % obj.name(),
             'Attributes:']
    # Get the name of all attributes
    for a in obj.listAttr():
        lines.append('  ' + a.name())
    result = '\n'.join(lines)
    print result

You'll notice that instead of repeatedly printing, we put our data into a list and print it only once at the end. We do this for three reasons.

First, appending substrings to a list of strings is faster than incrementally concatenating (adding) them into a large string. Never concatenate many strings together in Python. For example, consider the following expression.

'Hello, ' + username + ', it is ' + now + ' right now.'

Evaluating this expression creates several unnecessary intermediate strings that are immediately thrown away. Ideally we should minimize transient objects.

Second, joining a list of strings is a well-known pattern. It is said to be more Pythonic than string concatenations.

Tip

There is no formal definition for what Pythonic means, but here is an attempt: something is said to be Pythonic when enough people have said it is Pythonic, or it is very similar to something that is Pythonic. You should learn to use Pythonic idioms wherever possible, and I will point them out when we run across them.

Finally and most importantly, building a list and printing at the end is more maintainable and easier to test because we are separating decisions (creating the result string) from dependencies (the output stream we print to). If this were production code we'd remove the print (dependency) entirely so we can easily test the decisions in info. We facilitate that future change by putting print into the last line so our dependency is in one place instead of on every line.

Tip

Note the use of the % operator in the info function. We are using string formatting. If you are not familiar with string formatting in Python, refer to Appendix, Python Best Practices, for more information about it.

We can continue adding to this function to express more information about a node. Try adding support for printing relatives by using obj.listRelatives() if you are comfortable.

Understanding Python and MEL types

Python's type function, which we used earlier, returns the type of an object. Every object in Python, including the type object, has a type, and PyMEL is no different.

>>> type([])
<type 'list'>
>>> type(type([]))
<type 'type'>
>>> type(xform)
<class 'pymel.core.nodetypes.Transform'>

You may also be familiar with MEL's type strings. Because MEL does not possess a rich type system like Python, typing (if it can be called that) is done with strings, as we can see in the following example. The usages of MEL type strings are highlighted.

>>> pmc.joint()
nt.Joint(u'joint1')
>>> pmc.polySphere()
[nt.Transform(u'pSphere2'), nt.PolySphere(u'polySphere2')]
>>> pmc.ls(type='joint')
[nt.Joint(u'joint1')]
>>> pmc.ls(type='transform')
[...nt.Joint(u'joint1'), nt.Transform(u'pSphere1'), ...]
>>> pmc.ls(type='shape')
[...nt.Mesh(u'pSphereShape1'), ...]

This MEL type, as we'll call it, is very useful while scripting, but not very descriptive. For example, we need to know in advance that a joint is a specific type of transform, and thus returned from invoking pmc.ls(type='transform'). This relationship is not clearly expressed.

In contrast, these taxonomic relationships are much better expressed through Python's type system. If we go to the PyMEL documentation for its Joint class, we can see the following diagram of its type hierarchy:

This type hierarchy mimics Maya's underlying object oriented architecture, and we can use it to understand things about nodes we may not be totally familiar with. For example, we can know that a Joint has translate/rotate/scale attributes because it is a subclass of Transform. A subclass inherits the behavior, such as the attributes and methods, of its base class. A subclass is commonly called a child class, and a base class is commonly called a parent class or superclass.

Notice in the example below how the __bases__ attribute indicates that Transform is the base class of the Joint class.

>>> j = pmc.joint()
>>> j.type()
u'joint'
>>> type(j)
<class 'pymel.core.nodetypes.Joint'>
>>> type(j).__bases__
(<class 'pymel.core.nodetypes.Transform'>,)
>>> j.translate, j.rotate
(Attribute(u'joint2.translate'), Attribute(u'joint2.rotate'))

We will look into PyMEL's type hierarchies for the next few sections as a means of understanding how PyMEL nodes work. Don't get intimidated if these concepts are new. We won't be creating any of our own types in this section, and if you are familiar with Maya's nodes, the type hierarchies we are going to examine should be rather intuitive. Later in the book, once we are more familiar with Python and PyMEL, several exercises will require creating our own types.

Using the method resolution order

Even more useful than the __bases__ attribute is the __mro__ attribute. Method Resolution Order (MRO) is the order Python visits different types so it can figure out what to actually call. You generally don't need to understand the MRO mechanisms (they can be complex), but looking at the MRO will help you understand all the type information about an object. Let's look at the MRO for the Joint type:

>>> type(j).__mro__
(<class 'pymel.core.nodetypes.Joint'>,
<class 'pymel.core.nodetypes.Transform'>,
<class 'pymel.core.nodetypes.DagNode'>,
<class 'pymel.core.nodetypes.Entity'>,
<class 'pymel.core.nodetypes.ContainerBase'>,
<class 'pymel.core.nodetypes.DependNode'>,
<class 'pymel.core.general.PyNode'>,
<class 'pymel.util.utilitytypes.ProxyUnicode'>,
<type 'object'>)

This makes sense: a joint node is a special type of transform node, which is a type of DAG node, which is a type of dependency node, and so on, mirroring the inheritance diagram we previously saw. The fact that reality meets expectation here is a great testament to Maya and PyMEL.

When a call to j.name() is invoked, Python will walk along the MRO looking for the first appropriate method to call. In the case of PyMEL, looking at the MRO often tells us a lot about how an object behaves. This is not always the case, however. Python allows much more dynamic resolution mechanisms. We will not use these mechanisms, such as __getattr__ or __getattribute__, much in this book, but you should be aware that the MRO may not always tell the whole story.

Let's add the collection of type and MRO information into the minspect.py file's info function:

def info(obj):
    """Prints information about the object."""

    lines = ['Info for %s' % obj.name(),
             'Attributes:']
    # Get the name of all attributes
    for a in obj.listAttr():
        lines.append('  ' + a.name())
    lines.append('MEL type: %s' % obj.type())
    lines.append('MRO:')
    lines.extend(['  ' + t.__name__ for t in type(obj).__mro__])
    result = '\n'.join(lines)
    print result

PyNodes all the way down

In Python, there's a saying, "Its objects all the way down." This means that everything in Python, including numbers, strings, modules, functions, types, and so on, are all just objects. In MEL and maya.cmds I like to say, "Its strings all the way down." Because the type system in MEL and maya.cmds is so rudimentary, many things must be handled through strings. And in PyMEL, I like to say, "It's PyNodes all the way down."

Tip

The saying is adapted from "Its turtles all the way down." It is left as an exercise to the reader to uncover the origin of this quote. It may also be said in Python that, "Its dicts all the way down." Python is a language of many clever sayings.

Let's look at our PyMEL transform node to better understand how it is "PyNodes all the way down."

>>> type(xform).__mro__
(<class 'pymel.core.nodetypes.Transform'>,
<class 'pymel.core.nodetypes.DagNode'>,
<class 'pymel.core.nodetypes.Entity'>,
<class 'pymel.core.nodetypes.ContainerBase'>,
<class 'pymel.core.nodetypes.DependNode'>, 
<class 'pymel.core.general.PyNode'>, 
<class 'pymel.util.utilitytypes.ProxyUnicode'>,
<type 'object'>)
>>> type(xform.translate).__mro__
(<class 'pymel.core.general.Attribute'>,
<class 'pymel.core.general.PyNode'>,
<class 'pymel.util.utilitytypes.ProxyUnicode'>,
<type 'object'>)

There are a number of very interesting things going on in this short listing.

First, our two types—along with all PyMEL types, in fact—inherit from both PyNode and ProxyUnicode (as well as object, which all Python types inherit from). The PyNode type represents any Maya node (DAG/dependency nodes, attributes, Maya windows, and so on). Vitally, Attributes are also PyNodes. If we look at the PyMEL help for PyNode, we can see the distinguishing features of PyNodes are that they have a name/identity, connections, and history.

Second, anything that inherits from DependNode has attributes. So predictably, Attributes are not a DependNode, but our Transform is.

Third, Transform is also a DagNode. We can use our knowledge of Maya (or graph theory, if you're into that) to infer that this means the object can have a parent, children, instances, and so on. This is a great example where our knowledge of Maya maps directly onto PyMEL, and we aren't required to learn a new paradigm to understand how the PyMEL framework works.

Note

We build custom Maya nodes in Chapter 7, Taming the Maya API.

Finally, if we look at the __mro__ for a Joint, we will see the following information:

>>> type(pmc.joint()).__mro__
(<class 'pymel.core.nodetypes.Joint'>, 
<class 'pymel.core.nodetypes.Transform'>, 
..., 
<type 'object'>)

We can immediately understand much of how a PyMEL Joint works if we already understand Transform. In fact, everything about Transform is also true for every Joint. The deduction that every Joint behaves like a Transform is known as the Liskov substitution principle. It states, roughly, that if S is a subclass of T, then a program can use an instance of S instead of T without a change in behavior. It is a fundamental principle of good object-oriented design and manifests itself in well-designed frameworks such as PyMEL.

The fact that types inherit the behavior of their parents is important to keep in mind as you go through the rest of this book and program with PyMEL. Don't worry if you don't fully understand how inheritance works or how to best leverage it. It will become clearer as we proceed on our journey.

Tip

The ProxyUnicode class should be treated as an implementation detail. The only important user-facing detail is that it allows PyNodes to have string methods on them (.replace, .strip, and so on). As of writing this book, I've never used the string methods on a PyNode. Maybe there are valid uses but I can't imagine them. There are always better, more explicit ways of dealing with the node. If you need to deal with its name, call a name-returning method (name(), longName(), and the like) and manipulate that. Use the rename method to rename the node.

Understanding PyMEL data and math types

PyMEL's intuitive use of type hierarchies does not end with Maya node types. It also provides a very useful wrapper around Maya's mathematical data types, including vectors, quaternions, and matrices. These types are located in the pymel.core.datatypes namespace.

Let's take a closer look at xform's transform information.

>>> xform.translate
Attribute(u'pSphere1.translate')
>>> t = xform.translate.get()
>>> print t
[0.0, 0.0, 0.0]

The translation value of the sphere transform, which is highlighted, appears to be a list. It isn't. The translation value is an instance of pymel.core.datatypes.Vector. Sometimes we need to more aggressively introspect objects. I think this is one of the few areas where PyMEL made a mistake. Calling str(t) returns a string that looks like it came from a list, instead of looking like it came from a Vector. Make sure you have the correct type. I've spent hours hunting down bugs where I was using a Vector instead of a list, or vice versa.

>>> vect = xform.translate.get()
>>> lst = [0.0, 0.0, 0.0]
>>> str(vect)
'[0.0, 0.0, 0.0]'
>>> str(lst)
'[0.0, 0.0, 0.0]'
>>> print t, lst # The print implicitly calls str(t)
[0.0, 0.0, 0.0] [0.0, 0.0, 0.0]
>>> repr(t) # repr returns a more detailed string for an object
'dt.Vector([0.0, 0.0, 0.0])'
>>> repr(lst)
'[0.0, 0.0, 0.0]'

Using repr as highlighted in the preceding code shows us that vect is not a list. It is one of PyMEL's special data types. This has a number of benefits, despite its bad string representation.

First of all, Vector and other data types are list-like objects. The __iter__ method means we can iterate over it, just like a list.

>>> t = xform.translate.get()
>>> for c in t:
...     print c
0.0
0.0
0.0

The __getitem__ method means we can look up an index.

>>> t[0], t[1], t[2]
(0.0, 0.0, 0.0)

But it also behaves more like we would expect it to mathematically. When we add two vectors, we get the summed vector, instead of a six item list.

>>> [1, 2, 3] + [4, 5, 6] # Regular Python lists
[1, 2, 3, 4, 5, 6]
>>> repr(t + [1, 2, 3])
'dt.Vector([1.0, 2.0, 3.0])'

And finally, Vector has several useful methods on it, including name-based accessors and helper methods.

>>> t.x += 5 # Familiar name-based access
>>> t.y += 2
>>> t.x
5.0
>>> t.length() # And helpers!
5.385...

This sort of design, where a custom type implements several different interfaces or protocols, is powerful. For example, if you wanted to move a transform by some vector, you can just write the following code:

>>> def move_along_x(xform, vec):
...     t = xform.translate.get()
...     t[0] += vec[0]
...     xform.translate.set(t)
>>> j = pmc.joint()
>>> move_along_x(j, [1, 0, 0])
>>> j.translate.get()
dt.Vector([1.0, 0.0, 0.0])
>>> move_along_x(j, j.translate.get())
>>> j.translate.get()
dt.Vector([2.0, 0.0, 0.0])

Notice that at no point did we need to check whether vec was an instance of list or of Vector. We just require it to implement __getitem__ so we can access an index. Think about how much more natural this pattern makes using Vector, Quaternion, Matrix, and the other data types in pymel.core.datatypes.

When you need to represent some mathematical data or measurement, take a look at the pymel.core.datatypes namespace. The classes and functions there are quite important and useful!

Leveraging the REPL

The info function we've been building isn't just a useful learning exercise. It can be helpful in everyday programming. A major advantage of dynamic, interpreted languages such as Python has been their REPL: Read-Evaluate-Print Loop (REPL). We've actually been using a REPL all throughout this chapter.

Let's write some simple code in the interpreter to demonstrate the REPL:

>>> pmc.joint
<function joint at 0x0...>
>>>

Now let's see how this fits into the REPL flow:

  1. The interpreter prompts for input, indicated by the >>> characters.

  2. We type pmc.joint and hit Enter.

  3. The input string pmc.joint is parsed into a data structure. This is the read.

  4. The interpreter evaluates the data structure, finding PyMEL's joint function.

  5. The interpreter prints the result to the output stream.

  6. The interpreter prompts for more input (the loop).

The alternative to the REPL is the Edit-Compile-Run Loop of compiled languages. This is a much longer process, often lasting minutes instead of seconds and requiring a full restart of the application. In recent years, several compiled languages have created interpreters or REPL environments, but this is unavailable to C++ in Maya right now.

It stands to reason that anything we can do to improve our REPL experience will help us learn and explore our language and environment more effectively. Whereas minspect.info allows us to see runtime information about some object, next we'll write a function to bridge the gap from runtime information to static documentation.