コーディング — Python 3.12 モダンリファクタリング

2026-04-05 (Day 1) A: コーディング ★★★☆☆ StrEnum / dataclass(slots) / match文

概要

🏷️

StrEnum(Python 3.11+)

str のサブクラス。OrderStatus.PENDING == "pending" が成立し、DBやAPIとのシリアライズで .value 不要。

dataclass(slots=True)

__slots__ を自動生成。属性アクセスが速く・メモリ消費が少ない(約20%改善)。Python 3.10+ で安定。

🔀

match 文(PEP 634)

Structural Pattern Matching。if/elif の連鎖を読みやすいパターンマッチに変換。Python 3.10+ で安定。

🔍

isinstance vs type() !=

type(x) != float は継承を考慮しない。isinstance(x, float) が正しい。サブクラスも捕捉できる。

問題

以下の「レガシーな」Pythonコードがあります。Python 3.12+ のモダンな書き方にリファクタリングしてください。

リファクタリング要件

#要件
1@dataclass(slots=True) でクラスを定義する(Python 3.10+、メモリ効率が高い)
2statusStrEnum(Python 3.11+ 標準)で型安全にする
3can_cancel()match 文で書き直す
4型ヒントを全メソッドに付ける(mypy --strict 相当)
5apply_discount の例外を TypeError / ValueError で適切に raise する
6get_summary を f-string で書き直す
7items の要素も @dataclass の型として定義する(dict ではなく typed class に)

制約・前提条件

  • Python 3.12+ を前提
  • 標準ライブラリのみ(dataclasses, enum のみ使用可)
  • 既存の動作(インターフェース・戻り値)は変えない
期待する回答形式: Pythonコード

悪いコード (Before)

このコードには 7つのモダン化ポイント が隠れています。見つけてみてください。
# legacy_order.py

class Order:
    def __init__(self, order_id, status, items, total):
        self.order_id = order_id
        self.status = status   # "pending", "shipped", "delivered", "cancelled"
        self.items = items     # list of dict: [{"name": str, "qty": int, "price": float}]
        self.total = total

    def get_summary(self):
        summary = "Order: " + str(self.order_id) + "\n"
        summary += "Status: " + self.status + "\n"
        summary += "Items:\n"
        for item in self.items:
            summary += "  - " + item["name"] + " x" + str(item["qty"]) + " @ " + str(item["price"]) + "\n"
        summary += "Total: " + str(self.total)
        return summary

    def can_cancel(self):
        if self.status == "pending":
            return True
        elif self.status == "shipped":
            return False
        elif self.status == "delivered":
            return False
        elif self.status == "cancelled":
            return False
        else:
            return False

    def apply_discount(self, rate):
        if type(rate) != float:          # ← isinstance を使うべき
            raise Exception("rate must be float")
        if rate < 0 or rate > 1:
            raise Exception("rate must be between 0 and 1")
        self.total = self.total * (1 - rate)
        return self.total

ヒント(段階的開示)

ヒント1 — 方向性
Orderitems の要素は別々のクラスとして定義するのが 2026 年時点のデファクト。 dict を受け取るAPIは型チェックが効かないため、型付きのデータクラスに置き換える。
ヒント2 — アプローチ
  • StrEnumfrom enum import StrEnum でインポートできる(Python 3.11+)。str と互換性があるため .value 不要で比較できる
  • @dataclass(slots=True) を使うと __slots__ が自動生成される(通常のdataclassより約20%メモリ効率が良い)
  • match 文のガード句: case OrderStatus.PENDING: return True / case _: return False のように書ける
ヒント3 — 目指す骨格
from dataclasses import dataclass
from enum import StrEnum

class OrderStatus(StrEnum):
    PENDING = "pending"
    # ...

@dataclass(slots=True)
class OrderItem:
    name: str
    qty: int
    price: float

@dataclass(slots=True)
class Order:
    order_id: int
    status: OrderStatus
    items: list[OrderItem]   # list[X] 形式(Python 3.9+ で typing.List 不要)
    total: float

    def can_cancel(self) -> bool:
        match self.status:
            case OrderStatus.PENDING:
                return True
            case _:
                return False

設計構造図 — モダンPythonクラス設計

OrderStatus (StrEnum) str と互換性あり PENDING = "pending" SHIPPED = "shipped" DELIVERED / CANCELLED OrderItem @dataclass(slots=True) name: str qty: int price: float Order @dataclass(slots=True) order_id: int status: OrderStatus items: list[OrderItem] total: float Methods get_summary() -> str can_cancel() -> bool [match]

模範解答

from dataclasses import dataclass
from enum import StrEnum


class OrderStatus(StrEnum):
    PENDING   = "pending"
    SHIPPED   = "shipped"
    DELIVERED = "delivered"
    CANCELLED = "cancelled"


@dataclass(slots=True)
class OrderItem:
    name: str
    qty: int
    price: float


@dataclass(slots=True)
class Order:
    order_id: int
    status: OrderStatus
    items: list[OrderItem]
    total: float

    def get_summary(self) -> str:
        items_str = "\n".join(
            f"  - {item.name} x{item.qty} @ {item.price}"
            for item in self.items
        )
        return (
            f"Order: {self.order_id}\n"
            f"Status: {self.status}\n"
            f"Items:\n{items_str}\n"
            f"Total: {self.total}"
        )

    def can_cancel(self) -> bool:
        match self.status:
            case OrderStatus.PENDING:
                return True
            case _:
                return False

    def apply_discount(self, rate: float) -> float:
        if not isinstance(rate, float):
            raise TypeError(f"rate must be float, got {type(rate).__name__}")
        if not 0.0 <= rate <= 1.0:
            raise ValueError(f"rate must be between 0 and 1, got {rate}")
        self.total *= 1 - rate
        return self.total

ポイント解説

1 StrEnum(Python 3.11+)
str のサブクラスなので OrderStatus.PENDING == "pending"True。DBやAPIとのシリアライズで .value 不要になり、既存コードとの後方互換が保ちやすい。
2 @dataclass(slots=True)
__slots__ を自動生成し、属性アクセスが速く・メモリ消費が少ない。Python 3.10+ で安定。frozen=True と組み合わせると不変オブジェクトにもできる。
3 list[OrderItem](小文字)
Python 3.9+ から typing.List は不要。list[X], dict[K,V], tuple[X,...] の組み込み型で型ヒントを書くのが 2026 年のデファクト。
4 isinstance vs type() !=
type(x) != float は継承を考慮しない。isinstance(x, float) が正しい。さらに int も許容したい場合は isinstance(x, (int, float)) と書ける。
5 match 文の case _:
else 相当のフォールスルー。Structural Pattern Matching(PEP 634)は Python 3.10+ で安定、チームコードでも普及済み。

Before vs After 比較

✗ Before(レガシー)
class Order:
    # 型ヒントなし
    # status は生文字列
    # items は dict のリスト
    # type() != float で型チェック
    # if/elif の連鎖
    def can_cancel(self):
        if self.status == "pending":
            return True
        elif ...
✓ After(モダン)
@dataclass(slots=True)
class Order:
    status: OrderStatus   # StrEnum
    items: list[OrderItem]  # typed class

    def can_cancel(self) -> bool:
        match self.status:
            case OrderStatus.PENDING:
                return True
            case _:
                return False

実務への応用

販促システム(MOps)での活用例
  • 販促クーポン・割引ルールを StrEnum で管理: BigQueryへの書き出し・dbtモデルの文字列比較と型が一致しやすい
  • Argo Workflowsのタスク状態管理: pending / running / succeeded / failedStrEnum + match で書くとステートマシンが読みやすくなる
  • Kubernetes ConfigMap: @dataclass(slots=True, frozen=True) で「書き換え不可の設定オブジェクト」を作ると、ConfigMapから読み込んだ設定を安全に扱える

次のステップ

発展問題1: Order を Pydantic v2 の BaseModel で書き直すとどう変わるか?(バリデーション・JSON シリアライズの違いを比較)
発展問題2: apply_discount@dataclass(frozen=True) で不変にするにはどう設計を変えるか?(新しいインスタンスを返す設計)
  • 参考キーワード: PEP 634 Structural Pattern Matching, PEP 681 dataclass_transform, StrEnum, dataclasses.replace()

今日のまとめ

StrEnum + @dataclass(slots=True) + match 文の組み合わせが Python 3.12 時点のモダンな値オブジェクト設計のデファクト。

dict を渡すより型付きクラスにすることで、mypy・IDE補完・テストの品質が一段上がる。

isinstance vs type() !=TypeError vs ValueError の使い分けも実務での基礎。

自己評価

自分の回答

気づき・メモ