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

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

新規登録して質問してみよう
ただいま回答率
85.46%
Tkinter

Tkinterは、GUIツールキットである“Tk”をPythonから利用できるようにした標準ライブラリである。

Python

Pythonは、コードの読みやすさが特徴的なプログラミング言語の1つです。 強い型付け、動的型付けに対応しており、後方互換性がないバージョン2系とバージョン3系が使用されています。 商用製品の開発にも無料で使用でき、OSだけでなく仮想環境にも対応。Unicodeによる文字列操作をサポートしているため、日本語処理も標準で可能です。

Q&A

解決済

3回答

2633閲覧

tkinterでの再起呼び出し

ADATER

総合スコア2

Tkinter

Tkinterは、GUIツールキットである“Tk”をPythonから利用できるようにした標準ライブラリである。

Python

Pythonは、コードの読みやすさが特徴的なプログラミング言語の1つです。 強い型付け、動的型付けに対応しており、後方互換性がないバージョン2系とバージョン3系が使用されています。 商用製品の開発にも無料で使用でき、OSだけでなく仮想環境にも対応。Unicodeによる文字列操作をサポートしているため、日本語処理も標準で可能です。

0グッド

0クリップ

投稿2021/05/21 05:25

前提・実現したいこと

tkinterでアニメーションを実装するときに、afterメソッドで再帰呼び出しを使う方法がどの本にも挙がっていますが、再帰呼び出しを繰り返すと、スタックがたまって重くならないのでしょうか。
また、whileで実装したとき、updateメソッドを使ったのですが、インターネットにはそれはあまりよくない方法だという情報がたくさんのサイトに載っていました。さらに、updateを使って実装したところ、ウィンドウ右上の×マークを押してプログラムを終了したときに、下記のようなエラーが出ます。それは、何故なのでしょうか。
python初心者なので、かなり初歩的なところで詰まっているのかもしれません。すみません。
なお、下記のソースコードはupdateを使って円を動かすプログラムです。
拙いものですがどうか参考にしてください。
回答、よろしくお願いいたします。

発生している問題・エラーメッセージ

エラーメッセージ Traceback (most recent call last): File "D:\files\user\user\test-update.py", line 31, in <module> oval_move() File "D:\files\user\user\test-update.py", line 21, in oval_move cv.delete("oval1") File "C:\Users\Disco\anaconda3\lib\tkinter\__init__.py", line 2818, in delete self.tk.call((self._w, 'delete') + args) TclError: invalid command name ".!canvas"

該当のソースコード

python3.8.5

import tkinter as tk import time dx= 10 dy = 10 root = tk.Tk() root.geometry("200x200") cv = tk.Canvas(root,width = 200,height = 200, bg = "white") cv.place(x=0,y=0) def oval_move(): global dx,dy cv.delete("oval1") time.sleep(0.1) dx = dx+ 5 cv.create_oval(dx-10,dy-10,dx+10,dy+10,fill="black",tag="oval1") while True: oval_move() root.update()

試したこと

root.protocolでウィンドウの破棄を検出、whileをbreakしようとした。

補足情報(FW/ツールのバージョンなど)

Python3.8.5 anaconda2020.11

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

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

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

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

guest

回答3

0

オフトピかもしれませんが、再帰とupdate() について

再帰呼び出しでのスタックの推移

## 再帰呼び出しでの例 import inspect def fact(n): depth = len(inspect.stack()) print(f"fact({n}) stack-depth: {depth}") if n > 0: return n + fact(n - 1) else: return 0 print(fact(5))

txt

1fact(5) stack-depth: 2 2fact(4) stack-depth: 3 3fact(3) stack-depth: 4 4fact(2) stack-depth: 5 5fact(1) stack-depth: 6 6fact(0) stack-depth: 7 715 8

after() で一定時間毎に呼び出す

python

1import inspect 2import tkinter as tk 3 4root = tk.Tk() 5root.withdraw() # window は非表示 6 7def tick(): 8 depth = len(inspect.stack()) 9 print(f"tick() stack-depth: {depth}") 10 root.after(1000, tick) 11 12root.after_idle(tick) 13root.after(3*1000, root.quit) # 3秒後に終了 14root.mainloop()

再帰呼び出しではないので、スタックの深さは一定です。

txt

1tick() stack-depth: 5 2tick() stack-depth: 5 3tick() stack-depth: 5

イベントループ内での update()

python

1""" 2update() が再帰呼び出しになってしまう例。 3 4このコードは、問題を検証するだけのコードですが、 5実際のケースでは、イベントループ内で時間のかかる処理中に、 6update する場合に、発生する可能性があります。 7""" 8 9import inspect 10import tkinter as tk 11 12root = tk.Tk() 13root.withdraw() 14 15def tick(num): 16 depth = len(inspect.stack()) 17 print(f"tick({num}) start, stack-depth: {depth}") 18 19 if num > 0: 20 root.after_idle(tick, num-1) 21 22 ## コメントを外して update() の有無での挙動を確認 23 # root.update() 24 25 print(f"tick({num}) end") 26 27 28root.after_idle(tick, 3) 29root.after(3*1000, root.quit) 30root.mainloop()

updateがない場合

txt

1tick(3) start, stack-depth: 5 2tick(3) end 3tick(2) start, stack-depth: 5 4tick(2) end 5tick(1) start, stack-depth: 5 6tick(1) end 7tick(0) start, stack-depth: 5 8tick(0) end

updateが有る場合
イベントループの再帰呼び出しが発生しスタックが貯まります

txt

1tick(3) start, stack-depth: 13 2tick(2) start, stack-depth: 17 3tick(1) start, stack-depth: 21 4tick(0) start, stack-depth: 25 5tick(0) end 6tick(1) end 7tick(2) end 8tick(3) end

実際のコードでの良くない update()

こちらは逆に、コードの記述は再帰っぽくなってないが、
実際には再帰呼び出しが発生する(可能性がある)コードです。

python

1 2import inspect 3import time 4import tkinter as tk 5from tkinter import ttk 6 7MAXNUM = 10 8 9root = tk.Tk() 10var = tk.IntVar() 11progbar = ttk.Progressbar(root, 12 mode="determinate", 13 orient=tk.HORIZONTAL, 14 variable=var, 15 maximum=MAXNUM) 16progbar.pack() 17 18 19def process(count): 20 # XXX: この関数の実装は「あまり良くない」例での 21 # 具体的な問題点を示すものです。 22 depth = len(inspect.stack()) 23 print(f"process({count}) start, stack: {depth}") 24 25 for num in range(1, 1+MAXNUM): 26 var.set(num) 27 root.update() # XXX: ボタンが連打された時、イベントループが再帰する可能性 ⇛ イベントの割り込みが発生 28 time.sleep(0.5) # XXX: スリープ中は操作を受け付けない 29 30 print(f"process({count}) end") 31 32count = 0 33def start(): 34 # ボタンクリックの回数をカウント 35 global count 36 count += 1 37 process(count) 38 39button = ttk.Button(root, text="start", command=start) 40button.pack() 41 42root.mainloop() 43

ボタンを複数回クリックすると問題の挙動が確認できます。

update() 内でイベントが処置される(イベントループのネスト)為、
最初のイベントが完了しないまま他のイベントの処理を開始し、
他のイベントが全て終わった後で最初のイベントの残りを再開します。

投稿2021/05/21 10:28

編集2021/05/21 10:38
teamikl

総合スコア8664

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

0

該当するソースが出ていないのでエスパーして答えます。

「afterメソッドで再帰呼び出しを使う」
関数の中でafterメソッドを使ってその関数を呼んでいるので再帰に見えるかもしれませんが、afterメソッドは未来での実行を登録しているだけなので、再帰にはなりません。

「whileで実装したとき、updateメソッドを使ったのですが、インターネットにはそれはあまりよくない方法だという情報がたくさんのサイトに載っていました」
tkinterはGUIのアプリを作るもので、GUIのアプリは基本的にイベントドリブンという仕組み/考えかたで動作しています。
このイベントとはマウスのクリックとかテキストの書き込みはタイマーなどいろいろあります。 そしてこれらを扱うのがmainloopです。
tkinterで作ったアプリはmainloopでイベントを処理することで動作することを前提に作られていますから、その仕組みに沿わない使いかたをするとうまく動きません。

投稿2021/05/21 05:52

TakaiY

総合スコア12804

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

ADATER

2021/05/21 09:45

回答、本当にありがとうございました。勉強になりました。
guest

0

ベストアンサー

after での定期的な関数の呼び出しは、
コードの記述は再帰的に見えるが、呼び出し時のスタック利用は再帰呼び出しではありません。

  • after() はすぐに処理を戻し、関数の自体は一旦終了しているので、

 スタックは消化されている。

  • after() に登録した関数は、イベントループから呼ばれるので、

 「呼び出し元がその関数自身」にはなってない。

標準ライブラリの inspect モジュールを使うと、内部スタックの調査ができます。

python

1# 再帰する関数内で呼び出して、スタックが増えるかどうかを確認してみてください。 2import inspect 3depth = len(inspect.stack()) 4print(f"call stack depth: {depth}")

root.mainloop() 内で呼び出されるイベント内でのroot.update()は
イベントループの再帰呼び出しになる可能性がありますが、
質問のコードは mainloop がなく、自前のループなので
この場合のupdateメソッドは「あまり良くない方法」には該当しません。

質問のコードの場合の問題点は、
「ウィンドウを閉じた時にループを抜けるようになっていない事」が問題です。

python

1while True: 2 # ウィンドウを閉じた後、ループの次の繰り返しの時、 3 # 破棄されたキャンバスに対してアクセスしようとしてエラーになる 4 oval_move() 5 6 # ウィンドウを閉じた時、update() 内で root は破棄される。 7 root.update()

追記:

root.protocolでウィンドウの破棄を検出、whileをbreakしようとした。

解決策としては、これでも解消できるはずです。どのように break しようとしましたか?

この場合は、root.protocol に登録した関数は、外部の変数を通じて break の条件を変更する必要があります。
ループを中断する変数を導入したが、ローカル変数のフラグを変更しているだけ、
のような間違いはよくあるので変数のスコープを調べてみてください、


通常は、特別な理由がない限りは mainloop を使った方が良いです。

  • 複数のイベントループを一つのスレッド内で統合して処理したいとき等に mainloop を使わず update を用います。
  • 「イベントループ 内での」 update() は、再帰っぽい記述ではありませんが

 再帰呼び出しになる可能性があります。

  • 描画の更新だけなら update_idletasks

投稿2021/05/21 08:43

編集2021/05/21 09:44
teamikl

総合スコア8664

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

ADATER

2021/05/21 09:44

回答、本当にありがとうございました。 勉強になりました!
ADATER

2021/05/21 09:51

追記: それが、root.protocolで変数を変更してbreak しようとしたのですが、whileに条件を加えてみると、ウィンドウすら表示されなくなりました。 printでちゃんとループしているか調べたのですが、そこはきちんとループしていました。もしかしたら、どこかでミスしていたのかもしれません。 もう一度、やってみます。 改めて、本当にありがとうございました。
teamikl

2021/05/21 10:37

>printでちゃんとループしているか調べたのですが、そこはきちんとループしていました。 解決策としては正しいので、多分コーディングのミスだと思いますが、 ここは逆で、break したのならループを終わってなければいけないはずです。 編集で、質問のコードを「試したこと」のコードへ、 変更してもらえれば、具体的に指摘できると思います。
guest

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

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

ただいまの回答率
85.46%

質問をまとめることで
思考を整理して素早く解決

テンプレート機能で
簡単に質問をまとめる

質問する

関連した質問