hokkun blog

技術の話をメインに、たまに糖質制限の話とか色々したい予定。

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__ 属性がもともとの関数からコピーされる形になる。

久しぶりに技術記事っぽいの書いたけど、やっぱブログ続けられる人ってすごい。。結構骨だな。