2

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.

Mike Williamson
  • 4,915
  • 14
  • 67
  • 104
  • 1
    I'm trying to get my head around what you are trying to do here. Will you add code to the example under "Issue" to show what you explain in words in the paragraph below that example? Specifically, I need to see how `mythemes` is created/defined. I think this will help clarify what you are asking. – Code-Apprentice Jul 14 '20 at 19:32
  • @Code-Apprentice Done. See the addendum at the end. – Mike Williamson Jul 14 '20 at 19:43
  • 1
    With the answers below, I see that the problem is the `lambda` function and its closure. For more details about how this works, see https://stackoverflow.com/questions/2295290/what-do-lambda-function-closures-capture. The `factory()` function in James` answer provides the expected closure over its argument that `lambda` doesn't do. – Code-Apprentice Jul 14 '20 at 20:35

2 Answers2

2

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))
James
  • 32,991
  • 4
  • 47
  • 70
  • OK! Two birds with one stone! Now I **finally** understand the point of factory functions, which I always heard about and read about, but nothing really clicks until you see a use case. :) Thanks a bunch, @James. – Mike Williamson Jul 14 '20 at 19:52
  • It will be helpful for future readers if you point out the difference between the `factory()` function here and the `lambda` in the OP's code. Of course, you don't want to rehash all the information about closures that is already available, but at least mentioning this term will provide those that are interested with the correct terminology to google it. – Code-Apprentice Jul 14 '20 at 20:38
0

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

themes.register(theme_name, lambda theme_dict=theme_dict: theme_dict)
Paul M.
  • 10,481
  • 2
  • 9
  • 15
  • No, this does not work. Unfortunately, I cannot create a lambda function with any arguments. It must be argument-free as part of `altair`s API. – Mike Williamson Jul 14 '20 at 19:43