Python | 1 日 120 分 × 7 日アプリ学習:クラスで作る商品管理アプリ(中級編)

Web APP Python
スポンサーリンク

6日目のゴール

6日目のテーマは
「アプリ全体をクラスとしてまとめ、“アプリそのもの”をオブジェクトとして扱えるようになること」 です。

ここまでであなたは、

Product クラスで「商品という型」を作り
複数の商品をリストで管理し
CRUD・検索・並び替え・ファイル保存/読み込み

までを、関数とグローバル変数の組み合わせで作ってきました。

6日目では、ここから一段階進んで、

「商品管理アプリそのもの」をクラスにする
アプリの状態(商品一覧・ファイル名など)を __init__ で初期化する
メニュー処理やループもクラスのメソッドとしてまとめる

という「一段大きな粒度のクラス設計」を体験してもらいます。


まずは整理 アプリはいま何を持っているか

これまでの構成を言葉で分解する

5日目までのアプリを、ざっくり言葉で分解すると、こうなります。

商品を表す Product クラスがある。
商品一覧を表す products リストがある。
商品を追加・更新・削除・検索・表示する関数がある。
商品一覧をファイルに保存・読み込みする関数がある。
メニューを表示し、ユーザーの入力に応じて関数を呼び分ける main がある。

このうち、

「products リスト」
「データファイル名」
「メニューとループ」

は、本来「アプリ全体の状態・振る舞い」です。

これを一つのクラスにまとめると、
コードの見通しが一気によくなります。


アプリ全体を表すクラス ProductApp を作る

「アプリというオブジェクト」を作る発想

新しく ProductApp というクラスを作り、
「商品管理アプリそのもの」をオブジェクトとして表現します。

まずは骨組みから。

class ProductApp:
    def __init__(self, data_file):
        self.data_file = data_file
        self.products = []
Python

ここで深掘りしたいポイントは二つです。

一つ目は、__init__ が「アプリが生まれた瞬間に呼ばれる初期化処理」であること。
二つ目は、self.data_fileself.products が「このアプリオブジェクトが持つ状態」になっていること。

つまり、

app = ProductApp("products.json")

とした瞬間に、

「このアプリは products.json を使う」
「このアプリは products という商品一覧を持っている」

という状態がセットされます。


ファイル読み込み・保存をアプリのメソッドにする

「アプリが自分でデータを読み書きする」形にする

5日目では、ファイル読み込み・保存は
独立した関数として書いていました。

これを ProductApp のメソッドに移します。

import json


class ProductApp:
    def __init__(self, data_file):
        self.data_file = data_file
        self.products = []

    def load_products(self):
        try:
            with open(self.data_file, "r", encoding="utf-8") as f:
                data_list = json.load(f)
        except FileNotFoundError:
            print(f"{self.data_file} が見つかりません。空の商品一覧から始めます。")
            self.products = []
            return
        except json.JSONDecodeError:
            print("ファイルの内容が壊れているか、JSON ではありません。空の商品一覧から始めます。")
            self.products = []
            return
        except OSError as e:
            print("ファイルの読み込み中にエラーが発生しました。空の商品一覧から始めます。")
            print("詳細:", e)
            self.products = []
            return

        self.products = [Product.from_dict(d) for d in data_list]
        print(f"商品情報をファイルから読み込みました: {self.data_file}")

    def save_products(self):
        data_list = [p.to_dict() for p in self.products]

        try:
            with open(self.data_file, "w", encoding="utf-8") as f:
                json.dump(data_list, f, ensure_ascii=False, indent=2)
            print(f"商品情報をファイルに保存しました: {self.data_file}")
        except OSError as e:
            print("ファイルの保存中にエラーが発生しました。")
            print("詳細:", e)
Python

ここでの重要ポイントは、

self.data_fileself.products を使っていること。
読み込み後に self.products に Product のリストをセットしていること。
保存時には self.products から辞書のリストを作っていること。

つまり、

「このアプリオブジェクトは、自分の持つ商品一覧を、自分のファイルに読み書きできる」

という状態になっています。


商品操作の関数を「アプリのメソッド」に移す

add_product をメソッド化する

これまでの add_product(products) を、
ProductApp のメソッドとして書き直します。

class ProductApp:
    # __init__, load_products, save_products は省略

    def add_product(self):
        print("=== 商品登録 ===")
        product_id = input("商品ID: ").strip()
        name = input("商品名: ").strip()
        price_text = input("価格(整数): ").strip()
        stock_text = input("在庫数(整数): ").strip()

        if not price_text.isdigit() or not stock_text.isdigit():
            print("価格と在庫数は数字で入力してください。")
            return

        price = int(price_text)
        stock = int(stock_text)

        product = Product(product_id, name, price, stock)
        self.products.append(product)

        print("商品を登録しました。")
Python

ここでのポイントは、

products ではなく self.products を使っていること。
引数に products を渡さなくてよくなっていること。

「アプリが自分の中の products を直接操作している」
という形になっています。

一覧表示もメソッドにする

同じように、一覧表示も移します。

    def show_all_products(self):
        print("=== 商品一覧 ===")

        if not self.products:
            print("商品がまだ登録されていません。")
            return

        for product in self.products:
            product.show_info()
            print("----------")
Python

ここでも、
self.products をぐるっと回しているだけです。


検索・更新・削除もアプリの中に入れる

IDで商品を探すメソッド

まずは、find_product_by_id
ProductApp のメソッドにします。

    def find_product_by_id(self, product_id):
        for product in self.products:
            if product.product_id == product_id:
                return product
        return None
Python

これで、アプリの中から

p = self.find_product_by_id("p001")
Python

のように呼べます。

更新メソッド

更新も、self.productsself.find_product_by_id を使う形にします。

    def update_product(self):
        print("=== 商品更新 ===")
        product_id = input("更新したい商品ID: ").strip()

        product = self.find_product_by_id(product_id)
        if product is None:
            print("そのIDの商品は存在しません。")
            return

        print("現在の情報:")
        product.show_info()

        new_name = input("新しい商品名(変更しない場合は空欄のまま): ").strip()
        new_price_text = input("新しい価格(変更しない場合は空欄のまま): ").strip()
        new_stock_text = input("新しい在庫数(変更しない場合は空欄のまま): ").strip()

        if new_name != "":
            product.name = new_name

        if new_price_text != "":
            if not new_price_text.isdigit():
                print("価格は数字で入力してください。価格の変更は行いません。")
            else:
                product.price = int(new_price_text)

        if new_stock_text != "":
            if not new_stock_text.isdigit():
                print("在庫数は数字で入力してください。在庫数の変更は行いません。")
            else:
                product.stock = int(new_stock_text)

        print("商品情報を更新しました。")
        product.show_info()
Python

削除メソッド

削除も同様です。

    def delete_product(self):
        print("=== 商品削除 ===")
        product_id = input("削除したい商品ID: ").strip()

        product = self.find_product_by_id(product_id)
        if product is None:
            print("そのIDの商品は存在しません。")
            return

        print("削除対象の商品:")
        product.show_info()

        confirm = input("本当に削除しますか? (y/N): ").strip().lower()
        if confirm == "y":
            self.products.remove(product)
            print("商品を削除しました。")
        else:
            print("削除をキャンセルしました。")
Python

ここまで来ると、
「アプリの中に、アプリに必要な操作が全部入っている」
という感覚が出てきます。


メニューとメインループもクラスに入れる

show_menu と run をアプリのメソッドにする

最後に、メニュー表示とメインループを
ProductApp の中に入れます。

    def show_menu(self):
        print("==========")
        print("商品管理アプリ(クラス編 6日目)")
        print("1: 商品を登録する")
        print("2: 商品一覧を表示する")
        print("3: 商品情報を更新する")
        print("4: 商品を削除する")
        print("5: 商品をIDで表示する")
        print("6: 在庫が少ない商品を表示する")
        print("7: 商品一覧(価格の安い順)")
        print("8: 商品一覧(名前順)")
        print("9: 商品情報をファイルに保存する")
        print("0: 終了")
        print("==========")

    def run(self):
        self.load_products()

        while True:
            self.show_menu()
            choice = input("番号を選んでください: ").strip()

            if choice == "1":
                self.add_product()
            elif choice == "2":
                self.show_all_products()
            elif choice == "3":
                self.update_product()
            elif choice == "4":
                self.delete_product()
            elif choice == "5":
                self.show_product_detail()
            elif choice == "6":
                self.show_low_stock_products()
            elif choice == "7":
                self.show_products_sorted_by_price()
            elif choice == "8":
                self.show_products_sorted_by_name()
            elif choice == "9":
                self.save_products()
            elif choice == "0":
                print("アプリを終了します。変更を保存します。")
                self.save_products()
                break
            else:
                print("不正な入力です。0〜9 のどれかを選んでください。")
Python

ここでの大事なポイントは、

run が「このアプリオブジェクトを動かすメインループ」になっていること。
self.load_products() で「起動時にデータを読み込んでいる」こと。
各機能を self.メソッド名() で呼び分けていること。

これで、外側のコードは
とてもシンプルになります。

def main():
    app = ProductApp("products.json")
    app.run()


main()
Python

「アプリを作って、アプリを走らせる」
という、直感的な形になりました。


6日目のまとめ 今日つかんでほしい感覚

今日の本質は、これです。

__init__ は「オブジェクトが生まれた瞬間に、そのオブジェクトの状態を初期化する場所」。
Product は「商品という型」、ProductApp は「商品管理アプリという型」。
アプリ全体をクラスにすると、「状態(products, data_file)と振る舞い(CRUD, 保存, ループ)」が一つにまとまる。
self は「このアプリオブジェクト自身」であり、self.productsself.data_file によって状態を持てる。
外側から見ると、「app = ProductApp(…); app.run()」という、とても自然な形でアプリを扱える。

ここまで来たあなたは、
「クラスでデータを表現する」だけでなく
「クラスでアプリ全体を表現する」という一段上の設計に、
しっかり足を踏み入れています。

7日目では、このアプリ全体の構造を
自分の言葉で説明したり、少しアレンジしたりしながら、
中級編の総仕上げをしていきます。

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