-2

I have searched deeply for a solution to this problem, but, being new to Python and tkinter, it is difficult to find solutions to problems in a language you are largely unfamiliar with. I hope this is not a repeat question.

Suppose I create a series of buttons in tkinter, each of which will sequentially display a letter of the alphabet, defined as such:

from string import ascii_uppercase as alphabet
from tkinter import Button
blockLetterButtons = []
for i in range(0,26):
    blockLetterButtons.append(Button(competitorSelectionFrame, 
    text=alphabet[i], command=some_function(alphabet[i])))

Somewhere below this in the for loop is a .grid(...) for all of these.

The function that is being referenced in the command attribute is as the following:

def some_function(letter):
    print(letter)

So I recognize two problems with my code:

  1. The function in the command attribute is executing immediately upon running the code, because I am specifying a function call rather than a function name.
  2. Even if that was the proper way to call the function with a parameter, using alphabet[i] wouldn't work, because it would pass whatever the value of i is upon click, and not whatever i was upon execution of the for loop.

So my questions are the following:

  1. How can I specify a function in the command attribute and pass to that function with a parameter?
  2. How can I specify that parameter to be the letter of the alphabet specific to that button, without defining a button explicitly for every letter?

I am fairly certain I could use a list comprehension to solve the second problem, perhaps a dictionary with the key equal to the letter and the value equal to the button, but then I still wouldn't know how to get the key value from the button on its own.

martineau
  • 119,623
  • 25
  • 170
  • 301
SamBG
  • 270
  • 4
  • 16
  • 3
    `command=lambda i=i: some_function(alphabet[i])` is the idiomatic solution. The lambda makes a function that can be called later without needing any parameters; the parameter `i` with a default captures the value of the outer `i` at the moment the lambda is defined, so each button gets its distinct value. – jasonharper Apr 05 '18 at 02:47
  • That is certainly the idiomatic way, and much more concise than my answer. May I suggest posting that as an answer yourself? – Silvio Mayolo Apr 05 '18 at 02:52
  • @jasonharper This solution worked perfectly, thank you. So the lambda generates a function for each Button, and does so for the current instance of i in the loop. What exactly does i=i do? Is that the parameter for the function created by lambda? – SamBG Apr 05 '18 at 03:34

2 Answers2

0

Currying is the practice of passing some arguments to a function to produce a new function which can be called later but "remembers" its earlier arguments. You can use this technique to pass the state.

def some_function(letter):
    def f():
        print(letter)
    return f

...

command = some_function(alphabet[i])

So some_function(alphabet[i])() would call the function, but only using one set of parentheses simply stores the value for later.

Silvio Mayolo
  • 62,821
  • 6
  • 74
  • 116
  • [`functools.partial` already exists for this purpose](https://docs.python.org/3/library/functools.html#functools.partial). `command=partial(print, alphabet[i])` would work without defining new currying utility functions. – ShadowRanger Apr 05 '18 at 02:54
  • Strictly speaking, this is not currying, but partial application, [I believe](https://en.wikipedia.org/wiki/Currying#Contrast_with_partial_function_application), but I frequently get the two confused anyway... – juanpa.arrivillaga Apr 05 '18 at 02:58
0

You should use a higher order function (a function that returns a function). To demonstrate, I will not use tkinter but the principle is the same:

In [25]: def some_func_maker(letter):
    ...:     def some_func():
    ...:         print(letter)
    ...:     return some_func
    ...:

In [26]: buttons = []

In [27]: from string import ascii_uppercase as alphabet

In [28]: for i in range(26):
    ...:     buttons.append(some_func_maker(alphabet[i]))
    ...:

In [29]: for b in buttons: b()
A
B
C
D
E
F
G
H
I
J
K
L
M
N
O
P
Q
R
S
T
U
V
W
X
Y
Z

Note, you can inline this if you don't want to keep a some_func_maker around using a lambda:

In [37]: buttons = []
    ...: for c in alphabet:
    ...:     buttons.append(
    ...:         (lambda x: lambda: some_function(x))(c)
    ...:     )
    ...:

In [38]: for b in buttons: b()
A
B
C
D
E
F
G
H
I
J
K
L
M
N
O
P
Q
R
S
T
U
V
W
X
Y
Z

A common trick is that is less verbose is to use a default-argument with your lambda, however, I consider this more hacky, although it is quite common in Python:

In [43]: buttons = []
    ...: for c in alphabet:
    ...:     buttons.append(
    ...:         lambda x=c: some_function(x)
    ...:     )
    ...:

In [44]: buttons[0]()
A

In [45]: buttons[1]()
B

In [46]: buttons[-1]()
Z

Note also, it is consider an antipattern to use for i in range(n): some_iterable[i] when you can simply iterate over the iterable instead. Usually, you just want the actual value, so you just do for x in some_iterable.

juanpa.arrivillaga
  • 88,713
  • 10
  • 131
  • 172