I am having trouble building a Python function that launches TkInter objects, with commands bound to menu buttons, using button specifications held in a dictionary.
SITUATION
I am building a GUI in Python using TkInter. I have written a Display class (based on the GuiMaker class in Lutz, “Programming Python”) that should provide a window for data entry for a variety of entities, so the commands and display of any instance will vary. I would like to configure these entity-specific commands and displays in a dictionary file. The file is evaluated when the script is run, and its dictionary passed to a launcher function when a Display instance is called for. But the instances’ commands can’t find the instance methods I’m trying to bind to them.
SPECIFICATION IN FUNCTIONS WORKS
This isn’t a problem when the Display instance is launched with configurations specified in a dedicated function. For instance this works fine:
def launchEmployee():
display = ''
menuBar = [('File', 0, [('Save', 0, (lambda: display.onSave()))])]
title = 'Employee Data Entry'
display_args = {'title': title,
'menuBar': menuBar}
display = DisplayScreen(**display_args)
The DisplayScreen subclasses from the GuiMaker, which has methods for processing the menuBar object to create a menu. It has an onSave() method.
Done this way, the Display instance finds and runs its own onSave() method when the ‘Save’ button is clicked.
SPECIFICATION FROM DICTIONARY FILE DOESN’T WORK
But this doesn’t work when I try to launch the Display instance from a launcher function, pulling its specification from a dictionary held in a separate file.
config_file:
{'menuBar':[('File', 0, [('Save', 0, (lambda: display.onSave()))])],
'title': 'Employee Data Entry'}
script file:
config = eval(open('config_file', 'r').read())
def launchDisplay(config):
display = ''
display = DisplayScreen(**config)
Run this way, clicking ‘Save’ generates an error, saying there is no global object ‘display’.
THEORY: DICTIONARY CASE LOOKS FOR OBJECTS IN SCOPE AT EVAL() CALL
I speculate that in the function case, ‘display’ is a string object, whose lack of the method onSave() isn’t a problem for the the assigment to menuBar because its examination for the method is deferred inside the lambda function. When the Display instance is assigned to the ‘display’ object, this overloads the prior assignment of the string object, but Python still knows about ‘display’ and goes to it when asked for its onSave() method.
If so, the configuration case is failing because the ‘display’ object doesn’t exist at all when the config dictionary is created by evaluation. This doen’t cause an error at the eval() call because, again, the lambda function hides the object from inspection until called. But when called, Python goes looking for the ‘display’ object in scope at the moment of the eval() call, where it finds nothing, and then reports an error.
BUT: PUTTING EVAL() CALL IN SCOPE DOESN’T HELP
But I have tried moving the evaluation of the dictionary file into the function and after the creation of the ‘display’ string object, and this doesn’t work either.
SO:
What is going on here?
How can I specify methods to be bound to commands in a dictionary to be accessed when instantiating the display object? It really seems better to specify these screens in a configuration file than in a host of duplicative functions.
When your
lambdaexecutes is when scope applies, but the issue is a bit subtler.In the first case that
lambdais a nested function oflaunchEmployeeso the Python compiler (when it compiles the enclosing function) knows to scan its body for references to local variables of the enclosing function and forms the closure appropriatelyIn the second case, the
evalhides the nested function from the Python compiler at the time it’s compiling the enclosing function, so it doesn’t even know it’s an enclosing function, nor that it should form a closure, or how.I suggest you don’t play around with names but rather insert the new
displayobject into thelambdaafter instantiating it. That does require finding thelambda(or more than one lambda) but you may do it by systematically (e.g. recursively) walking all items in config’s values, looking for ones which are instances oftype(lambda:0)and adopting some convention such as, ‘the widget being created is referred in those lambdas by the name “widget” and is the last argument (with a default value)’.So you change your config file to:
and, after
display = DisplayScreen(**config), post-processconfigas follows:Admittedly somewhat-tricky code, but I don’t see a straightforward way to do it within your specified context of an
eval‘d string made into adictcontaining somelambdas.I would normally recommend standard library module
inspectfor such introspection, but since this post-processing function must inevitably mess around with thefunc_defaults(inspectonly examines, does not alter objects’ internals like that), it seems more consistent to have all of its code churn at the same, pretty-down-deep level.Edit: a simpler approach is possible if you don’t insist on having widgets be only local variables, but can instead make them e.g. attributes of a global object.
So at module level you have, say:
and in your function you assign the newly made widget to
widgets.foobarinstead of assigning to bare namefoobar. Then yourevaledlambdacan be something like:and everything will be fine, because, this way, no closure is needed (it’s only needed — and none is forthcoming due to the
eval— in your original approach to preserve the local variables you are using for the time thelambdawill be needing them).