PyMEL: Value of lambda function changing


#1

Im creating a drop-down menu which loops through some objects, creating a menuItem control for each object. All menuItem controls have a command with a lambda function - which takes the iterator value as argument.

import pymel.core as pm

def myMenu(menu):

    # Delete popup menu contents, as we need to rebuild...
    menu.deleteAllItems()

    # Mesh or components selected?
    sel = pm.filterExpand( selectionMask=(12, 31, 32, 34, 35) )
    if sel != [] and sel != None:
    
        # Get object list and active obj
        objList, someObj = funcA()
        if objList != [] and objList != None:

            # Rebuild the textScrollList
            for item in objList:
            
                # Create menuItem
                pm.menuItem(
                    command=lambda *args: funcB(someObj, item), # Problem here
                    label=item,
                    parent=menu
                )
                
        # Create default button
        pm.menuItem(
            command=lambda *args: funcC(someObj),
            label="Default",
            parent=menu
        )

    else: # No mesh is selected
        pm.menuItem(
            enable=False,
            label="No mesh selected",
            parent=menu
        )

Problem is commented in the middle of the code (# problem here).
Instead of getting say, three menuItem with commands that pass item1, item2 and item3 as variables, every single menuItem gets the last data that was inside the iterator.
I thought that lambda/anonymous functions would get the variable data that you feed them right there and now (ie: when an object with a lambda function has been created - the stuff inside the function wont change). The actual labels of these menuItem controls do get the correct names though.

Can someone explain this behavior?


#2

it’s long to explain it clear why your code doesn’t work. in couple words the lambda function takes not the value you pass but the pointer to variable where this value stored. so all items take the same pointer, which at the end holds the last item.

i would use functools.partial in this case
(like):

from functools import partial
sel = pm.ls(sl=1)
funs = []
def printItem(item, index = 0):
    print item, "=>", index 
    
for item in sel:
    funs.append(partial(printItem, item))
    
for x,fun in enumerate(funs): fun(x) 

see python docs for more details


#3

That was a pretty clear, short answer Denis.
+1 for partial here.

David


#4

Yeah pointers are a new concept for me which I haven’t really worked with previously.
Thank you for the code example! This makes sense.


#5

there is an example shows how ‘lamba args’ can be overwritten:

def foo(val): print "val is", val

vv = [2,]  
 
func = lambda *args: foo(vv[0])
func()
# 2

vv[0] = 3
func()
# 3

it’s almost the same what you did in the loop


#6

its a problem with lambdas in a lot of languages, or more or using closures in many languages. So just be weary of there usage in loops.

if you wanted to do it via a lambda you would need to bind the variable first.

this would work if you want to use a lambda

for item in objList:
  	pm.menuItem(
  		command=lambda x=item, *args: funcB(someObj, x),
  		label=item,
  		parent=menu
  	)
  

when you do it this way you are creating a new variable in the scope of the lambda to use that will properly be re-declared when the next iteration happens.

but you are likely best off to just use functools.partial


#7

never used this trick. and i like it! thanks


#8

Yeah default arguments are evaluated when functions are created, which gives the wanted result in this case.