官术网_书友最值得收藏!

Writing a skeleton converter library

Let's put composability to work for us. We'll implement a true rite of passage for programming in Maya—a routine to automatically convert a hierarchy of nodes into a hierarchy of joints (a skeleton). This task allows us to focus on building small, composable pieces of code, and string them together so that the character creator tool we build later in the chapter can be very simple. These composable pieces won't just serve our character creator. They'll serve us all throughout the book and our coding lives.

Writing the docstring and pseudocode

Before we start coding, we need a place to put the code. Create the skeletonutils.py file inside the development root you chose in Chapter 1, Introspecting Maya, Python, and PyMEL. This book's examples use C:\mayapybook\pylib.

After creating skeletonutils.py, open it in your IDE. Add the following code. The code defines a function, a docstring that explains precisely what it will do, and pseudocode describing the implementation. Docstrings are explained in more detail in the next section:

def convert_to_skeleton(rootnode, prefix='skel_', _parent=None):
    """Converts a hierarchy of nodes into joints that have the
    same transform, with their name prefixed with 'prefix'.
    Return the newly created root node.
    The new hierarchy will share the same parent as rootnode.

    :param rootnode: The root PyNode.
      Everything under it will be converted.
    :param prefix: String to prefix newly created nodes with.
    """
    # Create a joint from the given node with the new name.
    # Copy the transform and rotation.
    # Set the parent to rootnode's parent if _parent is None,
    # Otherwise set it to _parent.
    # Convert all the children recursively, using the newly
    # created joint as the parent.

We explicitly state that rootnode should be a PyNode object. It helps to be clear what you're expecting, and just take in what you need rather than try to convert it inside the function. That is, do not take in a string and convert it into a PyNode. The less the function does, the easier it is to understand and maintain. It will also run faster.

The pseudocode says the function works recursively. If you don't know what recursion is, don't worry. We'll explain it when we get to that part of the code.

And finally, the leading underscore of the _parent parameter indicates it is a protected parameter. Callers outside the module should not pass it in. Recall that we used a leading underscore in the name of minspect._py_to_helpstr in Chapter 1, Introspecting Maya, Python, and PyMEL, to indicate the function is protected.

Understanding docstrings and reStructured Text

In the preceding code, we used a docstring to explain what our function does. We've used docstrings already, for example in the minspect.pmhelp function in Chapter 1, Introspecting Maya, Python, and PyMEL. The more advanced usage in the preceding code, however, warrants more explanation.

Docstrings are any literal string directly following a definition (module, function, class, or method). By convention, triple-double-quotes are used ("""), which allows easy multiline strings. PEP 257 (http://www.python.org/dev/peps/pep-0257/) defines Docstring Conventions and is well worth a read.

The :param rootnode: lines in the previous docstring are reStructured Text markup (abbreviated rst or reST). This allows special formatting of your docstrings in a way that is also readable as plain text. reStructured Text is standard for Python so I'd suggest getting into the habit of using it. Many IDEs support special rendering for it and the popular Sphinx project (http://sphinx-doc.org/) can turn your Python docstrings into HTML and other forms of documentation.

In addition to param, there are other directives, such as type, which can give hints to your IDE about what members exist on a parameter, providing a richer experience. You can see more information about how Sphinx renders reStructured Text at http://sphinx-doc.org/markup/desc.html. In particular, I suggest getting comfortable with the param, type, rtype, return, and raises directives. You should also be familiar with the text styling markup, such as *italics*, **bold**, and ''code''.

Start simple, and even if you don't go any further in reStructured Text, you should get used to documenting your code with docstrings.

Writing the first implementation

Now that we've written what our code is supposed to do, let's go ahead and write a rough implementation:

import pymel.core as pmcdef convert_to_skeleton(rootnode, prefix='skel_', _parent=None):
    """Converts a hierarchy of nodes into joints that have the
    same transform, with their name prefixed with 'prefix'.
    Return the newly created root node.
    The new hierarchy will share the same parent as rootnode.

    :param rootnode: The root PyNode.
      Everything under it will be converted.
    :param prefix: String to prefix newly created nodes with.
    """
    # Create a joint from the given node with the new name.
    j = pmc.joint(name=prefix + rootnode.name())
    # Copy the transform and rotation.
    j.translate.set(rootnode.translate.get())
    j.rotate.set(rootnode.rotate.get())
    # Set the parent to rootnode's parent if _parent is None,
    # Otherwise set it to _parent.
    if _parent is None:
        _parent = rootnode.getParent()
    j.setParent(_parent)
    # Convert all the children recursively, using the newly
    # created joint as the parent.
    for c in rootnode.children():
        convert_to_skeleton(c, prefix, j)
    return j

All our code does is what the pseudocode says. The recursion that we mentioned previously happens where we call convert_to_skeleton inside convert_to_skeleton. If a function calls itself, the function is said to be recursive. Recursion is a powerful technique that should not be overused, but it is indispensable when walking a tree or a hierarchy.

Breaking the first implementation

It's not difficult to see all the places this code can break, where it is ambiguous, and the axes of likely change:

  • What happens if rootnode is not a valid PyNode? This is acceptable. The only way this can happen is if code calls it with a non-PyNode, and we clearly state in the docstring that rootnode must be a PyNode, so we should not guard against this error. We look at these sorts of decisions more in Chapter 3, Dealing with Errors.
  • What happens if rootnode is inside of a Maya namespace? Will the new hierarchy be part of that namespace?
  • If the value of rootnode.getParent() is the same as j.getParent(), some versions of Maya will raise an error.
  • What should happen to the inputs and outputs of the node being converted, such as any shape or materials? Should they be duplicated or connected to the new joint?
  • Right now we preserve the name/parent/translation/rotation of the source transform, but what if we want to preserve additional information, such as scale or custom attributes? How would we customize the joint with different parameters, such as color or size?

Nearly any code you write is liable to have many unanswered questions about its behavior and will need to change at some point. This is especially true of very high-level code, and anything having to deal with Maya nodes is high-level. The complexity is introduced by the scope of the node's interface. The interface of a Maya node is so complex, and the surface area of what it can affect is so great, that it is impossible to fully enumerate an operation's contract.

Understanding interface contracts

We use the term contract in the sense popularized by Bertrand Meyer's Design by Contract. The idea, in a simplified form, says that every function has:

  • A set of preconditions: Preconditions are the things that must be true for the function to do its work. For example, the convert_to_skeleton function requires an existing PyMEL Transform instance.
  • A set of postconditions: Postconditions are the things that are guaranteed to be true after the function returns. For example, the convert_to_skeleton function returns a new PyMEL Joint instance.

Keeping this simple idea of contracts in mind can greatly help your design. However, when we are programming with Maya, we have to accept many limitations.

An operation such as setting an item in a dictionary (my_dict[key] = value) has a very clear contract. The precondition would be that key is hashable (implements a valid __hash__ method). The postcondition would be that the value exists for the key, so my_dict[key] == value would return True immediately after the value is set.

But, let's say we have a Maya locator node. What should happen when we copy it? Should any children also be copied? Should the new node be under the existing node's parent? Should it be under the existing node's namespace or the active one? In these cases we need very precise semantics, which Maya makes available under the Edit | Duplicate Special tool. Similar options usually exist in Maya's script commands.

We will have to be forever vigilant against where Maya sets us up for failure by not allowing strong contracts. As we go through turning our initial naive implementation into something more robust, we'll build composable pieces with very precise semantics. If each piece of code does just what it says, and does only what it can comfortably guarantee, our codebase will be simpler overall.

Extracting the safe_setparent utility function

Given what we know about contracts and where the existing code can break, the first thing we can do is pull out some small functionality into utility functions. Utility functions are general purpose helpers that people tend to consolidate into a Python library.

Tip

Utility functions are necessary, but too many can be dangerous. Unless they are of obvious and immediate general use, utility functions should be put next to where they are used. Utility libraries tend to grow with functions that end up being used only in one place, and effectively managing a utility library requires thorough documentation and high reliability, usually achieved through automated testing. So, try to defer the creation of utility functions and libraries until you have more than one actual use for it.

The easiest thing to fix is the potential j.setParent(_parent) error we documented previously. We only want to set the parent if the new value is different from the existing value. Let's change our code into the following, adding the safe_setparent function:

def safe_setparent(node, parent):
    """'node.setParent(parent)' if 'parent' is
    not the same as 'node''s existing parent.
    """
    if node.getParent() != parent:
        node.setParent(parent)

def convert_to_skeleton(rootnode, prefix='skel_', _parent=None):
    j = pmc.joint(name=prefix + rootnode.name())
    if _parent is None:
        _parent = rootnode.getParent()
    safe_setparent(j, _parent)
    j.translate.set(rootnode.translate.get())
    j.rotate.set(rootnode.rotate.get())
    for c in rootnode.children():
        convert_to_skeleton(c, prefix, j)
    return j

We took the only set the parent if it will succeed approach (LBYL) rather than try to set the parent and just pass if it fails (EAFP) approach here. Recall the discussion about EAFP versus LBYL in because setParent raises a very unhelpful RuntimeError, which can be raised for any number of reasons (for example, creating a circular parent/child relationship). Rather than potentially swallowing an unexpected error, or doing something ugly like parsing the error message, we use LBYL. And while LBYL is not always considered Pythonic, this sort of pragmatism is.

So we've added the safe_setparent utility function, which handles the I wish it worked this way in the first place behavior we are trying to hide. Using utility functions in this way is smart.

Tip

If you find yourself having to patch default behaviors often, you should consider following the advice in Chapter 9, Becoming a Part of the Python Community, and submit this as an improvement to the PyMEL source. In fact, that's already been done, so Maya 2011 and newer should not display this behavior.

While it's always best when problems can be fixed at the source, sometimes in order to create composable code, you need to take matters into your own hands.

Learning how to refactor

What we just did with safe_setparent is called refactoring. Martin Fowler gives the following definition of refactoring.

Refactoring is a disciplined technique for restructuring an existing body of code, altering its internal structure without changing its external behavior. Its heart is a series of small behavior preserving transformations.

We identified a bug (if the current parent is the value to setParent, an exception is raised), identified a fix to the bug (check if the problematic condition exists), and implemented that fix by changing the function's internal structure. Fixing bugs is not an intrinsic part of refactoring, though it's often a motivation.

Making these small deliberate changes to our code facilitates building reliable modules that we can grow and maintain. Refactoring includes removing duplication, enhancing clarity, and making the code easier to work with. The importance of the practice cannot be understated.

To truly do safe refactoring, we'd need a full suite of automated tests to run, to make sure the refactoring does not break existing functionality. We'll build these tests where we can (such as in the minspect._py_to_helpstr example in Chapter 1, Introspecting Maya, Python, and PyMEL), but much of the book is void of them. Unit tests can be found in the code accompanying this book, along with instructions on how to run the tests. Refer to Appendix, Python Best Practices, for more information about acquiring and running the tests for the code in this book.

Simplifying the node to joint conversion

The next area of our implementation that needs work is the copying of attributes from the old object to the new joint. This is an area that will likely change as new features are added. For example, if this library were part of an auto-rigger, the dummy locators that define a pre-rigged character may have important custom attributes that need to be transferred to the joints.

Let's perform the highlighted refactoring, creating the _convert_to_joint function from code originally in the convert_to_skeleton function. The extracted code is highlighted in the following listing.

def _convert_to_joint(node, parent, prefix):
 j = pmc.joint(name=prefix + node.name())
 safe_setparent(j, parent)
 j.translate.set(node.translate.get())
 j.rotate.set(node.rotate.get())
 return j

def convert_to_skeleton(rootnode, prefix='skel_', _parent=None):
    if _parent is None:
        _parent = rootnode.getParent()
 j = _convert_to_joint(rootnode, _parent, prefix)
    for c in rootnode.children():
        convert_to_skeleton(c, prefix, j)
    return j

Now when additional attributes need to be copied, locked, or reconnected, we have a good place to put them. For example, let's suppose we want to color our joints depending on their position on the x axis. We have a clear home for the changes, which are highlighted in the following listing.

GREEN = 14
BLUE = 6
YELLOW = 17

def _convert_to_joint(node, parent, prefix):
    j = pmc.joint(name=prefix + node.name())
    safe_setparent(j, parent)
    j.translate.set(node.translate.get())
    j.rotate.set(node.rotate.get())
 x = j.translateX.get()
 if x < 0.001:
 col = GREEN
 elif x > 0.001:
 col = BLUE
 else:
 col = YELLOW
 j.overrideColor.set(col)
    return j

You can, of course, put this choosing and setting of the wire color into a function. It would help keep things organized. So let's refactor this implementation just a small bit, creating a calc_wirecolor function nested inside of the _convert_to_joint function.

def _convert_to_joint(node, parent, prefix):
    j = pmc.joint(name=prefix + node.name())
    safe_setparent(node, parent)
    j.translate.set(node.translate.get())
    j.rotate.set(node.rotate.get())
 def calc_wirecolor():
        x = j.translateX.get()
        if x < 0.001:
            return GREEN
        elif x > 0.001:
            return BLUE
        else:
            return YELLOW
    j.overrideColor.set(calc_wirecolor())
    return j

Now when we add more code, we can just put it into tiny nested functions similar to calc_wirecolor, instead of polluting the module globals with single-use functions. This has the added and important benefit of keeping the code as close as possible to where it's actually used. Of course, if you need a nested function in multiple scopes, you should pull it into a more accessible place.

Learning how to use closures

In the preceding example, we use the variable j inside the calc_wirecolor function, even though it is not a parameter. This calc_wirecolor function is called a closure, also called a nested function or inner function. We create a closure when a function closes over an outside variable (or more loosely, whenever we define a function inside a function or method). The formal definition of a closure sounds very technical, but we only need to understand this: because j is in the scope of calc_wirecolor (sort of like a global variable is accessible from within a function), it can be used inside the function. Using closures is an incredibly powerful technique. We'll use and explore it more throughout this book.

If you're stumped by closures, this is an area where thinking too hard is a drawback. In the following code, which does not use a closure, the spamneggs function can obviously refer to the spam function.

>>> def spam(adjective):
...     return adjective + ' spam'
>>> def spamneggs(adjective):
...     return spam(adjective) + ' + eggs'
>>> spamneggs('Boring')
'Boring spam + eggs'

We can also just move spam into spamneggs, as we've done with the calc_wirecolor and _convert_to_joint functions. Nested functions in Python work great!

>>> def spamneggs(adjective):
...     def spam(adjective):
...         return adjective + ' spam'
...     return spam(adjective) + ' + eggs'
>>> spamneggs('Decent')
'Decent spam + eggs'

Finally, we can just get rid of the duplication of the adjective parameter in spam and just have it use the adjective argument passed into spamneggs:

>>> def spamneggs(adjective):
...     def spam():
...         return adjective + ' spam'
...     return spam() + ' + eggs'
>>> spamneggs('Wonderful')
'Wonderful spam + eggs'

Closures are a truly important feature in Python, along with every other language that supports them. You should become familiar with them over time. If you're not comfortable yet, just plan on getting there. There are definitely some gotchas (don't create closures inside of Python loops!) but closures can really simplify your programs.

Dealing with node connections

Dealing with node connections is a common source of bugs when manipulating nodes. Often we write code that treats a node as a standalone entity, something we can reason about in the abstract. Nodes are, however, part of a potentially complex network of other nodes, of history, and of internal state.

It's very important that when we change the state of a node, especially if we are copying or deleting it, we consider its connections. In this case, we are just creating a new node and copying the value of certain attributes from the source, so we need not worry about connections.

Dealing with namespaces

In contrast to handling connections, which are an essential complexity in Maya, Maya namespaces are a hideous and constant nuisance; a true accidental complexity.

Tip

Accidental and essential complexities are terms in Fred Brooks' famous essay, No Silver Bullet. Accidental complexity is caused by our approach to a problem (an example would be MEL), while essential complexity is inherent in the problem being solved and is unavoidable (an example would be managing a hierarchy of objects).

For our skeleton converter, in what namespace do we want the newly created joints to be placed? Our three options are as follows:

  • The root namespace. This doesn't seem very correct so we won't bother considering it.
  • The namespace of the source object.
  • The current namespace.

Though the second option is a valid design choice, we'll avoid it and choose the third option for two reasons. First, the second option is more code. Putting something in the current namespace happens automatically, so we don't need to write any extra code. Second, any non-default behavior is a decision that our code needs to make and manage; by not making a decision in our function, we let the caller make a decision, or let the caller let its caller make a decision, and so on. This is much more in the spirit of this chapter. Think about our discussion of contracts and composability earlier. Prefer to write the simplest code possible.

Wrapping up the skeleton converter

It turns out that our skeleton converter is relatively simple, just a few dozen lines of code. There's nothing complex here, nothing that would trick another programmer (or yourself) several years in the future. And though we'll use our skeleton converter for our character creator in the next section, nothing stops a programmer from using it for their own purposes.

The following listing is the contents of skeletonutils.py so far.

GREEN = 14
BLUE = 6
YELLOW = 17

def _convert_to_joint(node, parent, prefix):
    j = pmc.joint(name=prefix + node.name())
    safe_setparent(node, parent)
    j.translate.set(node.translate.get())
    j.rotate.set(node.rotate.get())
    def calc_wirecolor():
        x = j.translateX.get()
        if x < 0.001:
            return GREEN
        elif x > 0.001:
            return BLUE
        else:
            return YELLOW
    j.overrideColor.set(calc_wirecolor())
    return j

def convert_to_skeleton(rootnode, prefix='skel_', _parent=None):
    """Converts a hierarchy of nodes into joints that have the
    same transform, with their name prefixed with 'prefix'.
    Return the newly created root node.

    :param rootnode: The root PyNode.
      Everything under it will be converted.
    :param prefix: String to prefix newly created nodes with.
    """
    if _parent is None:
        _parent = rootnode.getParent()
    j = _convert_to_joint(rootnode, _parent, prefix)
    for c in rootnode.children():
        convert_to_skeleton(c, prefix, j)
    return j

The key takeaway from the skeleton creator code is this:

Write the absolute simplest code you can for as much of your code as you can. The fastest code is the code which does not run. The code easiest to maintain is the code that was never written. Defer decisions to callers unless they are an important part of a contract.

As we'll see in the next example and beyond, there will still be complexity and we still need to write code. Decisions still need to be made. But we should make them in the best place possible.

主站蜘蛛池模板: 杭锦后旗| 宁陵县| 康定县| 油尖旺区| 北流市| 潼关县| 教育| 正定县| 且末县| 德江县| 桃江县| 肥东县| 绍兴县| 灵璧县| 宜城市| 宜昌市| 溧水县| 房产| 石屏县| 东乡县| 汶川县| 明水县| 绵阳市| 法库县| 南汇区| 应城市| 尼木县| 高邑县| 石家庄市| 滨州市| 廉江市| 澄江县| 遂宁市| 察雅县| 石阡县| 德阳市| 玉溪市| 布拖县| 景洪市| 尚志市| 长岭县|