Python事始め

Python

Pythonを使い始めて一か月ほど経ちました。
そんな私が「Pythonの場合は、こんな感じで作っていけばいいかなぁ~」と落ち着いたので、私なりのルールをまとめてみました。なので、Pythonのマナーに反することがあるかもしれませんが、ご容赦ください。

そもそもPythonを始めたきっかけは「紙の本をタブレットで読めるようにしたかった」という安易なことで、Windowsで使えるPDF作成ツールをいろいろ探してみたものの、結局Pythonのライブラリ(pymupdf)がいろいろ出来て良さそうだったので、自分で作ることにしました。
Pythonはライブラリやドキュメントが豊富なので、ほかにもいろいろなこと(OCRとか画像処理とかWebスクレイピングとか)が結構簡単に出来ちゃいそうです。
文法は慣れれば大したことはありませんが、型宣言しなくても良いというのはまだちょっと気持ち悪いです。
GUIライブラリはPython標準のTkinter.ttkを使ってみましたが、機能も見た目も特に不満はありません。PythonでWindows, Mac, Linuxアプリを作るならTkinter.ttkで十分だと思います。

開発環境

  • Windows11
  • Visual Studio Code 1.71.2
  • Python 3.10.7

ディレクトリ構成

└─project_name
    ├─.vscode
    │      launch.json
    │
    ├─project_name
    │  │  __init__.py
    │  │  __main__.py
    │  │  hoge.py
    │  │  foge.py
    │  │
    │  └─icon
    │        check_blue_small.png
    │        check_red_small.png
    │        info_blue_small.png
    │
    └─tests
        │  __init__.py
        │  test1.py
        │  test2.py
        │
        └─icon

プロジェクト名のディレクトリ(project_name)を作成して、その下にproject_nameディレクトリ、testディレクトリを作成します。
project_nameディレクトリが冗長な気がしますが、結果的にこの方が何かとやり易いです。
.vscodeディレクトリはVSCodeでデバッグをするときの設定ファイルがあり、VSCodeが勝手に作ってくれます。

project_name ディレクトリ

メインのソースはここに置いてます。

tests ディレクトリ

testsディレクトリは、単体試験や関数の挙動を確認するときに、ちょっとしたソースを書いて実行するディレクトリです。

__init__.py ファイル

Pythonのお約束です。VSCodeで編集・実行するだけなら、空ファイルで問題ありません。
しかし、WindowsのコマンドプロンプトやPowerShellでproject_nameディレクトリに移動してから実行すると、このようなエラーが出てしまいます。

PS C:\hogehoge\project_name> python -m project_name
Traceback (most recent call last):
  File "C:\Python\Python310\lib\runpy.py", line 196, in _run_module_as_main
    return _run_code(code, main_globals, None,
  File "C:\Python\Python310\lib\runpy.py", line 86, in _run_code
    exec(code, run_globals)
  File "C:\hogehoge\project_name\project_name\__main__.py", line 3, in <module>
    from fuga import fugafuga
ModuleNotFoundError: No module named 'fuga'

同じディレクトリにあるfuga.pyがimportできていないようです。
で、sys.pathを調べてみると、何故かモジュール検索パスが通ってないようです(同じディレクトリ上にあるのに…)。相対インポートなら実行できるのですが、なんかカッコ悪いので、__init__.pyに以下の3行を追加しました

import os
import sys
sys.path.append(os.path.join(os.path.dirname(__file__), '.'))

このほかに、画像データなどが必要な場合には、project_nameディレクトリの下にディレクトリを作成しています。ここでは、小さなPNGファイルをアイコンとして使うのでiconディレクトリを作成しました。

CUIの雛形

コマンドラインで実行する簡単なツールや単体試験では、こんな感じにしています。

import sys

def main(argv):
    # ここにコードを書く
    return 0

if __name__ == '__main__':
    sys.exit(main(sys.argv))

コマンドライン引数が必要なければ、こんな感じにしています。

def main():
    # ここにコードを書く
    return 0

if __name__ == '__main__':
    main()

GUI(Tkinter)の雛形

Tkinterでの画面設計は人それぞれだと思いますが、

  • 画面への配置はgrid()を使う
  • 機能ごとにフレームを分ける(入れ子にする)
  • 配置が上手くいかない場合は、新しいフレーム作る
  • フレーム内のウィジェットが多い場合は、新しいクラスを作る

というルールで設計することにしました。
基本的にはproject_nameディレクトリ内にapp.py, frame.py, global_variables.py, utility.py, exec_thread.py…というファイルを作成しています。

__main__.py ファイル

一番最初に実行されるファイルです。
ここでルートウインドウを作成してメインループを開始します。

import sys
from tkinter import *
from app import Application

def main(argv):
    root = Tk()
    app = Application(master=root)
    app.mainloop()
    return 0

if __name__ == '__main__':
    sys.exit(main(sys.argv))

app.py ファイル

ここでは、ルートウインドウにタイトルの追加とウインドウの伸縮設定をして、メインフレームと情報表示用フレーム(ステータスバー)を作成しています。

from tkinter import *
from tkinter import ttk
from frame import main_frame
from frame import status_frame

class Application(ttk.Frame):
    def __init__(self, master) -> None:
        super().__init__(master)

        master.title("Application Title")
        master.columnconfigure(0, weight=1)
        master.rowconfigure(0, weight=1)
        master.resizable(width=True, height=False)

        main_frame().create_widgets(master)
        status_frame().create_widgets(master)

frame.py ファイル

ここでは、各フレームごとにクラスを作成して、そのクラスにウィジェットを追加しています。
例えば、メインフレーム内にフレーム(frame_hoge)を作成したら、hoge_frameクラスを作成して、そのメンバ関数create_widgets(self, frame)内にラベルやボタンなどのウィジェットを追加していきます。呼び出し側のメインフレームではhoge_frameクラスのcreate_widgets関数を呼び出しています。

class main_frame:
    def __init__(self) -> None:
        pass

    def create_widgets(self, master):
        frame = ttk.Frame(master, padding=5)   # <--メインフレーム作成
        frame.grid(column=0, row=0, sticky=(N, W, E, S))
        frame.columnconfigure(0, weight=1)

        frame_hoge = ttk.Frame(frame)   # <--hogeフレーム作成
        frame_hoge.grid(column=0, row=0)
        hoge_frame().create_widgets(frame_hoge)  # <--hogeフレームにウィジェットを追加
          :
          :
class hoge_frame:
    def __init__(self) -> None:
        pass

    def create_widgets(self, frame) -> None:
        ttk.Label(frame, text="hogehoge").grid(column=0, row=0)
          :
          :

ステータスフレームでは、情報表示用ラベル(ttk.Label)、プログレスバー(ttk.Progressbar)、サイズグリップ(ttk.Sizegrip)を追加しました。

class status_frame:
    def __init__(self) -> None:
        pass

    def create_widgets(self, master) -> None:
        frame = ttk.Frame(master, relief=SUNKEN)
        frame.grid(column=0, row=1, sticky=(E, N, W))

        frame.columnconfigure(0, weight=1)
        frame.rowconfigure(0, weight=1)

        gvar.lbl_status = ttk.Label(frame, text="ステータスバー")
        gvar.lbl_status.grid(column=0, row=0, padx=10, pady=3, sticky=W)

        gvar.progbar = ttk.Progressbar(frame, orient="horizontal", length=150, mode="determinate")
        gvar.progbar.grid(column=1, row=0, padx=5, pady=3)

        ttk.Sizegrip(frame).grid(column=2, row=0, sticky=(S, E))

フレームごとにクラスを分けると、ソースの見た目もウィジェットの変更も楽にできます。しかし、class間での値の受け渡しが面倒になってしまいます。そこで次のglobal_variables.pyファイルを使ってグローバル変数を宣言します。

global_variables.py ファイル

このファイルでは、クラス間で値の受け渡しを容易にするため、グローバル変数を宣言しています。
Pythonでは「型宣言ができない」というのは古いバージョンでの話で、バージョン3.5以降は必須ではないものの「型宣言をすることは可能」です。(typing — 型ヒントのサポート
ただし、必須ではないのでここで型宣言しても、違う型の値を代入するとエラーもなく、型が変わってしまいます。
ですが、ここでグローバル変数を宣言しておけば、VSCodeがキーワードを補完してくれるのでタイプミスを防ぐことができます

from tkinter import *
from tkinter import ttk

g_count: int = 0
g_width: int = 0
g_height: int = 0

filename: StringVar
dirname: StringVar
lbl_status: ttk.Label
progbar: ttk.Progressbar
    :
    :

utility.py ファイル

ここでは、比較的使いまわし出来る関数を作成しています。
例えば、ファイル選択ダイアログやディレクトリ選択ダイアログ、テキストボックス(ttk.Entry)で入力規制をするための関数などです。

# ファイル選択ダイアログ
def open_file_dialog() -> None:
    fType = [("全てのファイル", "*")]
    if gvar.filename.get() == gvar.INIT_FILENAME:
        init_dir = os.path.abspath(os.path.dirname(__file__))
    else:
        init_dir = os.path.abspath(os.path.dirname(gvar.filename.get()))
    filename = filedialog.askopenfilename(filetypes=fType, initialdir=init_dir)
    if len(filename) != 0:
        gvar.filename.set(filename)

# ディレクトリ選択ダイアログ
def open_directory_dialog() -> None:
    if gvar.dirname.get() == gvar.INIT_DIRNAME:
        init_dir = os.path.abspath(os.path.dirname(__file__))
    else:
        init_dir = os.path.abspath(os.path.dirname(gvar.dirname.get()))
    dirname = filedialog.askdirectory(initialdir=init_dir)
    if len(dirname) != 0:
        gvar.dirname.set(dirname)

# 入力値チェック(半角数字のみ許可)
def onValidateNumber(s: str) -> bool:
    if re.match(re.compile('[0-9]+'), s):
        return True
    else:
        return False

# 入力値チェック(半角英数字と-_のみ許可)
def onValidateAlphanumeric(s: str) -> bool:
    if re.match(re.compile('[a-zA-Z0-9\-_]+'), s):
        return True
    else:
        return False

exec_thread.py ファイル

ここでは、比較的時間がかかる処理をスレッドで実行するためのクラスを作成しています。

import threading
import global_variables as gvar

class hoge_process(threading.Thread):
    def run(self):
        self.exec_hoge()

    def exec_hoge(self) -> None:
        for i in range(10000)
            if gvar.flag_abort:
                break

呼び出し側(frame.py)はこんな感じになります。

import exec_thread
          :
          :
        btn_exec = ttk.Button(frame_exec, text='処理', command=self.on_hoge_process)
        btn_exec.grid(column=0, row=0)
          :
          :
    def on_hoge_process(self) -> None:
        if messagebox.askyesno(title="確認", message="処理を実行しますか?"):
            sub_thread = exec_thread.hoge_process()
            sub_thread.setDaemon(True)
            sub_thread.start()

参考

Pythonの文法などは、まずは公式サイトのチュートリアルをやってみてください。

関数の使い方などは、こちらのサイトがとても参考になりました。

Tkinter.ttkは、TkDocsのチュートリアルをやれば、だいたいの流れが分かると思います。

プロジェクト構成などは、こちらのサイトが参考になりました。

コメント

タイトルとURLをコピーしました