Is it possible to create a function that returns a static value generated upon creation, not upon calling the function?

Problem Statement:

How do I create a function that will pass a value that is determined upon creation of the function, and not upon calling the function?


Background

Using altair I am trying to set up multiple themes, and in order to create a theme, I need to "register" it with code something like this:

def black_marks():     return {         'config': {             'mark': {                 'color': 'black',                 'fill': 'black'             }         }     }  # register the custom theme under a chosen name alt.themes.register('black_marks', black_marks)  # enable the newly registered theme alt.themes.enable('black_marks') 

At this point, since 'black_marks' is both registerd and enabled, any future use of altair will include those defaults.

Notice that the function black_marks returns a dict, and it is the function which needs to be registered.


Issue

I have several themes that I would like to set up at once, so I have created a loop through these configurations something like this in my code:

for theme_name, theme_dict in mythemes.items():     themes.register(theme_name, lambda: theme_dict) 

Then I discovered that the themes are not actually being registered at the time the themes.register function is being called. Instead, it is handled "lazily".

For instance, let’s say that the theme keys are: ['light', 'dark', 'paper']. Once I have completed the loop above, I discovered that all 3 of these theme names are registered (I can alt.themes.enable('light'), for instance), but they all point to the last one, paper. This is happening because theme_dict, in the final round, is indeed pointing to the theme associated with paper.


Desire

What I would like to do is to be able to somehow "hard code" what theme_dict is pointing to, so that themes.register points to a function which contains the dict that was generated on each pass. But no matter how I have tried to think about this, I cannot have the lambda function create a function that is "set in stone". Instead, the function will just return whatever is in the last iteration.

So, while this is sort of an altair-specific problem, I feel like the solution should be a generic one:

How do I create a function that will pass a value that is determined upon creation of the function, and not upon calling the function?


Fuller Example

As per requested in the comment, here is a fuller snapshot of my code:

for theme_key, theme_file in THEME_FILES.items(): # THEME_FILES is a dictionary with values containing file paths     with open(theme_file) as theme_fh:         raw_theme = toml.load(theme_fh)     dims = {         "width": raw_theme.pop("width"),         "height": raw_theme.pop("height"),     }     raw_theme["view"] = dims     final_theme["config"] = raw_theme     themes.register(theme_key, lambda: final_theme) 

(Apologies for the poor variable names… I was frustrated with the problems I was having, and renaming thinking the issue was an accidental overwrite of global vars.)

Again, just to be clear, the theme_key is proper registered. If, for instance, the different keys were ['light', 'dark', 'paper'], then I can see all 3. But all three of them point to the theme made with the last iteration. In this case, the 'paper' theme.

If I were to just iterate over 1 of the themes, it works perfectly fine. So I am fairly confident I have pinpointed the problem.

Asked on July 16, 2020 in Python.
Add Comment
2 Answer(s)

You can use a factory function that will generate a function that returns the theme’s dictionary. The dictionary will be captured in the closure of the returned function.

def factory(x):     def theme():         return x     return theme  for theme_name, theme_dict in mythemes.items():     themes.register(theme_name, factory(theme_dict)) 
Add Comment

If I’m understanding the issue correctly, this should have the desired effect:

themes.register(theme_name, lambda theme_dict=theme_dict: theme_dict) 
Answered on July 16, 2020.
Add Comment

Your Answer

By posting your answer, you agree to the privacy policy and terms of service.