1

I'd like to pass 2 variable arguments in a function to be assigned to a Button command. These variables change within a for loop that also creates the buttons.

Mostly inspired by some of the top answers here and here, here are my fail attempts at trying to solve this problem based on what I read:

I tried using partial:

self.dct[(i, j)] = Button(command=partial(self.on_click, i, j))

Another try:

self.dct[(i, j)] = Button(command=partial(partial(self.on_click, i), j))

And another:

self.dct[(i, j)] = Button(command=partial(self.on_click, [i, j]))

.. Guess what?

tup = [i, j]
self.dct[(i, j)] = Button(command=partial(self.on_click, tup))

And then, lambda:

self.dct[(i, j)] = Button(command=lambda i=i, j=j: self.on_click(i, j))

Here's my code:

import tkinter as tk

from functools import partial


class Board(tk.Frame):
    board = None
    images = None
    tile = None

    def __init__(self, parent):
        tk.Frame.__init__(self, parent)
        self.tile = {}
        for i in range(10):
            for j in range(10):
                self.tile[(i, j)]['btn_obj'] = tk.Button(self.board, command=partial(partial(self.on_click, i), j))

    def on_click(self, i, j):
        print("X: {}, Y:{}".format(j, i))

partial always causes an error like this:

TypeError: on_click() takes 2 positional arguments but 3 were given

It's always mismatched number of arguments.

Meanwhile, lambda gets the wrong value of the variables, causing something of an error in tkinter's part.

StardustGogeta
  • 3,331
  • 2
  • 18
  • 32
mashedpotatoes
  • 395
  • 2
  • 20

1 Answers1

2

The lambda in your question should work:

tk.Button(self.board, command=lambda i=i, j=j: self.on_click(i,j))

This binds the value i and j as the default values for the lambda parameters.

Personally I prefer lambda over partial, mainly because I don't have to import the functools module. If you wish to use partial, it should look like your first example:

tk.Button(self, command=partial(self.on_click, i, j))

In both cases, on_click will be passed the correct value for i and j based on their values when the button is created.

Here is an example based on your code, but with some unnecessary code removed for clarity:

import tkinter as tk
from functools import partial


class Board(tk.Frame):
    def __init__(self, parent, method):
        tk.Frame.__init__(self, parent, bd=2, relief="sunken")
        for i in range(10):
            for j in range(10):
                if method == "lambda":
                    button = tk.Button(self, command=lambda i=i, j=j: self.on_click(i,j))
                else:
                    button = tk.Button(self, command=partial(self.on_click, i, j))
                button.grid(row=i, column=j)

    def on_click(self, i, j):
        print("X: {}, Y:{}".format(j, i))

root = tk.Tk()
board1 = Board(root, method="lambda")
board2 = Board(root, method="partial")
board1.pack(side="top", fill="both", expand=True)
board2.pack(side="top", fill="both", expand=True)
root.mainloop()
Bryan Oakley
  • 370,779
  • 53
  • 539
  • 685
  • How would I do this with the `bind()` method? I did something like this: `button.bind("", partial(self.on_enter, i, j))` but this error shows up again: `TypeError: on_enter() takes 3 positional arguments but 4 were given` – mashedpotatoes Aug 06 '19 at 05:22
  • @mashedpotatoes: that's a different question, but the technique is precisely the same. You simply have to account for the extra argument sent by tkinter. Here's an example: https://stackoverflow.com/a/28996754/7432 – Bryan Oakley Aug 06 '19 at 13:47