Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to create an array of functions which partly depend on outside parameters? (Python)

Tags:

python-3.x

I am interested in creating a list / array of functions "G" consisting of many small functions "g". This essentially should correspond to a series of functions 'evolving' in time.

Each "g" takes-in two variables and returns the product of these variables with an outside global variable indexed at the same time-step.

Assume obs_mat (T x 1) is a pre-defined global array, and t corresponds to the time-steps

G = []

for t in range(T):
    # tried declaring obs here too.               
    def g(current_state, observation_noise):
        obs = obs_mat[t] 
        return current_state * observation_noise * obs 

G.append(g) 

Unfortunately when I test the resultant functions, they do not seem to pick up on the difference in the obs time-varying constant i.e. (Got G[0](100,100) same as G[5](100,100)). I tried playing around with the scope of obs but without much luck. Would anyone be able to help guide me in the right direction?

like image 727
Azmy Rajab Avatar asked Mar 16 '23 18:03

Azmy Rajab


2 Answers

This is a common "gotcha" to referencing variables from an outer scope when in an inner function. The outer variable is looked up when the inner function is run, not when the inner function is defined (so all versions of the function see the variable's last value). For each function to see a different value, you either need to make sure they're looking in separate namespaces, or you need to bind the value to a default parameter of the inner function.

Here's an approach that uses an extra namespace:

def make_func(x):
    def func(a, b):
        return a*b*x
    return func

list_of_funcs = [make_func(i) for i in range(10)]

Each inner function func has access to the x parameter in the enclosing make_func function. Since they're all created by separate calls to make_func, they each see separate namespaces with different x values.

Here's the other approach that uses a default argument (with functions created by a lambda expression):

list_of_funcs = [lambda a, b, x=i: a*b*x for i in range(10)]

In this version, the i variable from the list comprehension is bound to the default value of the x parameter in the lambda expression. This binding means that the functions wont care about the value of i changing later on. The downside to this solution is that any code that accidentally calls one of the functions with three arguments instead of two may work without an exception (perhaps with odd results).

like image 173
Blckknght Avatar answered May 11 '23 14:05

Blckknght


The problem you are running into is one of scoping. Function bodies aren't evaluated until the fuction is actually called, so the functions you have there will use whatever is the current value of the variable within their scope at time of evaluation (which means they'll have the same t if you call them all after the for-loop has ended)

In order to see the value that you would like, you'd need to immediately call the function and save the result.

I'm not really sure why you're using an array of functions. Perhaps what you're trying to do is map a partial function across the time series, something like the following?

from functools import partial

def g(current_state, observation_noise, t):
    obs = obs_mat[t]
    return current_state * observation_noise * obs

g_maker = partial(g, current, observation)
results = list(map(g_maker, range(T)))

What's happening here is that partial creates a partially-applied function, which is merely waiting for its final value to be evaluated. That final value is dynamic (but the first two are fixed in this example), so mapping that partially-applied function over a range of values gets you answers for each value.

Honestly, this is a guess because it's hard to see what else you are trying to do with this data and it's hard to see what you're trying to achieve with the array of functions (and there are certainly other ways to do this).

like image 36
erewok Avatar answered May 11 '23 15:05

erewok