Title: How to avoid recomputing several variables without completely refactoring code
Tags: python,optimization,design-patterns,functional-programming

Question: I have a category of functions 'detectors', that, given a video, a space scale parameter and a time scale parameter, detect some interest points. I wrote a 'MultiscaleDetector' function, that basically takes a list of space scale parameters and a 'detector' function as parameters and executes the given detector factor for every scale.
This is how they look:
def GaborDetector(v, sigma, tau, threshold, num_points):
    """
    Gabor Detector
    
    Keyword arguments:
    video -- input video (y_len, x_len, frames)
    sigma -- Gaussian kernel space standard deviation
    tau -- Gaussian kernel time standard deviation
    kappa -- Gabor response threshold
    """
    # setup video
    video = v.copy()
    video = video.astype(float)/video.max()
    video = video_smoothen_space(video, sigma)
    # first define a linspace of width -2tau to 2tau
    time = np.linspace(-2*tau, 2*tau, int(4*tau+1))
    omega = 4/tau
    # define the gabor filters
    h_ev = np.exp(-time**2/(2*tau**2)) * np.cos(2*np.pi*omega*time)
    h_od = np.exp(-time**2/(2*tau**2)) * np.sin(2*np.pi*omega*time)
    # normalize the L1 norm
    h_ev /= np.linalg.norm(h_ev, ord=1)
    h_od /= np.linalg.norm(h_od, ord=1)
    # compute the response
    response = (scp.convolve1d(video, h_ev, axis=2) ** 2) + (scp.convolve1d(video, h_od, axis=2) ** 2)
    points = interest_points(response, num=num_points, threshold=threshold, scale=sigma)
    return points

def MultiscaleDetector(detector, video, sigmas, tau, num_points):
    """
    Multiscale Detector

    Executes a detector at multiple scales. Detector has to be a function that
    takes a video as input, along with other parameters, and returns a list of interest points.

    
    Keyword arguments:
    detector -- function that returns interest points
    video -- input video (y_len, x_len, frames)
    sigmas -- list of scales
    """
    
    # for every scale, compute the response
    points = []
    for sigm in sigmas:
        found = detector(video, sigm, tau)
        points.append(found)

    # filter the points, currently irrelevant

As you can maybe already notice, there is some heavy computation done inside the "GaborDetector" function, that depends only on the time parameter. Therefore, the "MultiscaleDetector" function unnecessarily recomputes these variables in each call.
In an attempt to avoid refactoring the code, I came up with what I wished was a nice trick, but believed it would be vain:
def MultiscaleDetector(detector, video, sigmas, tau, num_points):
    """
    Multiscale Detector

    Executes a detector at multiple scales. Detector has to be a function that
    takes a video as input, along with other parameters, and returns a list of interest points.

    
    Keyword arguments:
    detector -- function that returns interest points
    video -- input video (y_len, x_len, frames)
    sigmas -- list of scales
    """
    
    optimization_trick = lambda s_param: detector(video, s_param, tau)
    # for every scale, compute the response
    points = []
    for sigm in sigmas:
        found = optimization_trick(sigm)
        points.append(found)

    # filter the points, currently irrelevant

I hoped that the variables dependent only on tau would remain "stored" somehow in the "optimization_trick" and avoid being recomputed. However, when I timed the different implementations, the difference was around 0.02 seconds, with the "optimized" function being faster.
EDIT:
The actual call that takes place is the following:
# read the video
video = read_video(video_name, num_frames, 0)
# get the interest points
detector = lambda v, s, t: GaborDetector(v, s, t, 0.3, 500)
scales = [3*(1.1)**i for i in range(6)]
start = time.time()
points = MultiscaleDetector(detector, video, scales, 1.5, 500)
end = time.time()
print("Time: {}".format(end-start))

I tried putting all the variables that i wanted to avoid in a different function like this:
def GaborDetectorTrial(v, sigma, tau, threshold, num_points):
    """
    Gabor Detector
    
    Keyword arguments:
    video -- input video (y_len, x_len, frames)
    sigma -- Gaussian kernel space standard deviation
    tau -- Gaussian kernel time standard deviation
    kappa -- Gabor response threshold
    """
    @lru_cache(maxsize=None)
    def time_function(tau):
        time = np.linspace(-2*tau, 2*tau, int(4*tau+1))
        omega = 4/tau
        # define the gabor filters
        h_ev = np.exp(-time**2/(2*tau**2)) * np.cos(2*np.pi*omega*time)
        h_od = np.exp(-time**2/(2*tau**2)) * np.sin(2*np.pi*omega*time)
        # normalize the L1 norm
        h_ev /= np.linalg.norm(h_ev, ord=1)
        h_od /= np.linalg.norm(h_od, ord=1)
        return h_ev, h_od
    # setup video
    video = v.copy()
    video = video.astype(float)/video.max()
    video = video_smoothen_space(video, sigma)
    # compute the response
    h_ev, h_od = time_function(tau)
    response = (scp.convolve1d(video, h_ev, axis=2) ** 2) + (scp.convolve1d(video, h_od, axis=2) ** 2)
    points = interest_points(response, num=num_points, threshold=threshold, scale=sigma)
    return points

The variables that i want to avoid recomputing are h_ev and h_od. The execution time was basically the same (actually, some ms slower).

Answer: You must define the to-be-cached function (time_function) in an appropriate scope, here: outside of GaborDetectorTrial - as it is in your code above, the time_function's life ends each time GaborDetectorTrial returns, and so does its cache's.