【Python初学者】楽天価格チェッカーを作ってみた ⑧(最終回) ― 全体統括(main_flow.py)
はじめに🐰
Python初学者のわたしが覚えるために、学んだことを整理し、理解を深めるために記事を書いています。
私が実際にやってみて、悩んだ部分や過程などを残していきます🐥シリーズ最終回です!
今回は main_flow.py の解説です。これまでのファイルすべてを組み合わせて、プログラム全体の処理を順番に実行するクラスです。「指揮者」のような役割を担っています。
本日のゴール⚽️
- クラスの
__init__で複数のオブジェクトを準備する方法を理解する -
try / exceptで予期せぬエラーを安全に処理できるようになる - 処理をメソッドに分割する設計の考え方を理解する
完成したコード
import os
import sys
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "./")))
from api.rakuten_api import RakutenAPI
from price_tools.price_list_builder import PriceListBuilder
from price_tools.price_stats import PriceStats
from utils.csv_saver import CsvSaver
from utils.popup import PopupManager
class MainFlow:
def __init__(self):
# ① 各機能クラスをここで準備する(まとめて持っておく)
self.api = RakutenAPI()
self.builder = PriceListBuilder()
self.stats = PriceStats()
self.csv = CsvSaver()
# -----------------------
# ① キーワード入力(ユーザー操作)
# -----------------------
def input_keywords(self) -> tuple[str, str]:
raw_input = PopupManager.ask_keywords()
# カンマ区切り → リスト化(空白は削除)
keyword_list = [kw.strip() for kw in raw_input.split(",") if kw.strip()]
# 表示用(見やすい形)
display_keyword = ", ".join(keyword_list)
# API用(スペース区切り)
api_keyword = " ".join(keyword_list)
return display_keyword, api_keyword
# -----------------------
# ② 楽天APIから商品取得(複数ページ対応)
# -----------------------
def fetch_all_products(self, api_keyword: str, max_pages: int = 5) -> list[dict]:
all_products = []
for page in range(1, max_pages + 1):
api_data = self.api.search(api_keyword, page=page)
if not api_data:
break
products = self.api.format_product_data(api_data)
if not products:
break
all_products.extend(products)
return all_products
# -----------------------
# ③ データ加工(フィルタ・並び替え・集計)
# -----------------------
def process_products(self, raw_list: list[dict]) -> tuple[list[dict], dict]:
# 条件に合う商品のみ残す
filtered = self.builder.filter_list(raw_list)
if not filtered:
return [], {}
# 安い順に並び替え
sorted_list = self.builder.sort_by_price(filtered)
# 最小・最大・平均などを計算
summary = self.stats.get_price_summary(sorted_list)
return sorted_list, summary
# -----------------------
# ④ 結果表示(画面出力)
# -----------------------
def print_result(self, display_keyword: str, sorted_list: list[dict], summary: dict):
print("\n" + "=" * 50)
print(f"検索結果:{display_keyword}")
print("=" * 50)
if not sorted_list:
print("該当商品が見つかりませんでした。")
return
print(f"件数:{len(sorted_list)}件")
print(f"最安:{summary.get('min_price', '-'):,}円")
print(f"平均:{summary.get('avr_price', '-'):,}円")
print(f"最高:{summary.get('max_price', '-'):,}円")
print("-" * 50)
print("▼ 上位10件(安い順)")
for i, p in enumerate(sorted_list[:10], start=1):
print(f"{i:>2}. {p['price']:>8,}円 {p['name'][:40]}")
# -----------------------
# ⑤ CSV保存
# -----------------------
def save_csv(self, display_keyword: str, sorted_list: list[dict], summary: dict):
filepath = self.csv.save(
keyword=display_keyword,
summary=summary,
product_list=sorted_list,
count=len(sorted_list)
)
PopupManager.show_complete(filepath)
# -----------------------
# ⑥ 全体の流れ(司令塔)
# -----------------------
def run(self):
try:
# ① キーワード入力
display_keyword, api_keyword = self.input_keywords()
# ② API取得
raw_list = self.fetch_all_products(api_keyword)
if not raw_list:
print("商品が取得できませんでした")
return
# ③ 加工
sorted_list, summary = self.process_products(raw_list)
# ④ 表示
self.print_result(display_keyword, sorted_list, summary)
if not sorted_list:
return
# ⑤ CSV保存
self.save_csv(display_keyword, sorted_list, summary)
except ValueError as e:
PopupManager.show_error(str(e))
except Exception as e:
PopupManager.show_error(f"予期せぬエラーが発生しました\n\n{e}")
コードの詳細解説
① main_flowは「司令塔」
def run(self):
ここは何も考えずに順番に呼んでいるだけです。
👉 料理でいうと「レシピ通りに工程を並べてるだけ」
② 処理をメソッドに分ける設計
def run(self) -> None:
display_keyword, api_keyword = self.input_keywords()
raw_list = self.fetch_all_products(api_keyword)
sorted_list, summary = self.process_products(raw_list)
self.print_result(display_keyword, sorted_list, summary)
self.save_csv(display_keyword, sorted_list, summary)
run() メソッドは①〜⑤の処理を順番に呼び出すだけです。各処理の詳細は別メソッドに任せています。
なぜ分けるのか?
-
run()を見るだけで全体の流れが一目でわかる - 各メソッドが独立しているので、1つを修正しても他に影響しない
- 部分的なテストが書きやすくなる
③ tuple で複数の値を返す
def input_keywords(self) -> tuple[str, str]:
...
return display_keyword, api_keyword
# 呼び出し側
display_keyword, api_keyword = self.input_keywords()
Pythonはカンマで区切ることで複数の値をまとめて返せます(タプル)。受け取る側も同様にカンマで分けて受け取れます(アンパック)。
# タプルの基本
def get_name_age():
return "えりこ", 25
name, age = get_name_age()
# name → "えりこ"
# age → 25
④ split() と strip() でキーワードを整形する
keyword_list = [kw.strip() for kw in raw_input.split(",") if kw.strip()]
ユーザーが「青汁, 雑穀米 , 」と入力したとき(スペースや末尾のカンマが混じっている)、きれいに整形しています。
raw_input = "青汁, 雑穀米 , "
# split(",") でカンマ区切り
raw_input.split(",")
# → ["青汁", " 雑穀米 ", " ", ""]
# strip() で前後の空白を除去
# if kw.strip() で空文字を除外
keyword_list = [kw.strip() for kw in raw_input.split(",") if kw.strip()]
# → ["青汁", "雑穀米"]
| メソッド | 役割 |
|---|---|
split(",") |
カンマで区切りリストにする |
strip() |
文字列の前後の空白(スペース・改行)を除去する |
⑤ for + range() でページ番号をループする
for page in range(1, max_pages + 1):
range(1, 6) は 1, 2, 3, 4, 5 を生成します(6は含まない)。max_pages = 5 のとき、range(1, 6) でページ1〜5をループします。
# range() の基本
range(5) # → 0, 1, 2, 3, 4
range(1, 6) # → 1, 2, 3, 4, 5
range(1, 10, 2) # → 1, 3, 5, 7, 9(2ずつ増える)
⑥ extend() でリストを結合する
all_products.extend(products)
append() は1つの要素を追加しますが、extend() はリストの中身を展開して追加します。
# append と extend の違い
a = [1, 2, 3]
b = [4, 5]
a.append(b) # → [1, 2, 3, [4, 5]] ← リストがそのまま入る
a.extend(b) # → [1, 2, 3, 4, 5] ← 中身が展開される
複数ページの商品をまとめて1つのリストにするため extend() を使っています。
⑦ f文字列の書式指定
print(f"最安価格:{summary.get('min_price', '-'):,}円")
print(f" {i:>2}. {product['price']:>8,}円")
f文字列では {変数:書式} の形で表示形式を指定できます。
| 書式 | 意味 | 例 |
|---|---|---|
:, |
3桁カンマ区切り | 1980 → 1,980 |
:>8 |
右寄せ(幅8文字) | 980 → 980
|
:<10 |
左寄せ(幅10文字) | abc → abc
|
価格の列を揃えて表示するために使っています。
⑧ try / except で2種類のエラーを分けて処理する
try:
...
except ValueError as e:
PopupManager.show_error(str(e))
except Exception as e:
PopupManager.show_error(f"予期せぬエラーが発生しました。\n\n{e}")
ValueError はキャンセル時など「想定内のエラー」です。Exception はその他すべての「想定外のエラー」を受け取ります。
try
↓ エラー発生
except ValueError ← まずこちらで確認(特定のエラー)
↓ 該当しない
except Exception ← それ以外のすべてをここで受け取る
except は上から順番に評価されるため、特定のエラーを先に書き、汎用的な Exception は最後にするのがルールです。
このプロジェクトを通じて学んだこと
全9回を通じて、次のPython知識が使われていました。
| ファイル | 主な学び |
|---|---|
| rakuten_api.py |
requests、API連携、.env、try/except
|
| price_list_builder.py | リスト内包表記、any()、sorted()、lambda
|
| price_stats.py |
min() max() sum()、// 整数除算 |
| csv_saver.py |
csvモジュール、datetime、ファイル操作 |
| popup.py |
tkinter、@staticmethod、イベント処理 |
| path_helper.py |
pathlib.Path、__file__、getattr()
|
| config.py | 定数の分離管理 |
| main_flow.py | クラス設計、tuple、split/strip、extend()
|
まとめ
main_flow.py はこのプロジェクトの指揮者です。各クラスを __init__ で準備し、run() から順番に呼び出すだけのシンプルな設計にすることで、全体の流れが一目でわかるようになっています。
シリーズを通じて「実際に動くツールを作りながらPythonを学ぶ」ことを目標に書いてきました。間違いや改善点があれば、ぜひコメントで教えてください!
シリーズ一覧(完結)
- ① プロジェクト全体像とフォルダ構成
- ② 楽天APIリクエスト(rakuten_api.py)
- ③ フィルタリング・並び替え(price_list_builder.py)
- ④ 統計計算(price_stats.py)
- ⑤ CSV保存(csv_saver.py)
- ⑥ ポップアップUI(popup.py)
- ⑦ パス管理・設定(path_helper.py & config.py)
- ⑧ 全体統括(main_flow.py) ← 今回(最終回)