2

I have made a simple "program launcher" in Python. I have a tab delimited text file, with, at the moment, just:

notepad     c:\windows\notepad.exe
write     c:\windows\write.exe

The program reads the textfile and creates an array of objects. Each object has a name property (e.g. notepad) and a route property (e.g. C:\windows\notepad.exe). Then, for each object, a button should be made with the correct name on the button, and clicking the button should execute the correct program using the route.

The program very nearly works. Indeed, the array of objects is formed correctly, because the for loop correctly prints out two different program names, and two different routes. The problem is that both buttons, although labeled correctly, launch the write program ! I believe the problem is arising somewhere in the callback, but my Python knowledge just isn't developed enough to solve this! As you can see from my code below, I have tried an "inline" callback, and with a "runprog" function defined. They both give the same outcome.

Your help would be appreciated.

import Tkinter as tk
import subprocess

class MyClass:
    def __init__(self, thename,theroute):
        self.thename=thename
        self.theroute=theroute

myprogs = []

myfile = open('progs.txt', 'r')
for line in myfile:
    segmentedLine = line.split("\t")
    myprogs.append(MyClass(segmentedLine[0],segmentedLine[1]))
myfile.close()

def runprog(progroute):
    print(progroute)
    subprocess.call([progroute])

root = tk.Tk()
button_list=[]

for prog in myprogs:
    print(prog.thename)
    print(prog.theroute)

    button_list.append(tk.Button(root, text=prog.thename, bg='red', command=lambda: runprog(prog.theroute)))
#    button_list.append(tk.Button(root, text=prog.thename, bg='red', command= lambda: subprocess.call(prog.theroute)))

# show buttons
for button in button_list:
    button.pack(side='left', padx=10)
root.mainloop()

2 Answers2

4

Change your command to look like this:

tk.Button(..., command=lambda route=prog.theroute: runprog(route))

Notice how the lambda has a keyword argument where you set the default value to the route you want to associate with this button. By giving the keyword arg a default value, you are "binding" this value to this specific lambda.

Another option is to use functools.partial, which many people find a little less intimidating than lambda. With this, your button would look like this:

import functools 
...
tk.Button(..., command=functools.partial(runprog,route)

A third option is to move the "runprog" function to the class instead of in the main part of your program. In that case the problem becomes much simpler because each button is tied specifically to a unique object.

tk.Button(..., command=prog.runprog)
Bryan Oakley
  • 370,779
  • 53
  • 539
  • 685
  • Thanks for your help Bryan. I have tried your first suggestion, and python compiles, but when I click on the first button (notepad) I get the long "tkinter callback exception" error which I posted below. I tried changing notepad for chrome and I get the same problem ! So it may be that suggestion is great, and that I have some other problem in my program. Perhaps it doesn't like the first program in my array ? (although it is able to print them out fine when I loop through). I will now try your third suggestion (moving it to the class) – user2094585 Apr 13 '13 at 13:43
  • Hurrah !! It is now all working. The fact, Bryan, that the arrays would print out, yet I still could not get ANY of your suggestions to work, led me to think this might be a white space issue. I added .strip() to the end of the "segmented lines". I am using your third suggestion, which is simplest. Thanks again. – user2094585 Apr 13 '13 at 14:03
-1

Just change this line:

button_list.append(tk.Button(root, text=prog.thename, bg='red', command=lambda: runprog(prog.theroute)))

to:

button_list.append(tk.Button(root, text=prog.thename, bg='red',
          command= (lambda route:(lambda: runprog(route))) (prog.theroute)))

Reasoning: when you create a lambda function (or any other function within a function), it does have access (in Python 2, read-only access) to the variables in the outer function scope. However, it does access the "live" variable in that scope - when the lambda is called, the value retrieved from "prog" will be whatever "prog" means at that time, which in this case will be the last "prog" on your list (since the user will only click a button long after the whole interface is built)

This change introduces an intermediary scope - another function body into which the current "prog" value is passed - and prog.theroute is assigned to the "route" variable in the moment the expression is run. That is done once for each program in the list. The inner lambda which is the actual callback does use the "route" variable in the intermediate scope - which holds a different value for each pass of the loop.

Salomon Zhang
  • 1,553
  • 3
  • 23
  • 41
jsbueno
  • 99,910
  • 10
  • 151
  • 209
  • Thanks jsbueno. I'm not sure I fully understand your explanation, I will need some time to ponder that. I have made the change that you suggest though. The "write" button works (as before), but the "notepad" button gives me this long error which I do not understand: – user2094585 Apr 13 '13 at 10:44
  • Exception in Tkinter callback Traceback (most recent call last): File "C:\Python27_32\lib\lib-tk\Tkinter.py", line 1410, in __call__ return self.func(*args) File "C:\Users\Matt\Desktop\Python\program_launcher3.py", line 28, in – user2094585 Apr 13 '13 at 10:45
  • button_list.append(tk.Button(root, text=prog.thename, bg='red', command= (lambda route:(lambda: runprog(route))) (prog.theroute))) File "C:\Users\Matt\Desktop\Python\program_launcher3.py", line 19, in runprog subprocess.call([progroute]) File "C:\Python27_32\lib\subprocess.py", line 493, in call return Popen(*popenargs, **kwargs).wait() File "C:\Python27_32\lib\subprocess.py", line 679, in __init__ errread, errwrite) File "C:\Python27_32\lib\subprocess.py", line 896, in _execute_child startupinfo) WindowsError: [Error 2] The system cannot find the file specifid – user2094585 Apr 13 '13 at 10:45
  • There's no need for two lambdas. That's an overly complex solution. – Bryan Oakley Apr 13 '13 at 12:25
  • Oh yes, @bryan? So - you solution is just "more magic" not "simpler" - the 2 lambdas make explicit what is happening. Yours freeze the value by cretaing a diffenret function object witht he current value in the "func-defaults" - I don't think it is simpler because of thaty - it is equaly logical, but I think it is harder for novices to understand what really is going on on the way you do it. That said, it is fine to use you rway, and I'd use it in my code - but I see noreason to downvote this answer for that. – jsbueno Apr 15 '13 at 01:30