tkinter と functools.partial()
就職活動のときに買った パーフェクトPython | Pythonサポーターズ | 工学 | Kindleストア | Amazon を最近改めて通読し始めた。一応大学生までは Python をメインで書いていたので、9章の言語仕様の部分まではサラッと流した。ただ、正直ジェネレータとかデコレータとか、無理やり他の方法でやればできてしまうあたりについては実践的な経験がないので、今後苦労しそうだ。折に触れて読み返そうと思う。そして、11章の実践開発:チャットアプリケーションの章で少しハマったので残しておく。
tkinter を利用した GUI アプリケーションの開発の部分で、tkinter のメインループを回すメソッドである、tkinter.mainloop()
に対してフックをしてasyncore
のループを回す部分で、tkinter のバージョンが変わってエラーが発生する。サンプルコード(サポートページ:パーフェクトPython:|技術評論社より)を一部引用する。
def idle_task(root): try: asyncore.loop(count=1, timeout=1) finally: root.after(200, functools.partial(idle_task, root)) def main(): root = tkinter.Tk() root.after(200, functools.partial(idle_task, root)) ... root.mainloop()
こちらの functools.partial() は __name__
属性をデフォルトで持たないが(これはもともと)、いつの変更からか(おそらく Python 3.4.x あたりだということらしい。こちら参照)root.after()
がそれを参照してしまう。
# tkinter.__init__.py 一部省略 def after(self, ms, func=None, *args): if not func: self.tk.call('after', ms) else: def callit(): try: func(*args) finally: try: self.deletecommand(name) except TclError: pass callit.__name__ = func.__name__ # ここで死ぬ name = self._register(callit) return self.tk.call('after', ms, name)
なので、サンプルコードそのままでは動かなくて、何らかの方法で __name__
属性を与え(?)なくてはならない。
そもそも、ここではコールバックを渡すことを目標にしているわけなので、上記で引用した回答にあるように、ラムダ式を使う手もある。ただ、partial で作った高階関数も、そのまま元の関数の属性をコピーするのが自然と考えるのなら、functools.update_wrapper
を使うのが自然かなと思う。元のサンプルコードを変更すれば、
def idle_task(root): try: asyncore.loop(count=1, timeout=1) finally: wrapped_func = functools.partial(idle_task, root) functools.update_wrapper(wrapped_func, idle_task) root.after(200, wrapped_func) # main() 側も当然変更する
となる。こうすると、__name__
属性や __doc__
属性がもともとの関数からコピーされる形になる。
久しぶりに技術記事っぽいの書いたけど、やっぱブログ続けられる人ってすごい。。結構骨だな。