Using locals() duplicates function arguments

Hello here!

Here is a screenshot of the issue:

And a copy of the code:

import rhinoscriptsyntax as rs

def TestLocals(argument_A = 0, argument_B = "True"):
    argument_A = 1
    argument_B = "False"
    locals()
    return argument_A, argument_B

if __name__=="__main__":
    print "Argument value: " + str(TestLocals())

As you can see, using “locals()” function seems to duplicate both variables (argument_A & argumentB), which is infortunate since after that, I don’t know which version of the same variable is being modified and/or used. Is there a way to avoid this duplication?

Thank you!

Looks like this is a leaky abstraction in IronPython being revealed by the debugger. I don’t see the same behavior in CPython, but I do see it in plain IronPython outside Rhino. (I’m running your script in the REPL, breaking in pdb at the same place as you, and asking pdb to print(locals()).) I don’t think there is much that can be done in Rhino if this is how pdb works in IronPython. Maybe @Alain knows more about this?

The good news is that you only see these new ClosureCell type variables when in the debugger, and in both a normal run and in the debugger everything works as if they were not there. In the function scope, and when the function returns, argument_A/B have their values modified as written (try a print(argument_A) or print(locals()) before your breakpoint). Even when running in the debugger, the function scope cannot access argument_A1.

It’s only in the variables preview of the debugger that these ClosureCell type variables temporarily “replace” the initial arguments.

Why do you need to work with locals() in the debugger?

Hello Mr. Cuvilliers,

Thank you for the answer!

The of locals() is not specific to debug mode; I would like it to work principally in non-debug. Ive made a simplified example here, where locals() has no use.
I use it to dodge another issue, as shown here: "Who called me" Issue

You said that the function scope cannot access argument_A1. That is true if I try to reach it directly using the debugger. However, line 4 of my example did modify argument_A1 (ClosureCell), and did not modify argument_A (int).

That is infortunate because I need the modified version of argument_A. And in use, i’ve seen both modified and unmodified version of argument_A, at line 7. Do you have an idea how I could solve it? I think making a copy of all input arguments (and use those copies thereafter in the function) may solve the case but it is ugly (and much harder to read) as hell!

Here, if I run the test code I provided, I get “Argument value: (1, ‘False’)”, as expected. The bug is hard to reproduce in a simplified case: If I succeed to do so, I’m posting it here.

Of course, solving the initial issue (‘Who calls me issue’) would be even better; making the use of locals() irrelevant.

Thanks again!

Alexandre

Hi Alexandre,

What I meant was that if I run this:

import rhinoscriptsyntax as rs

def TestLocals(argument_A = 0, argument_B = "True"):
    print(argument_A, argument_B)
    print(locals())
    argument_A = 1
    argument_B = "False"
    print(argument_A, argument_B)
    # print(argument_A1)  # This does not work, argument_A1 does not exist
    print(locals())
    locals()
    return argument_A, argument_B

if __name__=="__main__":
    print "Argument value: " + str(TestLocals())

I always get this output, whether I’m running normal or debug mode:

(0, 'True')
{'argument_A': 0, 'argument_B': 'True'}
(1, 'False')
{'argument_A': 1, 'argument_B': 'False'}
Argument value: (1, 'False')

That seems perfectly normal to me and there’s no need to know about the ClosureCell variables implementation details. argument_A value shows its current value at all times.

It is true that when stopping in the debugger and looking at the printout of the variables, you will get new temporary variables that seem to hold the current values of the function arguments, while the original variables do not reflect the changes made to them. Am I correct in understanding that this is only a display issue and that no code relies on accessing these hidden ClosureCell variables?

I’m trying to combine your two posts now, is this how you want them to work?

import inspect
import os
import rhinoscriptsyntax as rs

def GetCallerInfo():
    outerframes = inspect.getouterframes(inspect.currentframe(), 0)
    try:
        callerInfo = outerframes[1][0]
        try:
            f_code = callerInfo.f_code
            functionName = f_code.co_name
            completeFilename = f_code.co_filename
    
            # Remove the .py extension from filename
            filename         = os.path.basename(completeFilename)[0:-3] 
     
            f_locals         = callerInfo.f_locals
            args             = f_locals.items()
              
            argsCount        = f_code.co_argcount
            args             = args[0:argsCount]
        finally:
            del callerInfo
    finally:
        del outerframes

    return filename, functionName, args

def TestLocals(argument_A = 0, argument_B = "True"):
    argument_A = 1
    argument_B = "False"
    locals()  # Remove this line for a different result!
    print GetCallerInfo()
    return argument_A, argument_B

TestLocals()

That does not show the correct result, as you mentioned:

('TempScript', 'TestLocals', [('argument_A', 0), ('argument_B', 'True')])

Removing locals() gets me back to the normal stuff. This gets me back to my original question :), is it possible to remove it in your code?

If not, I would really look into the function decorator I mentioned in the other thread. It won’t print the “current” value of the arguments as they are inside the function body, but I think you could modify it to print these before and after the function runs.

If you really need debug prints of the variables inside the function, I think logging.debug(locals()) is what you need, and the CallerInfo function (or the decorator) can have the role of just printing the variables as they are when entering the function call. Would that work?

It is absolutely possible to remove locals(). It was meant to replace the

f_locals = callerInfo.f_locals

line, which did not work in the original version. Thank you for your help; it is much appreciated!

Alexandre