Saturday 11 February 2023

JavaScript arguments vs Python locals()

There's a technique in JavaScript combining the arguments (array-like) object and the spread operator that comes pretty handy. When I have a function receiving multiple parameters and invoking another function with those same parameters, I use the arguments object for convenience, rather than typing again the parameters list. I mean:


function format(name, age, city, country){
    console.log(`formatting: ${name} - ${age} [${city}, ${country}]`);
}

function validate(name, age, city, country){
    if (city == "Paris") {
        console.log("validation OK");
        format(...arguments);   //equivalent to: format(name, age, city, country)
    }
    else {
        console.log("validation KO");
    }
}


validate("Francois", 40, "Paris", "France");
validate("Francois", 40, "Lyon", "France");

// validation OK
// formatting: Francois - 40 [Paris, France]
// validation KO



One could wonder what happens if we modify the value of one of the function arguments before invoking the second function? Does the arguments-object that we are passing over reflect those changes? This is an important consideration in some use cases. Well, this is explained in this subsection. In not strict-mode the value gets updated, I mean:


function format(name, age, city, country){
    console.log(`formatting: ${name} - ${age} [${city}, ${country}]`);
}

function enhance(name, age, city, country){
    if (city == "Paris") {
        city = city.toUpperCase()
        age += 2;
        format(...arguments);  
    }
}

enhance("Francois", 40, "Paris", "France");

// formatting: Francois - 42 [PARIS, France]

What about using this same technique in Python? Well, Python does not have an equivalent to the arguments-object, but in many cases the locals function will do the trick.



def format(name, age, city, country):
    print(f"formatting: {name} - {age} [{city}, {country}]")

def validate(name, age, city, country):
    if city == "Paris":
        print("validation OK")
        format(**locals()) # equivalent to: format(name, age, city, country)
    else:
        print("validation KO");


validate("Francois", 40, "Paris", "France")
validate("Francois", 40, "Lyon", "France")

# validation OK
# formatting: Francois - 40 [Paris, France]
# validation KO

locals() returns a dictionary representing the current local symbol table, which includes the function arguments, that's why the above example works. But this means that if our function is a closure, the free vars trapped by the closure are also included, so this technique won't work for closures. There are more limitations. As we define new variables in our function they will be added to the dictionary returned by locals(), so again the technique would fail. Notice that different calls to locals() in one function return the same dictionary (I mean, it points to the same position in memory, the id()). As new variables are defined, they are added as new keys in that dictionary, and if values are updated, they are updated in the dictionary. This means that our second javascript example also works in Python:



def format(name, age, city, country):
    print(f"formatting: {name} - {age} [{city}, {country}]")
    
def enhance(name, age, city, country):
    if city == "Paris":
        city = city.upper()
        age += 2
        format(**locals()); 


enhance("Francois", 40, "Paris", "France")
# formatting: Francois - 42 [PARIS, France]


No comments:

Post a Comment