概要
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+、メモリ効率が高い) |
| 2 | status を StrEnum(Python 3.11+ 標準)で型安全にする |
| 3 | can_cancel() を match 文で書き直す |
| 4 | 型ヒントを全メソッドに付ける(mypy --strict 相当) |
| 5 | apply_discount の例外を TypeError / ValueError で適切に raise する |
| 6 | get_summary を f-string で書き直す |
| 7 | items の要素も @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 — 方向性
Order と items の要素は別々のクラスとして定義するのが 2026 年時点のデファクト。
dict を受け取るAPIは型チェックが効かないため、型付きのデータクラスに置き換える。
ヒント2 — アプローチ
StrEnumはfrom 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クラス設計
模範解答
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+ から
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 / failedをStrEnum+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 の使い分けも実務での基礎。