Monday, February 23, 2009

Python Lambdas and Maya GUIs

I love epiphanies. :)

For some reason, I haven't been able to figure out "lambda" functions in Python. Granted, I've not put a great deal of time into them. It's just that whenever I would see them in someone else's code, I couldn't immediately figure out what they were doing, so I'd just add another mark next to the "Need to research lambdas" entry in my mental to-do list and move along. To make a long story short, I ran across a page today that flipped the light switch on lambdas. But that's not the epiphany of which I speak. The epiphany hit when I began trying to figure out what (if anything) I could do with those lovely little lambdas in my Python programming at work.

Most of my development work involves the creation of tools with some kind of graphical user interface (GUI). When assigning a command to a Maya GUI element in Python, it expects either a string that contains some Python code to execute, or a pointer to a function or method that will be called. In the vast majority of situations, I'll use the latter option. If I don't need to pass any data to the target function, there's no problem, and Maya gets the function pointer as expected:

import maya.cmds as mc

def blah(*args):
# "*args" is required because even though the
# command doesn't pass any data, Maya passes
# some anyway. Go figure...
print "You touched me!"

mc.window(w=500, h=500)
mc.columnLayout()
mc.button(l="Touch!", c=blah)
mc.showWindow()

However, in most of the GUIs that I create, some control will need to pass specific data to the function that it calls. The problem is that once you include parentheses to pass data to the function, Maya is no longer getting a function pointer. It's getting the value (if any) returned by the called function, or None if the function doesn't return anything.

Up until now, I've been using nested functions to get around this problem. By defining and returning a "dummy" function inside the main function that is called by the GUI element, Maya will get the function pointer it wants, and I can pass in any data that I please:

import maya.cmds as mc

def blah(value):
def b(*args):
# "*args" is still required because this is the
# function that Maya will ultimately call when
# the button is pushed
print "I was given:", value
return b

mc.window(w=500, h=500)
mc.columnLayout()
mc.button(l="Touch!", c=blah(10))
mc.showWindow()

This process has been working fine, but in the back of my mind, I kept hoping to find a more elegant solution.

Enter my new friend: the lambda!

After some experimentation, I learned two very helpful things about lambdas. The first is that the expression evaluated by a lambda doesn't necessarily have to have any connection to the data it is passed. For example, a "normal" lambda definition might look something like this:

x = lambda y: y * 2

In this case, calling x(5) will return 10 (the 5 gets passed to y, when is then evaluated through the expression y*2 to yield 10, which is then returned). However, the expression can be changed to return something that has nothing to do with the value passed in through y, like so:

x = lambda y: 15

In this case, no matter what value you pass, 15 will always be returned.

Based on this example alone, I can already begin to use lambdas to simplify my example code above. (It's simpler on the function definition side of things because we get rid of the nesting issue, but some might see the syntax of assigning the desired function call to the GUI element a tad more confusing.)

import maya.cmds as mc

def blah(value):
print "I was given:", value

mc.window(w=500, h=500)
mc.columnLayout()
mc.button(l="Touch!", c=lambda x:blah(10))
mc.showWindow()

What happens is that the mystery-data passed by Maya gets assigned to x in the lambda definition, but we don't need to use it. All we need is to call our function with the desired value.

For some GUI elements, though, the data passed by Maya is actually useful. Take an intSlider, for example:
import maya.cmds as mc

def blah(value):
print "The slider value is", value

mc.window(w=500, h=500)
mc.columnLayout()
mc.intSlider(dragCommand=lambda x:blah(int(x)))
mc.showWindow()

In this situation, the data passed by Maya when dragging the slider is the slider's value. However, it's passed as a Unicode string, so I just converted it to an integer before passing it to the function. This means I don't have to query the slider, as the data I need has already been passed.

Some GUI operations, like dragging and dropping, pass more than one argument to the target function. No matter...just provide the requisite number of arguments in the lambda definition (i.e. lambda w,x,y,z: ....) and pass them along to the target function as desired. Or, as in one particular case where I wanted to substitute my own data in place of what the drag operation passed, you can take advantage of the other nifty thing I learned through my experiments: a lambda can accept arbitrary argument lists, just like normal functions.

# other GUI code here
mc.button(l="blah, dragCallback=lambda *x:boo(myData))


That's all for now. Happy Python GUI building!

3 comments:

drake said...

You could make use of pymel to ease "callback" stuff.

Here is some snippets about callbacks from pymel:

One common point of confusion when building UIs with python is command callbacks. There are several different ways to handle
command callbacks on user interface widgets.

Function Name as String
~~~~~~~~~~~~~~~~~~~~~~~
Function Object
~~~~~~~~~~~~~~~
Lambda Functions
~~~~~~~~~~~~~~~~

Combining lambda functions with the lessons we learned above adds more versatility to command callbacks. You can choose
exactly which args you want to pass along.

.. python::

from pymel import *

def buttonPressed(name):
print "pressed %s!" % name

win = window(title="My Window")
layout = columnLayout()
name = 'chad'
btn = button( command = lambda *args: buttonPressed(name) )

showWindow()

---------------------
Personally, I use lambda function method with nested function such that I don't need to care about what parameters delivered:

ex,

btn1 = radioButtonGrp(, ... c=lambda *args: myCallback())
def myCallback():
val = btn1.getSelect()
....

Justin said...

Thanks for the comment, Drake! Forgive me if I'm missing something, though, but I don't see how the examples you provided show that pymel makes the callback process easier. It looks like the examples shown make the same points about lambdas that I was making. What is the specific advantage that pymel provides?

drake said...

Sorry for confusing you that.

You are right. Your method and Pymel's suggestion are exactly the same. The difference is that Pymel encapsulate window controls as a class such that you can make programming OO for better understanding :)

Ex. val = btn1.getSelect()