Wednesday, February 25, 2009

Maya and Python lambdas, part 3

A thought occurred to me this morning before coming into work. I was still thinking that there had to be some way of further improving my use of lambdas in Maya GUI creation. Then it hit me: assign the data from the loop as a default value for one of the lambda's arguments!

import maya.cmds as mc

def showStuff (stuff):
print "You've given me %s!" % stuff

stuffList = ["an apple", "a pear", "a pickle"]
mc.window("Stuff to Give", w=300, h=200)
cl = columnLayout()
for item in stuffList:
mc.button(l=item, c=lambda x, i=item:showStuff(i))

Look, lambda factory! :) Here's my guess as to why this works when it doesn't work to put the variable "inside" the lambda...

By stuffing a variable into the function/method call inside the lambda function, it appears that the value of the variable isn't retrieved until the lambda function is executed. For a variable defined by a loop, this means that the variable value is the same as it was during the last iteration through the loop.

However, by taking a variable and assigning it as the default for one of the lambda's arguments, the value assigned to that variable is retrieved and stored in the lambda function definition. When called by Maya when the appropriate GUI element is used, the lambda function already has that value, and knows to pass it as the default for the appropriate argument. Because no other data is passed to replace it, it can be used reliably inside the function as part of the real function call we want to make.

The only thing to keep in mind when using this technique is the extra data that Maya passes on its own, which is caught (and promptly ignored) in the example above by the x argument.

Clean and simple. Me likey.

Tuesday, February 24, 2009

More Lambda and GUI fun

Just tripped over an interesting problem with the lambda stuff I shared in the last post. It's just peachy if you're passing a literal value:

# other code omitted for brevity
mc.button(l="click me", c=lambda x:colorMe("Purple"))

However, if you're creating a collection of controls using a loop, it no worky correctly:

names = ["me", "you", "him"]
for name in names:
mc.button(l=name, c=lambda x:nameMe(name)

In this example, no matter which button you click, it will pass "him".

I tried a number of ways to get around this, and wasn't successful until I revisited the page that flipped the lambda light switch for me. The second example on that page shows how to create a "lambda factory" of sorts. In the context of the loop situation, the factory serves to isolate the creation of the lambda function from the loop. While the factory function was a standalone item in the example on that page, it would be convenient to nest said factory inside the same function/method that contains the loop. Here's a more fleshed-out example:

import maya.cmds as mc

def addButtons (names):
# here's the factory function
def factory (nm):
return lambda x:showName(nm)

# and here's our loop
for name in names:
mc.button(l=name, c=factory(name))

It's a bit more extra code than I'd hoped for, but it's only needed when using the lambda technique inside a loop, and still allows me to keep the target functions/methods clean by avoiding nesting.

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.button(l="Touch!", c=blah)

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.button(l="Touch!", c=blah(10))

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.button(l="Touch!", c=lambda x:blah(10))

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.intSlider(dragCommand=lambda x:blah(int(x)))

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!

Monday, February 16, 2009

Hardware woes, begone!

Okay, I think I've finally finished fiddling with this finicky figurer (the only synonym for "computer" that I could find that started with F).

After my last post, I ran across some other info that said that the power supply might be to blame, and I began seeing other evidence to support that theory. Shortly thereafter the computer just refused to boot, so I had to do something. With the exception of the first item below, most of my attempted somethings were done this past Saturday...
  • Bought a beefier power supply. Still nothing.
  • Bought a new motherboard. Got it home and found that it didn't have enough slots to take all my existing RAM sticks.
  • Exchanged the motherboard for a newer one that required a new CPU (picked a nice energy-efficient dual-core). Got home and found that the location of all the externals (keyboard and audio hookups, plus the location of the PCI slots) wouldn't work with my case.
  • Trekked to Best Buy for a new case. I was tired of messing with hardware, so I paid them to move all the guts from the old box to the new one while we went and had a nice Valentine's Day family dinner.
  • Came back a few hours later to find that my old RAM wouldn't work with the new motherboard, and that the board only had one IDE connector, so the DVD drive had no place to connect. Bought new RAM and a new SATA DVD drive, and had the Geek Squad install both. Brought everything home.
  • Nothing booted. Found that the hard drives were connected in the wrong order. Had to swap their positions in the case because of the awkwardness of the IDE cable.
  • Found that the DVD drive wasn't being recognized because it was connected to the wrong SATA connector on the motherboard.
  • Booted into BIOS, set everything up, then started to boot into Windows. Had to re-authorize Windows, which ended up taking several phone calls to MS. Thankfully I brought the old machine home, or I would not have had access to the product key label that was conveniently stuck to the back of the case.
Based on information that I read in some of the articles I found online after the last post, I thought that I would have to run a repair install of Windows in order to get it to play nice with the new hardware. Once the re-authorization was finished, though, Windows booted up without any issues and just started bugging me about all the new hardware it was finding. I haven't had any BSOD's or any noticeable system hiccups.

In the end, all this hardware futzing cost about the same as (or more likely more than) a new MAChine (*ahem*), and in the end I wound up with essentially a new machine, so I guess it kinda served its purpose. Our key concern was getting access to all the data on my hard drives again (especially financial data). It also got us thinking about where we want some of said data to reside for better long-term, computer-independent access.

So there we go! Done!

I hope!