質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

ただいまの
回答率

88.81%

python tkinter 「ラベルの文字をクリックするとその文字をprint()で表示する」ことをしたい

解決済

回答 2

投稿

  • 評価
  • クリップ 1
  • VIEW 1,170

hikaru632

score 21

実現したいこと

「ラベルの文字をクリックするとその文字をprint()で表示する」ということをしたいです。

困っていること

for in でリストから取り出した文字を表示させるとき、
ラベルではうまく取り出した文字を表示させるのに対して、
そのラベルをクリックしたときにprint()で表示させる文字は、
どの文字をクリックしても、リストの一番最後になってしまいます・・・

どうやったら、クリックして表示させる文字をラベルの文字と同じにできるのでしょうか。
また、これはどうしてこうなってしまったのでしょうか。

ソースコード

import tkinter as tk

root = tk.Tk()
root.geometry("200x400")

word_list = ["あ","い","う","え","お","か","き","く","け","こ"]

#ラベルの文字を押すとクリックされた文字を表示する(はずだった)関数の定義
def click_word(event) :
    print(word)

for word in word_list :
     label = tk.Label(root, text = f"{word}", font = ("",20))
     label.pack()
     label.bind("<Button-1>", lambda event : click_word(event))

root.mainloop()

補足

関数の定義「def ~」をfor in の中に書いてみても、同じようになりました。
関数を定義するときに引数に「word」を渡してみても、同じようになりました。

実際に作っているものはこれではないのですが、実際に作っているもののコードをのせると長いので、ここのソースコードは同じ状況を作ったものをのせています。
ラベルの文字をクリックすると何かの処理をするのに、ラベル一つ一つに対して関数を作ればうまくいくのかもしれませんが、実際に作っているものは、リストの要素の中で条件を満たしたものだけをラベルで表示し、
それをクリックして何かの処理をするといった感じで、条件を満たしたものが何なのか、数はいくつなのかがわからないので、このやり方でやろうと思いました。
プログラミング教室などに通っているわけでもなく、趣味で気の向くままにやっているので、
大事な知識や考え方などが欠けていると思います・・・

  • 気になる質問をクリップする

    クリップした質問は、後からいつでもマイページで確認できます。

    またクリップした質問に回答があった際、通知やメールを受け取ることができます。

    クリップを取り消します

  • 良い質問の評価を上げる

    以下のような質問は評価を上げましょう

    • 質問内容が明確
    • 自分も答えを知りたい
    • 質問者以外のユーザにも役立つ

    評価が高い質問は、TOPページの「注目」タブのフィードに表示されやすくなります。

    質問の評価を上げたことを取り消します

  • 評価を下げられる数の上限に達しました

    評価を下げることができません

    • 1日5回まで評価を下げられます
    • 1日に1ユーザに対して2回まで評価を下げられます

    質問の評価を下げる

    teratailでは下記のような質問を「具体的に困っていることがない質問」、「サイトポリシーに違反する質問」と定義し、推奨していません。

    • プログラミングに関係のない質問
    • やってほしいことだけを記載した丸投げの質問
    • 問題・課題が含まれていない質問
    • 意図的に内容が抹消された質問
    • 過去に投稿した質問と同じ内容の質問
    • 広告と受け取られるような投稿

    評価が下がると、TOPページの「アクティブ」「注目」タブのフィードに表示されにくくなります。

    質問の評価を下げたことを取り消します

    この機能は開放されていません

    評価を下げる条件を満たしてません

    評価を下げる理由を選択してください

    詳細な説明はこちら

    上記に当てはまらず、質問内容が明確になっていない質問には「情報の追加・修正依頼」機能からコメントをしてください。

    質問の評価を下げる機能の利用条件

    この機能を利用するためには、以下の事項を行う必要があります。

回答 2

checkベストアンサー

+2

本件の場合、外側のスコープの変数を参照せずとも「イベントの発生に関わる情報」がeventに渡されるのでそこから情報を引き出せばよいです。そのような目的があるからこそイベントハンドラーにはeventという引数が渡されるわけです。

...

def click_word(event) :
    print(event.widget['text'])

...

しかし・・・変数のスコープの問題を「きちんと理解する」ことで上記の方法ではない方法でも実現は可能です。質問者さんの疑問はその点にあると思うのでそれについてもコメントしてみます。

どうしてこうなってしまったのでしょうか。 
(A)関数の定義「def ~」をfor in の中に書いてみても、同じようになりました。
(B)関数を定義するときに引数に「word」を渡してみても、同じようになりました。 

このコードの動きを理解するには変数wordのスコープの範囲を知っておく必要があります。
本件の場合wordは__main__モジュールのグローバル変数になります。要はfor文の繰り返し部分にとどまらずfor文の前後で参照した場合も常に同じグローバル変数のアクセスという意味になります。

for文による繰り返しが全て完了した後、wordの値は最後の要素の値'こ'になっており、それ以降wordに他の値を代入しない限りその値は常に'こ'にしかなりません。

(A),(B)のいずれにせよ変数wordのスコープ範囲は変わらないので結果は同様になります。

(A)

...

for word in word_list:

    def click_word(event):
        print(word)

    ...
    label.bind("<Button-1>", lambda event: click_word(event))

...


上記のようにfor文の内側に関数を書いたとしても関数本体で参照しているwordはグローバル変数であることに変わりはないので結果は同じです。

(B)

def click_word(event, w):
    print(w)            # 引数w

for word in word_list:  # グローバル変数word
    ...
    label.bind("<Button-1>", lambda event: click_word(event, word))  # グローバル変数word

...


上記のようにwordを関数の引数に渡そうとしても、lambda式(それも結局は関数と同じ)の内側で参照している変数wordがグローバル変数の参照であるという意味は変わらないので結局(A)と同じ結果にしかなりません。

ループの各繰り返しを実行している時点での変数wordの値を関数へ渡すには「wordのその時点の値を別のローカル変数に束縛しておく」必要があります。Python言語ではある値に束縛されたローカル変数を形成するには「関数の呼び出し」を行うのが基本になります(他の言語ですと変数のスコープを形成するような構文があったりしますがPythonにはあまりないです※)。もちろん「クリックしたときに起動される関数」を呼び出してはならないので、「クリックしたときに起動される関数を返すような別の関数を呼び出す」という考え方にします。

def click_function(w):

    def click_word(event):
        print(w)            # 外側の関数の引数w

    return click_word

for word in word_list:  # グローバル変数word
    ...
    label.bind("<Button-1>", click_function(word))

...

Pythonの関数(Python以外の言語の関数も同様だが)は通常「その関数の内側で宣言されたローカル変数以外に関数の外側のスコープにある変数(グローバル変数や外側の関数のローカル変数)」も参照する能力を持っています。

外にあるスコープを参照する能力のある関数のことを特にクロージャーと呼ぶことがあります。クロージャーは元としたローカル変数wの定義元であるclick_functionの実行が終わった後でもwの束縛情報を保存し続けることができるのですね。つまりclick_functionの実行時にしか存在しないはずのローカル変数がclick_word関数の中に「密かに記録されている」わけです。

なお上記はlambda式を用いてもっと短く書くこともできます。defによって関数を入れ子で定義したのと同様にlambdaの入れ子になるので慣れないとちょっと戸惑うかも知れません。

def click_word(event, w):
    print(w)

for word in word_list:
    ...
    label.bind("<Button-1>", (lambda x: lambda ev: click_word(ev, x))(word))

...

脱線:
※他の言語でのローカル変数を形成する構文

例えばJavaScript(ECMAScript 2015)だと次のように{}letを組み合わせた書き方ができます。

for (let word of word_list) {
  ...
  label.addEventListener('click', ev -> onClick(ev, word));
}


この場合wordはPythonのようにグローバル変数ではなくfor文の繰り返し本体({}の中)でのみ有効な局所的な変数になり、質問者さんがやろうとした(A)や(B)のアプローチで期待どうりに動いてくれます。残念ながらPythonはこのように手軽に変数のスコープを形成する手段がありませんので「一旦別の関数を呼び出す」ということが必要になります。

投稿

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2019/04/07 11:40 編集

    関数に渡されるのがグローバル変数であるがために、for in でグローバル変数が変わってしまうと、関数で使われる値も新しいグローバル変数になってしまい、
    しかもイベントが発生しない限り関数は実行されないので、for in が実行され終わってしまい、最後にグローバル変数を定義した「こ」になっていた。
    それを回避するために、クロージャーという方法を用いると、外側の関数で受け取ったグローバル変数は内側の関数ではローカル変数となるので、for in でグローバル変数を変えてしまっても、ローカル変数は変わらず、変数を束縛・保存しておくことができるということでしょうか?

    キャンセル

  • 2019/04/07 12:14

    概念としての変数が何かといえば「変数名とその値のペア情報」と考えられます。
    グローバル変数であれ外側のローカル変数であれ「変数名とその値のペア」であることに変わりはありません。どちらの場合でも参照しようとしている変数の値が後から変化すれば参照結果もかわります。

    click_functionのローカル変数wは「その呼び出しに関しては後から値を変更していない」ためclick_word関数から参照したとき常に同じ値が得られるということです。click_functionから返される関数click_wordはclick_functionを呼び出すたびに新たに生成される別々の関数になる点に注意してください。その別々の関数で「それぞれ異なるwの値を表すクロージャー」になっているわけです。

    キャンセル

  • 2019/04/07 12:47 編集

    なるほど!
    この問題を解決するいくつかの方法を紹介してくださった上、ちょうど頭の中でごちゃごちゃとしてよく理解していなかったスコープや、クロージャーをわかりやすく、こんなに丁寧で詳しく説明してくださって本当に感激です!!

    概念やロジックを理解した上で、問題を解決し、それを方法だけでなく概念やロジックなどとともにわかりやすく教えられるなんてかっこいいなと思いました。
    KSwordOfHasteさんみたいにできるようになりたいです。
    私はまだ子供なので、プログラミングは趣味の範囲でしかやっておらず、とりあえず作れればいいかという感じで、根本的な理解はガン無視していましたが、今後は概念や色々な方法の勉強もしていこうと思いました。

    実際に作っているプログラムもここで教えていただいたことを使ってうまくできました!

    ありがとうございました!!

    キャンセル

+1

クリックされた文字というのを(どこからか)取得して表示する、ということをしなければなりませんが、提示のコードではそうなってませんね。
このコードではどういう操作をしている(つもり)のか、説明ができるでしょうか

投稿

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

15分調べてもわからないことは、teratailで質問しよう!

  • ただいまの回答率 88.81%
  • 質問をまとめることで、思考を整理して素早く解決
  • テンプレート機能で、簡単に質問をまとめられる

関連した質問

同じタグがついた質問を見る