85216


環境：Python 3.9.5

下のコードの(1)(2)を入れ替えるとなぜ異なる挙動をするのでしょうか？

[code]

    from tkinter import ttk
    import tkinter as tk
    from tkinter import filedialog
    
    def browse_csvfile(i):
      """
        エクスプローラーによりファイルを選び，そのファイルパスを押されたボタンの真左の
        Entryに挿入する．
      """
      filepaths[i]=filedialog.askopenfilename(filetypes = [('csv file','*.csv'), ], title='load')
      filepath_entrys[i].delete(0, tk.END) 
      filepath_entrys[i].insert(tk.END, filepaths[i])
    
    app = tk.Tk()
    
    main_frm=ttk.Frame(app)
    filepaths = [tk.StringVar() for _ in range(2)]
    filepath_entrys = [ttk.Entry(app, textvariable=i) for i in filepaths]
    [filepath_entrys[i].grid(column=0, row=i, sticky=tk.EW) for i in range(2)]
    filepath_entrys[0].focus()
    
    # (1)
    load_buttons = [ttk.Button(app, text="push", command=lambda:browse_csvfile(i)) for i in range(2)]
    
    # (2)
    # load_buttons = [0,0]
    # load_buttons[0]=ttk.Button(app, text="push", command=lambda:browse_csvfile(0))
    # load_buttons[1]=ttk.Button(app, text="push", command=lambda:browse_csvfile(1))
    
    [load_buttons[i].grid(column=1, row=i) for i in range(2)]
    
    app.mainloop()
    
[/code]

* * *

上のプログラムを実行すると，下の画像のようになります．  
(1)の場合では上下どちらのボタンを押しても下のEntryボックスにしかパスが代入されませんでしたが，(2)では欲しい動作をしてくれました．

[![GUI](https://i.stack.imgur.com/SZXXD.png)](https://i.stack.imgur.com/SZXXD.png)

なにか初歩的な思い違いをしているかもしれませんが，なぜこのようになるか全く思いつきません．  
異なる挙動の原因がわかる方いらっしゃいましたらご教示ください．


CC BY-SA 4.0
2021-12-18T11:40:42.150
85213
2022-11-06T12:42:04.873
2022-11-06T12:42:04.873
50560
50560
question
python python3 tkinter
tkinterのButtonウィジェットをリスト内包表記で複数作ると，Button押下時の挙動がおかしくなる


`browse_csvfile` 関数に `print(i)` を挿入してみると判るかと思いますが、上下どちらのボタンを押しても `1` が表示されます。

[code]

    def browse_csvfile(i):
      """
        エクスプローラーによりファイルを選び，そのファイルパスを押されたボタンの真左の
        Entryに挿入する．
      """
      print(i)
      filepaths[i]=filedialog.askopenfilename(filetypes = [('csv file','*.csv'), ], title='load')
                             :
    
[/code]

問題点は、`ttk.Button` の `command` に `lambda` 式を指定していることです。`lambda` 式は closure
であるために、変数 `i` は list comprehension のスコープ内に存在し続けることになります。そして、
**ボタンがクリックされた時点で評価される** ことになるわけですが、その時には既に list comprehension
の繰り返し処理は完了していますので `i` の値は `1` になっているわけです。

[code]

    load_buttons = [ttk.Button(app, text="push", command=lambda:browse_csvfile(i)) for i in range(2)]
    
[/code]

これを解決するには`functools` の `partial` 関数を使います。

[code]

    from functools import partial
    load_buttons = [ttk.Button(app, text="push", command=partial(browse_csvfile, i)) for i in range(2)]
    
[/code]

この場合、変数 `i` は list comprehension の時点で評価されます。つまり、上のボタンをクリックすると
`browse_csvfile(0)`, 下のボタンをクリックすると `browse_csvfile(1)` が実行されることになります。


CC BY-SA 4.0
2021-12-18T16:31:28.273
85216
2021-12-18T16:31:28.273
47127
85213
answer

[code]

    # (1)
    load_buttons = [ttk.Button(app, text="push", command=lambda :browse_csvfile(i)) for i in range(2)]
    
[/code]

の

[code]

    command=lambda :browse_csvfile(i)
    
[/code]

の部分は

[code]

    def func():
      browse_csvfile(i)
    
    command=func
    
[/code]

と同義です．`load_buttons`定義時ではボタンに`func`関数を結びつけただけであり，`i`は参照されません．ボタンが押されると`func`を経由して初めて`browse_csvfile(i)`を呼び出します．ただし，このときには既に`i=1`となっており，どちらのボタンを押しても`browse_csvfile(1)`が実行されてしまいます．

したがって，所望の結果を得るためには`lambda`部を以下のように，  
`lambda
arg=i:browse_csvfile(arg)`とすることでも，ボタン定義時に各ボタンの`command`に異なる引数を与えることができます．

[code]

    load_buttons = [ttk.Button(app, text="push", command=lambda arg=i:browse_csvfile(arg)) for i in range(2)]
    
[/code]


CC BY-SA 4.0
2021-12-19T04:48:52.117
85222
2021-12-19T04:48:52.117
50560
85213
answer