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

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

ただいまの
回答率

89.49%

Pythonで処理を分けるスマートな書き方を教えてください。(Tkinter)

解決済

回答 2

投稿 編集

  • 評価
  • クリップ 0
  • VIEW 471

decoyxyz

score 11

Pythonのインスタンス変数の書き方について、悩んでしまいました。
落ち着いて考えると打開策があるような気も致しますが、煮詰まってしまったので、ご教示お願い致します。

例えば、以下のコードでは、Aという処理とBという処理をメソッドに書き出しています。

class Test:
    def __init__(self):
        self.v = 10
        self.setA()
        self.setB()
        while True:
            self.addA()
            self.addB()

    def setA(self):
        self.A = 10

    def addA(self):
        self.A+= self.v

    def setB(self):
        self.B = 10

    def addB(self):
        self.B += self.v


しかしながら、この書き方は、インスタンス変数AとBがinitの外に書き出されているため、あまりスマートではない気がします。
(例えinitの中に書いても、変数が増えていくたびにinitに追記していくのは苦痛です。)
また、メソッド内のすべての変数名に、どのメソッドに属しているような記号を付ける必要があります。

より具体的に本件で悩んでしまったのは、TkinterでGUIを作る際です。
2つのframeをrootとして、処理をメソッドに分けて書いています。
ボタンクリックによるイベント処理を書きやすくするために、極力self以外の引数は書かないようにしています。

import tkinter
import datetime

class Test:
    def __init__(self):
        self.root = tkinter.Tk()
        frameA = tkinter.Frame(self.root)
        frameA.pack()
        frameB = tkinter.Frame(self.root)
        frameB.pack()
        self.drawA(frameA)
        self.drawB(frameB)
        self.update()
        self.root.mainloop()

    def update(self):
        self.updateA()
        self.updateB()
        self.root.after(1000, self.update)

    def drawA(self, root):
        self.A_message = tkinter.Label(root, text="TestAAAA")
        self.A_message.pack()

    def updateA(self):
        self.A_message["text"] = str(datetime.datetime.now())

    def drawB(self, root):
        self.B_message = tkinter.Label(root, text="TestBBBB")
        self.B_message.pack()

    def updateB(self):
        self.B_message["text"] = str(datetime.datetime.now())


Test()


この書き方では以下のデメリットがあると思います。

  • 「self.B_label_XXX...」などと変数名の最初にメソッド固有の名前を付ける必要があり煩わしい。
  • メソッド固有の名前にしているため、コードを使いまわしするには必ず変数名の変更が必要になる。
    (ウィジェットの数が多くなると深刻です。)
  • 名前空間を消費する。

そこで、辞書を宣言して、すべてのwidgetをその中に放り込んでしまうということを思いつきました。

import tkinter
import datetime
import sys

class Test:
    def __init__(self):
        self.root = tkinter.Tk()
        frameA = tkinter.Frame(self.root)
        frameA.pack()
        frameB = tkinter.Frame(self.root)
        frameB.pack()
        self.widget = {}
        self.drawA(frameA)
        self.drawB(frameB)
        self.update()
        self.root.mainloop()

    def update(self):
        self.updateA()
        self.updateB()
        self.root.after(1000, self.update)

    def drawA(self, root):
        key = sys._getframe().f_code.co_name[-1]
        self.widget[("label", key)] = tkinter.Label(root, text="TestAAAA")
        self.widget[("label", key)].pack()

    def updateA(self):
        key = sys._getframe().f_code.co_name[-1]
        self.widget[("label", key)]["text"] = str(datetime.datetime.now())

    def drawB(self, root):
        key = sys._getframe().f_code.co_name[-1]
        self.widget[("label", key)] = tkinter.Label(root, text="TestBBBB")
        self.widget[("label", key)].pack()

    def updateB(self):
        key = sys._getframe().f_code.co_name[-1]
        self.widget[("label", key)]["text"] = str(datetime.datetime.now())


Test()


すべてのインスタンス変数の宣言がinitの中にあり、しかもコードの使いまわしが容易になりました。
しかし、変数名に変わるキーは「"label"」のように文字列になってしまっているため、IDEによるリファクタリングが効かなくなってしまいます。

クラス内にクラスを置いてみる・・・

import tkinter
import datetime

class Test:
    def __init__(self):
        self.root = tkinter.Tk()
        frameA = tkinter.Frame(self.root)
        frameA.pack()
        frameB = tkinter.Frame(self.root)
        frameB.pack()
        self.drawA = self.DrawA(self, frameA)
        self.drawB = self.DrawA(self, frameB)
        self.update()
        self.root.mainloop()

    def update(self):
        self.drawA.update()
        self.drawB.update()

        self.root.after(1000, self.update)


    class DrawA:
        def __init__(self, self_, root):
            self.root = root
            self.message = tkinter.Label(root, text="TestAAAA")
            self.message.pack()

        def update(self):
            self.message["text"] = str(datetime.datetime.now())


    class DrawB:
        def __init__(self, self_, root):
            self.root = root
            self.message = tkinter.Label(root, text="TestBBBB")
            self.message.pack()

        def update(self):
            self.message["text"] = str(datetime.datetime.now())


Test()


・・・前の2つよりは改善された気がしますが、ネストが深くなってしまった気がします。

どうか、このようなケースでスマートに処理を分ける方法がありましたら、ご教示ください。

2019/05/20追記

ご回答ありがとうございました。
コードを簡略化しすぎていたため、分かりにくい質問となってしまったことをお詫びします。

ご回答を参考に、一旦、クラス内クラスを使うこととしました。
以下のコードは、「左のメニューのボタンをクリックすると、RobotAとRobotBのコントロールパネルを表示する」というサンプルコードです。

import tkinter
import datetime

class Test:
    def __init__(self):
        # ウインドウの作成
        self.root = tkinter.Tk()

        # 大まかなフレームを作成
        self.root_menu = tkinter.Frame(self.root)
        self.root_menu.grid(column=0, row=0, sticky=tkinter.NSEW)
        self.root_contents = tkinter.Frame(self.root)
        self.root_contents.grid(column=1, row=0, sticky=tkinter.NSEW)

        # root_contentsに表示されるframe
        self.select_contents = None

        # root_contentsがリサイズされるようにする
        self.root.columnconfigure(1, weight=1)
        self.root.rowconfigure(1, weight=0)

        # メニューを作成
        self.root_menu_buttons = {}
        for x in dir(Test):
            if x[0:4] == "Draw":
                item = tkinter.Button(self.root_menu, text=x[4:], command=self.selector(x[4:]))
                item.pack()
                self.root_menu_buttons[x[4:]] = item

        # 子項目に渡すframeを作成
        self.contents_frames = {}
        for x in self.root_menu_buttons:
            self.contents_frames[x] = tkinter.Frame(self.root_contents)

        # 子項目をインスタンス化
        self.contents_instance = {}
        for x in self.root_menu_buttons:
            exec("self.contents_instance[x] = self.Draw" + x + "(self, self.contents_frames[x])")


        self.update()
        self.root.mainloop()

    def selector(self, select):
        def func():
            # 表示されているものを隠す
            if self.select_contents is not None:
                self.select_contents.pack_forget()
                print("隠す")
            # 選択されたウィジェットを表示する。
            self.contents_frames[select].pack(fill=tkinter.BOTH)
            self.select_contents = self.contents_frames[select]
        return func

    def update(self):
        for x in self.root_menu_buttons:
            self.contents_instance[x].update()

        self.root.after(1000, self.update)

    # RobotAとRobotBは全く別の製品で処理の共通化は必要ない。
    class DrawRobotA:
        def __init__(self, self_, root):
            self.root = root
            tkinter.Label(root, text="RobotAのコントロールパネル").pack()
            self.message = tkinter.Label(root, text="RobotA")
            self.message.pack()
            # 以下、RobotB固有の複数のwidgetが並ぶ

        def update(self):
            self.message["text"] = "RobotA" + str(datetime.datetime.now())

    class DrawRobotB:
        def __init__(self, self_, root):
            self.root = root
            tkinter.Label(root, text="RobotBのコントロールパネル").pack()
            self.message = tkinter.Label(root, text="RobotB")
            self.message.pack()
            # 以下、RobotB固有の複数のwidgetが並ぶ

        def update(self):
            self.message["text"] = "RobotB" + str(datetime.datetime.now())

Test()

一応解決となりましたので、様子をみてベストアンサーを決定してクローズとさせていただきます。

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

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

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

    クリップを取り消します

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

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

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

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

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

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

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

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

    質問の評価を下げる

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

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

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

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

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

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

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

    詳細な説明はこちら

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

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

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

質問への追記・修正、ベストアンサー選択の依頼

  • y_waiwai

    2019/05/20 08:22

    あなたの言うスマート、というのが理解できません。
    具体的にどうしたいんでしょうか。
    関数を増やさなければ損でいい?
    名前を消費しなければそんでいい?
    ネストを浅くすればそんでいい?
    何を目指してるんでしょう

    キャンセル

  • decoyxyz

    2019/05/20 16:09

    抽象的な質問で申し訳ございません。
    2019/05/20の夜に質問を追記致します。

    キャンセル

回答 2

checkベストアンサー

0

ご質問を拝見する限り質問者さんが例として挙げておられるコードは単純化しており、質問意図は(例には書かれていない)他の色々なアプリケーションの機能において如何に個々の機能間の依存関係(例えばネームスペース内での属性名の衝突など)を抑えるかについてのPythonにおける方法論だと思います。

例ではA_message, B_messgeは共通化できるものになってしまってますが、これはたまたまそういう例としてしまっただけで本来は別々の実装のものを想定しておられるのではないでしょうか。

さてPythonにおいて機能を分割するのに使える実装の置き所には

・パッケージ/モジュール
・クラス
・関数

があります。どこに置くかは再利用性や機能の規模によりケースバイケースなわけですが、Pythonの場合、質問者さんがトライしておられるようにクラスと関数の定義場所はかなり柔軟でありほとんどどこにでも定義することができます。

自分にはスマートの基準がはっきりあるわけではないですが

  • クラスや関数の入れ子はせいぜい0か1にした
    それ以上になるとスマートかどうか以前に把握しづらくなる。

  • 似たようなパターンを複数個所に書くのは基本的にいやだ
    前述したとおり本件においては質問者さんの興味はここにはないと思います。

  • 互いに関連のない雑多なインスタンス変数が一つのクラスにごちゃごちゃあるのはいやだ
    言い換えれば機能の分割のしかたです。ここが質問者さんの興味の焦点だと思います。

パッケージやモジュールを分割するとなると相当まとまりのある大きな機能をイメージします。アプリケーションレベルですと、例えば「キャンバスの上に様々な図形を管理し、それらをアニメーションできるような機能」なんてものなら他のモジュールに独立したクラスとして置きたくなります。

本件の「一定時間ごとに自動的に現在時刻を表示するラベル」を例にとるとパッケージやモジュールに置くようなものではなさそうな印象で、クラスとしてなら悪くはない気がします。ただしアプリケーションが持つ単一の機能(複数個所に出現しないという意味)としてこれをクラスに定義するのはどうでしょう?ちょっとやりすぎな香りがします。しかしながらそういう機能がアプリケーションの複数に出現するならクラスとすることは自然に感じます。

もしクラスにするとしてその置きどころですが・・・

class App:
   def m1(self, ...):
       class LocalClass1:
           def m2(_self, ...):
               ... _selfをアクセス ...
               ... selfをアクセス ...

   class LocalClass2:
       def m2(self, ...):
           ... selfをアクセス ...

class FunctionInApp:
    def m2(self, ...):
        ... selfをアクセス ...

LocalClass2がFnctionInAppと違う点はLocalClass2がモジュールのグローバルな名前空間上ではApp.LocalClass2となり他のグローバルな名前と違う空間にあることだと思います。しかし逆に言えば違いはそこにしかなく、LocalClass2がAppの文脈で何かFuncitionInAppより有利なアクセスができるかというとそういうことはありません。一方LocalClass1はAppクラスのm1メソッドの実行の文脈(ローカル変数)をアクセスできます。しかしながらこのクラスは一時的にしか存在しないわけなので他の機能との共用はもはやできません。

結論を言いますともしアプリケーションの機能を分割するなら「アプリケーションの機能の定義の置き所である__main__モジュール」のトップレベルにFunctionInAppを置くのが自然(少なくとも一つの方法)に感じます。必ずいつもそうという意味ではなく多くの場合そこでいいんじゃないかというほどの意味です。本当に局所的にしか使わない機能ならLocalClass1あるいは同じ位置に置いた関数あるいはAppのメソッドでよいと思います。


コメントしてはみたものの「スマートかどうか」という観点にマッチした回答になってないような気がします。いろいろ思うところがあるのですが回答が発散しそうに思えたので止めました。中途半端感が激しい回答に終わってしまったような気がします...

投稿

  • 回答の評価を上げる

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

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

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

  • 回答の評価を下げる

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

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

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

  • 2019/05/20 16:08

    ご回答ありがとうございます!
    私が求めていた回答です。
    力不足により分かりにくい説明となってしまいましたが、汲み取って頂き感激です。

    クラスにより、機能の切り分けを行いたいと思います。
    ありがとうございました。

    キャンセル

0

必要以上に処理を分割するのがそもそもスマートではないというケースのような。

↓じゃ駄目ですか?

import tkinter
import datetime

class Test:
    def __init__(self):
        self.root = tkinter.Tk()

        frameA = tkinter.Frame(self.root)
        frameA.pack()
        frameB = tkinter.Frame(self.root)
        frameB.pack()

        self.A_message = tkinter.Label(frameA, text="TestAAAA")
        self.A_message.pack()

        self.B_message = tkinter.Label(frameB, text="TestBBBB")
        self.B_message.pack()

        self.update()

        self.root.mainloop()

    def update(self):
        self.A_message["text"] = str(datetime.datetime.now())
        self.B_message["text"] = str(datetime.datetime.now())
        self.root.after(1000, self.update)

Test()

投稿

  • 回答の評価を上げる

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

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

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

  • 回答の評価を下げる

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

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

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

  • 2019/05/20 15:55

    ご回答ありがとうございます。
    説明不足で申し訳ございません。

    実際のGUIでは、
    root > 20個のframe※ > それぞれのframeに50程度のウィジェット
    となっており、すべてをinitに記入すると膨大な長さになります。

    ※メニュー、トップページ、それぞれの機能のページなど

    後日、例のコードをもっと分かりやすく更新致します。

    キャンセル

  • 2019/05/20 16:08

    そこまで規模が大きいとそもそも「tkinterで書くべきか」というあたりから見直すべきかもしれません。
    pyqtとかならツールで画面を作って読み込むということもできます。

    キャンセル

  • 2019/05/20 22:39

    tkinterで書くべきではないかもしれないというのは重々承知しておりますが、過去のコードとの兼ね合いもありPyqtに移行するのは難しい状態です・・・

    キャンセル

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

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