0

I need to create a pack of callbacks in runtime which use values from a dictionary (one key-value pair per callback). After the loop over the dict, the dict is got lost, but callbacks should have keep the correct values. (It reminds me about scopes and clojures in JavaScript)

Simple python script to reproduce my problem:

cmds = list()

def main():
    values = {'label1':'value1', 'label2':'value2'}

    for l,v in values.items():
        add_command(command = lambda: apply_new_item(l,v))


def apply_new_item(label, value):
    print('{}: {}'.format(label, value))


def add_command(command):
    cmds.append(command)


def call_actions():
    for command in cmds:
        command()


# Create callbacks
main()
# Check callbacks
call_actions()

For the case if I am trying to resolve my problem completely wrong way, I would like to give some context where I need such behavior. I have an application which uses TKinter to create UI. The application should support loading of custom entity configuration, which is python script itself. The requirement for the script is to have one or more functions defined which provide python object of specific structure (which defines custom entity). I load given by user config file as python module and gather all functions with specific names as custom entity providers. Then I call each config provider, convert result to my internal entity configuration and want to add in runtime a menu item which should create an instance from the corresponding entity config (instance creation involves some calls to random, so two entities which are created from the same config are always different). So, I have a dict (name to internal config) with correct values, but when I add TKinter menu item in foreach loop over the dict, all commands point to the last value of iterators:

custom_entities_menu = tk.Menu(entities_menu)
# ...
for name, cfg in custom_configs.items():
    custom_entities_menu.add_command(label = name, command = lambda: self.__create_new_entity(name, cfg))
usr-N
  • 25
  • 1
  • 6
  • It looks like you are suffering from the lambda in a loop problem. The parameters get bound in the wrong way. See: https://stackoverflow.com/questions/19837486/python-lambda-in-a-loop – quamrana Mar 15 '20 at 19:38
  • 1
    You could do something like `lambda name=name, cfg=cfg: self.__create_new_entity(name, cfg)` to capture the current values. – MrBean Bremen Mar 15 '20 at 19:40
  • Thank you @quamrana and MrBeanBremen, you are right. – usr-N Mar 16 '20 at 17:54

1 Answers1

1

Functools or binding arguments may work. My understanding is that functools is more pythonic:

import functools

cmds = list()

def main():
    values = {'label1':'value1', 'label2':'value2'}

    for l,v in values.items():
        add_command(command = functools.partial(apply_new_item, l,v))

def apply_new_item(label, value):
    print('{}: {}'.format(label, value))


def add_command(command):
    cmds.append(command)


def call_actions():
    for command in cmds:
        command()


# Create callbacks
main()
# Check callbacks
call_actions()

It would also work with binding arguments in lambda:

cmds = list()

def main():
    values = {'label1':'value1', 'label2':'value2'}

    for l,v in values.items():
        add_command(command = lambda l=l, v=v: apply_new_item(l,v))

def apply_new_item(label, value):
    print('{}: {}'.format(label, value))


def add_command(command):
    cmds.append(command)


def call_actions():
    for command in cmds:
        command()


# Create callbacks
main()
# Check callbacks
call_actions()

Edit: added binding args in lambda
Edit: moved functools partial to the top because it's more pythonic

E. Bassett
  • 166
  • 7