_branch_
@_branch_

Are you sure you want to delete the question?

Leaving a resolved question undeleted may help others!

Pythonスクリプトの構造化

Discussion

Closed

こんにちは

ご覧いただきありがとうございます.Pythonスクリプトを独学で(?)記述しております.開発しながら随時機能を追加していったので,なかなかなクソコードと化してしまいました.

三行で書くと
1.自動翻訳を使った外国語入力補助コンソール(Wxpython)
2.Python+Selenium+Chromium
3.クソコード
という感じです.詳しくはこちらの記事でアレコレ書いておりまして,以下に示しているコードは記事のものと同じ内容でございます.ここから以下の仕様を満たすように更新を加えようと思い,先に構造化のご指導をいただいた方がよいなということで質問いたしました.

実現したいこと・お伺いしたいこと

①Classごと独立させる?

class Translator:の部分は自動翻訳関数として独立させて,他のスクリプトからでも利用できるような汎用性を持つを思うのですが,その場合Translator.pyとでもして独立させたらよいですか?

②ボタン操作部分のクラス

コードの終盤にボタン関連のイベントを連続させているのですが,これはClass BtnEvent:とでもしてインスタンス化したほうがキレイでしょうか?というかそもそもこの場合そのように定義するメリットはありますか?

③global関数の定義

書きはじめはmain文をglobalスコープ(?)に記述していたのですが,くり返し実行するにあたって,毎回Chromiumが起動する羽目になるのでコンストラクタから初期起動時にのみTranslatorをインスタンス化するようにしました.main関数として関数化した時のエラーがすべて変数のスコープに関するものだったので,応急処置的にglobal変数とすることで逃げたのですが,どうもよろしい気がせず...この点についてもご教授いただけませんでしょうか.

3つもありますが,どれか一つだけでも大変助かります.
どうぞよろしくお願いいたします.

import wx
import wx.lib.scrolledpanel
import pyperclip
import time
import urllib.parse
import chromedriver_binary
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from bs4 import BeautifulSoup
from time import sleep


class Translator:       #seleniumによる翻訳を定義するクラス
    def __init__(self):
        self.options = Options()
        self.options.binary_location =  "C:\\Program Files\\chrome-win\\chrome.exe"
        self.options.add_argument("--headless")          # ヘッドレスモードで起動
        self.browser = webdriver.Chrome(options=self.options)
        #self.browser.minimize_window()                   # プロキシ有効の場合は最小化で対応
        self.browser.implicitly_wait(2)

    def trans(self, txt , lg1 , lg2):      # lg1からlg2に翻訳する関数

        # 翻訳したい文をURLに埋め込んでからアクセス
        text_for_url = urllib.parse.quote_plus(txt, safe='')
        url = "https://translate.google.co.jp/#{1}/{2}/{0}".format(text_for_url , lg1 , lg2)
        self.browser.get(url)

        # 少し待つ
        wait_time = len(txt) / 1000
        if wait_time < 0.5:
            wait_time = 0.5
        time.sleep(wait_time)

        # 翻訳結果を抽出
        soup = BeautifulSoup(self.browser.page_source, "html.parser")
        ret =  soup.find(class_="tlid-translation translation")

        return ret.text    

    def quit(self):
        self.browser.quit()


def main():
    # クリップボードの内容を取得
    orig = str(pyperclip.paste())

    # 改行を検出して分割.この際改行情報"\r\n"は失われる
    orignal_elements = orig.splitlines()
    print(orignal_elements)

    # 翻訳に関わる文字列を格納する変数
    origs = []
    en_txt = []
    jp_txt = []

    # 空白行を取得するためのグローバル変数
    global blank_row
    blank_row = []
    count = 0
    while count < len(orignal_elements):
        buf = orignal_elements[count]           #"   "のような空白のみの要素を抹殺
        buf = buf.replace(" " , "")             #単にスペースを消すだけでは外国語に対応できないので,差分をとる
        if buf != "":                           #空白でない場合は翻訳対象に追加
            origs.append(orignal_elements[count])
        else:
            blank_row.append(count)             #空白の場合はもともと空白行だったので,要素の場所をintで取得
        count += 1

    # 翻訳する2言語を設定[母国語,外国語]    将来的にプルダウンから言語を選択できるように構築する予定.
    global lg
    lg = ["ja" , "en"]

    # 逆翻訳にする二言語を設定 [外国語,日本語]
    global rev_lg
    rev_lg = list(reversed(lg))

    # GUIの準備
    app = wx.App()

    # 読み込み中の表示
    read_frame = wx.Frame(None, wx.ID_ANY, "翻訳中...", size=(250,0))
    read_frame.Centre() #中央に表示
    read_frame.Show()

    global rows
    rows = len(origs)           #lenは0を含むため,行数に注意

    for row in range(rows):     # 原文をもとに先に翻訳しておく
        #原文
        txt = origs[row]
        #print("原文:",txt)

        #英文
        en_txt.append(translator.trans(origs[row], *lg))
        #print("英文:",en_txt)

        #再翻訳
        jp_txt.append(translator.trans(en_txt[row], *rev_lg))

    size = (900,600)
    global frame
    frame = wx.Frame(None, wx.ID_ANY, '翻訳ちゃん', size=size)
    panel = wx.lib.scrolledpanel.ScrolledPanel(frame,-1, size=size, pos=(0,28), style=wx.SIMPLE_BORDER)
    panel.SetupScrolling()
    panel.SetBackgroundColour('#AFAFAF')

    global text
    global en_text
    global jp_text
    global btn
    layout = wx.FlexGridSizer(rows+1,4,0,0)

    text = [""]*rows            #原文テキストウィジェットの準備
    en_text = [""]*rows         #英文テキストウィジェットの準備
    jp_text = [""]*rows         #再訳文テキストウィジェットの準備
    btn = [""]*rows             #翻訳ボタンウィジェットの準備

    cellsize = (270,90)
    for row in range(rows):
        #原文
        txt = origs[row]
        text[row] = wx.TextCtrl(panel, row , txt, style = wx.TE_MULTILINE,size=cellsize)
        #print("原文:",txt)

        #英文
        en_text[row] = wx.TextCtrl(panel, row , en_txt[row], style = wx.TE_MULTILINE,size=cellsize)
        en_text[row].Disable()      #書き込み禁止
        #print("英文:",en_txt)

        #再翻訳
        jp_text[row] = wx.TextCtrl(panel, row , jp_txt[row], style = wx.TE_MULTILINE,size=cellsize)
        jp_text[row].Disable()      #書き込み禁止
        #print("再翻訳文:",jp_txt)

        #翻訳ボタン
        btn[row] = wx.Button(panel, row, "翻訳", size=(60, 40))
        btn[row].Bind(wx.EVT_BUTTON, OnClickBtn)            #ボタンをイベントにバインド


        #ウィジェットの配置
        #layout.Add(text[row], flag=wx.ALIGN_LEFT | wx.GROW)         #原文
        layout.Add(text[row], flag=wx.SHAPED)         #原文
        layout.Add(jp_text[row], flag=wx.SHAPED)      #再翻訳
        layout.Add(en_text[row], flag=wx.SHAPED)      #英文        
        layout.Add(btn[row],flag=wx.SHAPED | 
            wx.ALIGN_CENTER_VERTICAL | wx.TE_MULTILINE)             #翻訳ボタン

    copy_btn = wx.Button(panel, wx.ID_ANY, "翻訳完了", size=(80, 40))
    copy_btn.Bind(wx.EVT_BUTTON, OnClickCopyBtn)
    layout.Add(copy_btn,flag=wx.SHAPED | 
        wx.ALIGN_CENTER_VERTICAL | wx.TE_MULTILINE)

    retrans_btn = wx.Button(panel, wx.ID_ANY, "各セルをリセットして再翻訳", size=(200, 40))
    retrans_btn.Bind(wx.EVT_BUTTON, OnClickRetransBtn)
    layout.Add(retrans_btn,flag=wx.SHAPED | 
        wx.ALIGN_CENTER_VERTICAL | wx.TE_MULTILINE)

    exit_btn = wx.Button(panel, wx.ID_ANY, "翻訳ちゃんを終了", size=(120, 40))
    exit_btn.Bind(wx.EVT_BUTTON, OnClickExitBtn)
    layout.Add(exit_btn,flag=wx.SHAPED | 
        wx.ALIGN_CENTER_VERTICAL | wx.TE_MULTILINE)

    # ボタンの配置
    layout.AddGrowableCol(0, 3)
    layout.AddGrowableCol(1, 3)
    layout.AddGrowableCol(2, 3)
    layout.AddGrowableCol(3, 1)

    # レイアウトの更新
    panel.SetSizer(layout)

    # ステータスバーを定義
    frame.CreateStatusBar()
    frame.SetStatusText("オンラインサービス利用のため守秘義務を遵守の上ご利用ください")

    read_frame.Close()
    frame.Centre() #中央に表示
    frame.Show()
    app.MainLoop()


def OnClickBtn(event):
    num = event.GetId()

    btn[num].Disable()

    n_txt = text[num].GetValue()
    n_en_txt = translator.trans(n_txt, *lg)
    n_jp_txt = translator.trans(n_en_txt, *rev_lg)

    en_text[num].SetValue(n_en_txt)
    jp_text[num].SetValue(n_jp_txt)

    btn[num].Enable()

def copy_all():         #翻訳結果の英文をクリップボードにコピーする関数

    fin_txt = ""        # クリップボードにコピーする文字列 
    fin_txts = []       # 翻訳後の英文をセルごとに格納する文字列

    for row in range(rows):
        fin_txts.append(en_text[row].GetValue()+"\r\n")     #splitlinesメソッドで失われた改行情報を復元

    for blank in blank_row:
        fin_txts.insert(blank,"\r\n")                       #空白行の場所はblank_rowに格納されているので空白行を追加

    for element in fin_txts:
        fin_txt += element                                  #改行情報を復元したので出力用文字列として結合

    return fin_txt

def OnClickCopyBtn(event):          #【翻訳完了】ボタンが押された場合
    pyperclip.copy(copy_all())

def OnClickRetransBtn(event):       #【各セルをリセットして再翻訳】ボタンが押された場合
    frame.Close()
    main()

def OnClickExitBtn(event):          #【翻訳ちゃんを終了】ボタンが押された場合
    pyperclip.copy(copy_all())
    frame.Close()
    translator.quit()


if __name__ == "__main__":
    translator=Translator()
main()
0

人様に指導できるほど経験積んでるわけじゃないので、私だったらこうするかな、という程度で書き込ませてもらいます。

①別にファイルを分けてもいいのですが、このコード自体をtranslator.pyという名前(名前は全部小文字にする決まり)にして、Pythonの慣習に従って、現在のファイルをキッチリモジュール化した方がいいと思います。つまり、

translator.py
class Translator:
    ...

def main():
    ...

if __name__ == "__main__":
    main()

という書き方を守れば、別のPythonスクリプトで

from translator import Translator

としてTranslatorクラスを利用することができます。importした際にはmainが実行されず、ファイル内のクラスや関数だけをつまみ食いできるというのが、この書き方をするメリットなので。

なお、提示されたコード

if __name__ == "__main__":
    translator = Translator()
main()

だと、必ずmainが実行されてしまうのでNGです。main()はifの中に入れます。

https://note.nkmk.me/python-if-name-main/ (参考)

②特にいじらなくていいんじゃないですかね。ボタンになにかデータを持たせたいとか、長い処理をさせたいっていうならMyButtonみたいなクラスを作ってデータや関数を管理するかも(参考)

③は意外と難しいんですよね。慣れると関数とデータの影響範囲をうまく絞り込んでクラスなどによってカプセル化できるんですけど。

例えばですけど、変数translatorをグローバルとして用意しなければいけないのは、関数OnClickBtnやOnClickExitBtnで使っているからですよね。であれば、OnClickBtnやOnClickExitBtnをmain関数の中に移動させてしまえばいいです(関数の中に関数を書いても何の問題もありません)。

def main():
    translator = Translator()  # translatorの作成場所はここに移動
    ...

    text = [""]*rows            #原文テキストウィジェットの準備
    en_text = [""]*rows         #英文テキストウィジェットの準備
    jp_text = [""]*rows         #再訳文テキストウィジェットの準備
    btn = [""]*rows             #翻訳ボタンウィジェットの準備

    # ボタン関数はここに移動
    def OnClickBtn(event):
        ...

    def copy_all():
        ...

    def OnClickExitBtn(event):
        ...

  ...
    cellsize = (270,90)
    ...

if __name__ == '__main__':
    main()

こうすればmainの外でtranslatorを必要とすることはなくなるので、グローバルである必要がなくなります。このように関数や変数をどこに置くかを吟味すればほとんどグローバルを使う必要はないです。

そうすると今度はmainが長くなって見栄えが悪いかもしれませんが、関数化やクラスを使ってまとまった処理を括りだして整理していきます。この辺りは経験が必要で難しいところです。

2Like

Your answer might help someone💌