【第10回】クラス超入門:CoffeeLog / FishingLog を「ひとつの存在」として扱う

Python入門

ここまでのシリーズで、

  • 辞書(dict)で「1件分のログ」を表現し
  • リストで「複数件のログ」をまとめて扱い
  • if / for / while でログを絞り込んだり、対話的に入力したりし
  • CSVファイルに保存して、あとから読み返せるようにし
  • 関数で「よく使う処理」に名前をつけて整理し
  • 例外処理(try / except)で、入力ミスやファイルの有無にやさしく対応する

ところまで来ました。

だいぶ“ちゃんとしたツール”になってきましたが、
コードを眺めていると、こんな感覚も出てきませんか?

  • 「コーヒーログって、毎回同じキーを持った辞書だよな」
  • 「スコアからコメントを返す処理、どこかにくっつけておけたら楽なのに」
  • 「釣りログも、ブラウンかどうかとか、50アップかどうかとか、
     そのログ自身に聞けたら気持ちいいな」

そのタイミングで登場するのが クラス(class) です。

今回のテーマは、

CoffeeLog / FishingLog という「型」を作り、
それぞれを“ひとつの存在”として扱えるようになること。

難しいオブジェクト指向の理論は脇に置いておいて、
湖のログ・コーヒーのログを、Pythonの中で「ひとりひとりのキャラ」として
見ていく感覚を育てていきましょう


この回のゴールと全体像

第10回で目指すゴールは、次の4つです。

  1. クラス / オブジェクトのざっくりしたイメージ(設計図と実体)が持てる
  2. class__init__self、メソッド(インスタンスメソッド)の基本がわかる
  3. CoffeeLog / FishingLog クラスを定義し、属性・メソッドを使える
  4. to_dict() でCSVとの橋渡しができる(過去のコードとつながる)

ここまで来れば、

  • 「データ(状態)」
  • 「そこにくっつく処理(メソッド)」

ひとまとめの“型”として扱う 入口に立てます。


クラスとオブジェクトとは?— 設計図と、その実体たち

まずはイメージから。

  • クラス(class)
    • 設計図・型・テンプレート のようなもの
    • CoffeeLog というクラスは「コーヒーログってこういう項目を持つし、
        こういう振る舞い(メソッド)もできるよ」という設計図
  • オブジェクト/インスタンス(instance)
    • クラスという設計図から 実際に作られた“モノ”
    • 2025/05/01 の「エチオピア浅煎り 4.5点」のログ1件が、CoffeeLogのインスタンス

たとえば、

「コーヒーログという“種族”の概念がクラスで、
 実際に飲んだ一杯一杯がインスタンス」

くらいのイメージでOKです。


いちばん小さいクラスの例を見てみる

抽象的な話が続くと眠くなるので、
まずは 最小クラス の例から。

sample_class.py

class Hello:
    def say_hello(self):
        print("こんにちは、クラスの世界!")


# クラスからインスタンス(実体)を作る
greeter = Hello()

# メソッドを呼び出す
greeter.say_hello()

ターミナルから実行します:

python3 sample_class.py

実行結果:

こんにちは、クラスの世界!

ポイントは3つ:

  • class Hello: でクラスの定義を始める
  • greeter = Hello()インスタンスを1つ作る
  • greeter.say_hello()インスタンスのメソッドを呼ぶ

self は「そのインスタンス自身」を表すキーワードで、
クラスの メソッドの1番目の引数 に必ず書くのがルールです(呼ぶ側で書く必要はない)。


init で「生まれたときの初期状態」を決める

さっきの Hello クラスは「何の情報も持たない」クラスでした。

コーヒーログ / 釣りログを扱うなら、

  • コーヒー名
  • 焙煎度
  • メモ
  • スコア

のような情報を、そのインスタンスが 最初から持っている 状態にしたくなります。

その「生まれた瞬間の初期化」を行うのが、__init__ メソッドです。

CoffeeLog の最初の形

class CoffeeLog:
    def __init__(self, name, roast, memo, score):
        # self.〇〇 が「インスタンスが持つ属性」
        self.name = name
        self.roast = roast
        self.memo = memo
        self.score = score

使う側はこうなります。

log1 = CoffeeLog(
    name="エチオピア浅煎り",
    roast="浅煎り",
    memo="フルーティーで朝の湖に合う",
    score=4.5,
)

print(log1.name)   # → "エチオピア浅煎り"
print(log1.score)  # → 4.5
  • __init__ の引数(name, roast, ...)は、インスタンスを作るときに渡す材料
  • self.〇〇 は、その材料をインスタンスの「属性(プロパティ)」として保存している

self は「このインスタンス自身」を表すので、
self.name は「このログの名前」、self.score は「このログのスコア」です。


メソッドで「そのログに聞いてほしいこと」をまとめる

クラスの本領発揮は、

「そのデータに関する処理を、そのクラスのメソッドとして生やす」

ところにあります。

CoffeeLog にとって自然な質問といえば、例えば:

  • 「このコーヒーはお気に入りレベルか?」
  • 「どんなコメントをつけるべき?」

などです。

CoffeeLog にメソッドを足してみる

class CoffeeLog:
    def __init__(self, name, roast, memo, score):
        self.name = name
        self.roast = roast
        self.memo = memo
        self.score = score

    def is_favorite(self, threshold=4.0):
        """お気に入り判定(デフォルト4.0以上)"""
        return self.score >= threshold

    def comment(self):
        """スコアに応じたコメントを返す"""

        if self.score >= 4.5:
            return "かなり特別な一杯。記念すべきレベル。"
        elif self.score >= 4.0:
            return "安定して美味しい。リピートしたい。"
        elif self.score >= 3.0:
            return "普通に美味しい。気分次第でまた飲むかも。"
        else:
            return "好みとは少し違うかもしれない。"

使う側:

log1 = CoffeeLog(
    name="エチオピア浅煎り",
    roast="浅煎り",
    memo="フルーティーで朝の湖に合う",
    score=4.5,
)

print(log1.name, "はお気に入り?:", log1.is_favorite())     # True
print("コメント:", log1.comment())

以前は「スコアに応じたコメント」を関数として外に置いていましたが、
こうして CoffeeLog のメソッド にすると、

  • log1.comment()
  • log2.comment()

のように「そのログ自身に聞く」形になります。


FishingLog クラスも作ってみる

同じように、釣りログもクラスにしてみましょう。
釣りログの場合、自然な質問は:

  • 50アップか?
  • ブラウンか?レイクか?
  • リリースしたか?

などです。

FishingLog の基本形

class FishingLog:
    def __init__(self, date, lake, fish_type, length_cm, is_released):
        self.date = date
        self.lake = lake
        self.fish_type = fish_type
        self.length_cm = length_cm
        self.is_released = is_released

    def is_50up(self):
        return self.length_cm >= 50

    def is_brown(self):
        return self.fish_type == "ブラウントラウト"

    def summary(self):
        base = f"{self.date} {self.lake} {self.fish_type} {self.length_cm}cm"
        if self.is_released:
            base += "(リリース)"
        return base

使う側:

log = FishingLog(
    date="2025/05/01",
    lake="中禅寺湖",
    fish_type="ブラウントラウト",
    length_cm=52,
    is_released=True,
)

print(log.summary())        # 1行サマリー
print("50アップ?:", log.is_50up())      # True
print("ブラウン?:", log.is_brown())    # True

だんだん、「1件のログ」が ただの辞書ではなく“キャラ” に見えてきたらOKです。


CSVとの橋渡し用に to_dict() / from_dict() を用意する

これまでのシリーズでは、CSVとのやり取りに

  • 辞書のリスト({"name": ..., "score": ...}
  • csv.DictWriter / csv.DictReader

を使ってきました。

クラスと仲良くさせるには、

  • クラス → 辞書 → CSV
  • CSV → 辞書 → クラス

という橋渡しをするメソッドを用意すると便利です。

CoffeeLog に to_dict() / from_dict() を足す

class CoffeeLog:
    def __init__(self, name, roast, memo, score):
        self.name = name
        self.roast = roast
        self.memo = memo
        self.score = float(score)  # ここで型をそろえておく

    def is_favorite(self, threshold=4.0):
        return self.score >= threshold

    def comment(self):
        if self.score >= 4.5:
            return "かなり特別な一杯。記念すべきレベル。"
        elif self.score >= 4.0:
            return "安定して美味しい。リピートしたい。"
        elif self.score >= 3.0:
            return "普通に美味しい。気分次第でまた飲むかも。"
        else:
            return "好みとは少し違うかもしれない。"

    def to_dict(self):
        """CSVに書き出す用の辞書に変換"""
        return {
            "name": self.name,
            "roast": self.roast,
            "memo": self.memo,
            "score": self.score,
        }

    @classmethod
    def from_dict(cls, row):
        """CSVから読み込んだ辞書からCoffeeLogを作る"""
        return cls(
            name=row["name"],
            roast=row["roast"],
            memo=row["memo"],
            score=float(row["score"]),
        )

ここで初めて @classmethodcls が登場しましたが、

  • @classmethod:クラスに紐づいたメソッド
  • cls:そのクラス自身(CoffeeLogそのもの)

と思っておけばOKです。

CoffeeLog.from_dict(row) と呼ぶと、
CoffeeLog(...) でインスタンスを作って返してくれます。

書き込み側:クラスのリスト → 辞書のリスト → CSV

import csv

logs = [
    CoffeeLog("エチオピア浅煎り", "浅煎り", "朝の湖に合う", 4.5),
    CoffeeLog("グアテマラ中煎り", "中煎り", "安定した味", 4.0),
]

with open("coffee_logs.csv", "w", newline="", encoding="utf-8") as f:
    fieldnames = ["name", "roast", "memo", "score"]
    writer = csv.DictWriter(f, fieldnames=fieldnames)
    writer.writeheader()

    dict_logs = [log.to_dict() for log in logs]
    writer.writerows(dict_logs)

読み込み側:CSV → 辞書 → クラスのリスト

import csv

coffee_logs = []

with open("coffee_logs.csv", "r", newline="", encoding="utf-8") as f:
    reader = csv.DictReader(f)

    for row in reader:
        log = CoffeeLog.from_dict(row)
        coffee_logs.append(log)

# 50アップ…ではなく「スコア4.0以上のお気に入り」だけ表示
for log in coffee_logs:
    if log.is_favorite():
        print(log.name, log.score, log.comment())

これで、

  • CSVとの橋渡しは to_dict() / from_dict()
  • ログとしての振る舞いは is_favorite() / comment()

と役割を分けられました。


FishingLog も同じパターンで CSV とつなげる

FishingLog でも同じように to_dict() / from_dict() を用意できます。

class FishingLog:
    FIELDNAMES = ["date", "lake", "fish_type", "fish_length_cm", "is_released"]

    def __init__(self, date, lake, fish_type, length_cm, is_released):
        self.date = date
        self.lake = lake
        self.fish_type = fish_type
        self.length_cm = int(length_cm)
        self.is_released = bool(is_released)

    def is_50up(self):
        return self.length_cm >= 50

    def is_brown(self):
        return self.fish_type == "ブラウントラウト"

    def summary(self):
        base = f"{self.date} {self.lake} {self.fish_type} {self.length_cm}cm"
        if self.is_released:
            base += "(リリース)"
        return base

    def to_dict(self):
        return {
            "date": self.date,
            "lake": self.lake,
            "fish_type": self.fish_type,
            "fish_length_cm": self.length_cm,
            "is_released": self.is_released,
        }

    @classmethod
    def from_dict(cls, row):
        return cls(
            date=row["date"],
            lake=row["lake"],
            fish_type=row["fish_type"],
            length_cm=int(row["fish_length_cm"]),
            is_released=row["is_released"] in ("True", "true", "1", "y", "Y"),
        )

こうしておけば、

  • 「今シーズンの釣りログCSV」を読み込んで
  • FishingLog のリストにして
  • 50アップだけを .is_50up() で絞り込んで、.summary() で表示する

という流れが素直に書けるようになります。


コーヒーログクラス版の「全部入り」サンプル

ここまで出てきた要素をひとつにまとめた
クラス版コーヒーログツール のサンプルを載せておきます。

ファイル名:coffee_logger_class.py

# coffee_logger_class.py

import csv
import os


class CoffeeLog:
    def __init__(self, name, roast, memo, score):
        self.name = name
        self.roast = roast
        self.memo = memo
        self.score = float(score)

    def is_favorite(self, threshold=4.0):
        return self.score >= threshold

    def comment(self):
        if self.score >= 4.5:
            return "かなり特別な一杯。記念すべきレベル。"
        elif self.score >= 4.0:
            return "安定して美味しい。リピートしたい。"
        elif self.score >= 3.0:
            return "普通に美味しい。気分次第でまた飲むかも。"
        else:
            return "好みとは少し違うかもしれない。"

    def to_dict(self):
        return {
            "name": self.name,
            "roast": self.roast,
            "memo": self.memo,
            "score": self.score,
        }

    @classmethod
    def from_dict(cls, row):
        return cls(
            name=row["name"],
            roast=row["roast"],
            memo=row["memo"],
            score=float(row["score"]),
        )


def input_one_coffee_log():
    name = input("コーヒー名を入力してください(終了するには q):")
    if name == "q":
        return None

    roast = input("焙煎度(浅煎り・中煎り・深煎りなど):")
    memo = input("ひとことメモ:")
    score_str = input("スコア(0〜5の数字):")

    try:
        score = float(score_str)
    except ValueError:
        print("数値に変換できなかったので、スコアを 3.0 にします。")
        score = 3.0

    return CoffeeLog(name, roast, memo, score)


def append_logs_to_csv(filename, fieldnames, logs):
    if not logs:
        print("書き込むログがありません。")
        return

    file_exists = os.path.exists(filename)

    with open(filename, "a", newline="", encoding="utf-8") as f:
        writer = csv.DictWriter(f, fieldnames=fieldnames)
        if not file_exists:
            writer.writeheader()

        dict_logs = [log.to_dict() for log in logs]
        writer.writerows(dict_logs)

    print(f"{filename} に {len(logs)} 件のログを書き込みました。")


def main():
    filename = "coffee_logs.csv"
    fieldnames = ["name", "roast", "memo", "score"]
    logs = []

    while True:
        log = input_one_coffee_log()
        if log is None:
            print("入力を終了します。")
            break

        logs.append(log)
        print("1件ログを追加しました。")
        print("----------------------")

    append_logs_to_csv(filename, fieldnames, logs)


if __name__ == "__main__":
    main()

実行方法

1. coffee_logger_class.py を作って上のコードを貼る
2. ターミナルでファイルのあるフォルダへ移動
3. 実行:

python3 coffee_logger_class.py

4. コーヒー名などを入力し、やめたくなったらコーヒー名に q
5. 同じフォルダに coffee_logs.csv ができていればOK

中身をあとから読み込んで、CoffeeLog.from_dict(row) でインスタンスに戻せば、
「昔の一杯」に .is_favorite().comment() で再び問いかけることができます。


今日のまとめ – ログが「ただのデータ」から「小さな存在」になる

今回は、

  • クラスとオブジェクト(インスタンス)のイメージ(設計図と実体)
  • class / __init__ / self / メソッドの基本
  • CoffeeLog / FishingLog に属性とメソッドを持たせる
  • to_dict() / from_dict() でCSVとの橋渡しをする
  • クラス版のコーヒーログツール(coffee_logger_class.py)の例

を見てきました。

これで、
コーヒーや釣りのログは 「ただの辞書の集まり」 から、

「名前やスコアやコメントを持った、小さな存在(オブジェクト)」

に一歩近づいたはずです。

コメント