JSON形式のAPIレスポンスや、データベースから取得したレコードなど、構造化されたデータを扱う際、Pythonでは手軽に辞書(dict)を使いがちです。しかし、アプリケーションが少しでも複雑になってくると、この「とりあえず辞書」のアプローチは、コードを脆く、読みにくくする原因となります。
より堅牢でメンテナンス性の高いコードを書くための解決策は、そのデータを表現するためのクラスを定義することです。今回は、辞書ベースのアプローチが抱える問題点と、Python 3.7から導入されたdataclassがいかにしてそれを解決するのかを解説します。
問題点:データとロジックが分離した、辞書ベースのアプローチ
書籍のデータ(タイトル、著者名、出版年)を扱うシナリオを考えてみましょう。まず、辞書を使ってデータを表現し、それらを操作する関数をいくつか作成します。
悪い例:データは辞書、処理はバラバラの関数
from datetime import date
# データを辞書として定義
book_dict = {
    "title": "Python実践入門",
    "author_last_name": "山田",
    "author_first_name": "太郎",
    "publish_year": 2022,
}
# 書籍のフルタイトルを取得する関数
def get_book_display_title(a_book: dict) -> str:
    return f'『{a_book["title"]}』({a_book["author_first_name"]} {a_book["author_last_name"]})'
# 出版からの経過年数を計算する関数
def get_years_since_publication(a_book: dict) -> int:
    return date.today().year - a_book["publish_year"]
# 関数の利用
print(get_book_display_title(book_dict))
print(f"出版から{get_years_since_publication(book_dict)}年が経過")
このアプローチには、いくつかの根深い問題があります。
- データとロジックの分離: 
book_dictというデータと、それを操作するget_...という関数群が完全に分離しています。これにより、書籍に関する処理がコードベースのあちこちに散らばってしまいます。 - 構造の保証がない: 
get_book_display_title関数は、引数a_bookが"title"というキーを持つことを暗黙的に期待しています。もしキーが存在しない辞書が渡された場合、実行時にKeyErrorが発生します。データの「あるべき姿(スキーマ)」がどこにも定義されていません。 - 保守性の低さ: もし将来、キーの名前を
"publish_year"から"published_on"に変更する必要が生じたら、このキーを参照している全ての関数を探し出して修正しなければなりません。これは非常に手間がかかり、修正漏れによるバグの原因となります。 - 開発ツールの恩恵を受けられない: IDEは
book_dict["..."]の中身を知らないため、キー名の自動補完やタイポのチェックといったサポート機能が働きません。 
解決策:dataclassでデータとロジックを一つにまとめる
これらの問題は、オブジェクト指向の基本である**「カプセル化」**、つまりデータとそれに関連する振る舞いを一つの単位(クラス)にまとめることで解決できます。Pythonでは、dataclassを使うとそのためのクラスを驚くほど簡単に定義できます。
良い例:Bookクラスを定義し、データと処理をカプセル化
from dataclasses import dataclass
from datetime import date
@dataclass
class Book:
    # 属性(データ)を型ヒント付きで明確に定義
    title: str
    author_last_name: str
    author_first_name: str
    publish_year: int
    @property
    def display_title(self) -> str:
        """書籍のフルタイトルを返すプロパティ。"""
        return f'『{self.title}』({self.author_first_name} {self.author_last_name})'
    @property
    def years_since_publication(self) -> int:
        """出版からの経過年数を返すプロパティ。"""
        return date.today().year - self.publish_year
# クラスの利用
book = Book(
    title="Python実践入門",
    author_last_name="山田",
    author_first_name="太郎",
    publish_year=2022,
)
# オブジェクトの属性やプロパティとして、直感的にアクセスできる
print(book.display_title)
print(f"出版から{book.years_since_publication}年が経過")
この変更により、コードの質は劇的に向上しました。
- 信頼できる唯一の情報源: 
Bookクラスの定義が、このデータの構造と型に関する「信頼できる唯一の情報源」となります。 - カプセル化: 書籍に関するデータ(属性)とロジック(プロパティ)が
Bookクラスという一つの場所にまとまりました。get_years_since_publication(book_dict)ではなく、book.years_since_publicationと直感的にアクセスできます。 - 保守性の向上: 
publish_yearの名前を変更したくなったら、クラス定義の1箇所を修正するだけで済みます。 - 型安全性と開発効率: 型ヒントにより、IDEは
book.とタイプしただけでtitleやdisplay_titleといった属性を補完してくれます。これにより、タイプミスが減り、開発スピードが向上します。 
まとめ
データの取り扱いにおける指針はシンプルです。
- 一時的で構造のないデータには辞書を使う。
 - アプリケーション内で一貫した構造を持つデータ(ユーザー、商品、書籍など)には、必ずクラスを定義する。
 
特にdataclassは、データ構造を表現するためのクラスを最小限の記述で作成できる、非常に強力なツールです。辞書を関数に渡して処理するコードを見かけたら、それはクラスを導入する絶好の機会かもしれません。データとその振る舞いを一つのクラスにまとめることで、コードをより堅牢で、直感的で、メンテナンスしやすいものにしましょう。
