【Python】dataclassの使い方|__init__自動生成・default_factory・frozen

【Python】dataclassの使い方|__init__自動生成・default_factory・frozen Python

dataclass(データクラス)は、データを入れるためのクラスを簡単に作る仕組みです。通常のクラス__init____repr__を毎回書くのは手間ですが、@dataclassを付けるだけで、これらが自動で生成されます。座標・設定・APIのレスポンスなど、「値をまとめて持つ」用途のクラスが、数行で書けます。

つまずきやすいのは、リストや辞書を初期値にするときです。items: list = []のように直接書くとエラーになり、field(default_factory=list)を使う必要があります。これは関数の可変デフォルト引数と同じ落とし穴への対策です。この記事では、実機のPython 3.12で確認しながら、dataclassを整理します。

先に結論

  • @dataclassを付けると、__init____repr____eq__が自動生成されます。
  • フィールドはx: intのように型ヒント付きで書きます。デフォルト値も指定できます。
  • リスト・辞書を初期値にするときはfield(default_factory=list)を使います。
  • 直接items: list = []と書くとエラーになります。
  • @dataclass(frozen=True)変更できない(イミュータブル)クラスになります。
  • asdict()で辞書に変換できます。

土台のクラス(class)、フィールドに付ける型ヒント、可変デフォルトの罠が共通する関数(def)もあわせて参考になります。

スポンサーリンク

dataclassとは(定型コードを自動生成)

通常のクラスで「値をまとめて持つ」だけのものを作ると、__init__self.x = xを並べ、__repr__で表示を整え……と同じような定型コードを毎回書くことになります。dataclassは、この定型部分を自動生成してくれるため、フィールドの定義だけに集中できます。

基本:@dataclassとフィールド

使い方は簡単で、クラスに@dataclassを付け、フィールドを型ヒント付きで宣言するだけです。__init__を書かなくても、フィールドを引数に取るコンストラクタが自動で作られます。

@dataclass の基本
from dataclasses import dataclass

@dataclass
class Point:
    x: int
    y: int = 0      # デフォルト値も指定できる

# __init__ を書いていないのに、引数を取れる
p = Point(10, 20)
print(p.x, p.y)     # 10 20

# デフォルト値が使われる
print(Point(5))     # Point(x=5, y=0)

実機でも、__init__を書いていないのにPoint(10, 20)でインスタンスが作れ、p.x=10、p.y=20になりました。y: int = 0のようにデフォルト値も指定できます。通常のクラスならdef __init__(self, x, y=0): self.x = x; self.y = yと書くところを、dataclassはフィールドの宣言だけで済ませてくれます。

自動生成される__repr__と__eq__

dataclass__init__だけでなく、見やすい表示の__repr__と、値で比較する__eq__も自動生成します。これらは通常のクラスでは自分で書く必要があるものです。

__repr__ と __eq__ が自動
from dataclasses import dataclass

@dataclass
class Point:
    x: int
    y: int = 0

# __repr__: 中身が分かりやすく表示される
print(Point(10, 20))          # Point(x=10, y=20)

# __eq__: 中身が同じなら等しいと判定される
print(Point(1, 2) == Point(1, 2))   # True
print(Point(1, 2) == Point(1, 3))   # False
表示と比較が最初から使える

実機で確認したところ、print(Point(10, 20))Point(x=10, y=20)中身が分かりやすく表示され、Point(1, 2) == Point(1, 2)Trueになりました。通常のクラスでは、printすると<__main__.Point object at 0x...>のような分かりにくい表示になり、==もデフォルトでは「同じオブジェクトか」しか見ません(中身が同じでもFalse)。dataclassは、中身を見やすく表示する__repr__と、フィールドの値で等しさを判定する__eq__を自動で用意してくれるため、デバッグや比較がそのまま行えます。これだけでも、データを持つクラスをdataclassにする価値があります。

可変デフォルトはdefault_factory

注意が必要なのが、リストや辞書を初期値にする場合です。items: list = []のように直接書くと、エラーになります。これを避けるため、field(default_factory=...)を使います。

可変デフォルトの正しい書き方
from dataclasses import dataclass, field

# NG: リストを直接デフォルトにするとエラー
# @dataclass
# class Bad:
#     items: list = []      # ValueError: mutable default ...

# OK: field(default_factory=list) を使う
@dataclass
class Cart:
    items: list = field(default_factory=list)

a = Cart()
b = Cart()
a.items.append("りんご")
print(a.items)   # ['りんご']
print(b.items)   # [](a と b で別のリスト)
リスト・辞書の初期値はfield(default_factory)で

実機で確認したところ、items: list = []と直接書いたdataclassを定義しようとすると、ValueError: mutable default <class 'list'> for field itemsというエラーで定義そのものが失敗しました。これは、もし許してしまうとすべてのインスタンスが同じ1つのリストを共有してしまうためで、Pythonがあえてエラーにして防いでいます(関数の可変デフォルト引数クラス属性と同じ落とし穴です)。正しくはfield(default_factory=list)を使います。実機でも、これによりabそれぞれ別のリストを持ちaに追加してもbは空のままでした。リスト・辞書・集合などの初期値は、必ずfield(default_factory=...)で指定してください。default_factoryには、初期値を作る関数(listdictなど)を渡します。

frozen=Trueでイミュータブル

@dataclass(frozen=True)とすると、作成後に値を変更できない(イミュータブル)クラスになります。誤って書き換えられたくない設定値などに使います。

frozen で変更不可にする
from dataclasses import dataclass

@dataclass(frozen=True)
class Config:
    host: str
    port: int

c = Config("localhost", 8080)
print(c.host)        # localhost

# 値を変更しようとするとエラー
# c.port = 9000      # FrozenInstanceError

実機でも、frozen=Trueのデータクラスで作ったインスタンスの値を変更しようとすると、FrozenInstanceErrorになりました。frozen=Trueは「作ったら変更しない」データに向いていて、うっかり書き換えるミスを防げます。また、変更できないオブジェクトは辞書のキーや集合の要素にも使える(ハッシュ可能になる)という利点もあります。設定値・座標・定数的なデータなど、変わってほしくない値をまとめるときに便利です。

asdict・フィールドの順番

asdict()を使うと、データクラスを辞書に変換できます。JSONにしたいときなどに便利です。また、フィールドの順番には「デフォルト値なし」を「デフォルト値あり」より前に書くというルールがあります。

asdict と フィールドの順番
from dataclasses import dataclass, asdict

@dataclass
class Point:
    x: int
    y: int = 0

# 辞書に変換(JSON化などに便利)
print(asdict(Point(1, 2)))   # {'x': 1, 'y': 2}

# NG: デフォルトありの後にデフォルトなしは書けない
# @dataclass
# class Bad:
#     x: int = 0
#     y: int          # TypeError: 非デフォルトがデフォルトの後

実機でも、asdict(Point(1, 2)){'x': 1, 'y': 2}という辞書を返しました。JSONとして保存・送信したいときなどに役立ちます。また、フィールドの順番には注意が必要で、デフォルト値のあるフィールドより後に、デフォルト値のないフィールドを置くことはできません(通常の関数の引数と同じルールです)。デフォルトなしのフィールドを先に、デフォルトありを後に書きます。

主なポイントまとめ

dataclassの要点をまとめます。

項目 ポイント
定義 @dataclass+フィールドを型ヒント付きで
自動生成 __init____repr____eq__
デフォルト値 y: int = 0
可変デフォルト field(default_factory=list)(必須)
変更不可 @dataclass(frozen=True)
辞書化 asdict(インスタンス)

よくある失敗

リストを直接デフォルトにする

items: list = []はエラーです。field(default_factory=list)を使います。

__init__を自分で書いてしまう

dataclassが自動生成します。フィールドを宣言するだけで十分です。

デフォルトありの後にデフォルトなし

順番のルール違反でエラーです。デフォルトなしを先に書きます。

frozenなのに値を変更する

frozen=Trueは変更不可です。変更したいならfrozenを外します。

importを忘れる

from dataclasses import dataclass, fieldのように読み込みます。

よくある質問

Qdataclassは普通のクラスと何が違いますか?
A@dataclassを付けると、__init____repr____eq__が自動で生成されます。通常のクラスではこれらを自分で書く必要がありますが、dataclassではフィールドを型ヒント付きで宣言するだけで済みます。値をまとめて持つ用途のクラスを、短く書けます。
Qリストを初期値にするとエラーになります。
Aitems: list = []のように直接書くと、すべてのインスタンスでリストを共有してしまうため、Pythonがエラーにします。field(default_factory=list)を使ってください。これにより、各インスタンスが別々のリストを持つようになります。
Qdataclassを辞書に変換するには?
Afrom dataclasses import asdictとしてasdict(インスタンス)を使います。フィールド名をキー、値を値とした辞書が得られ、JSONとして保存・送信したいときなどに便利です。
Q値を変更できないdataclassを作るには?
A@dataclass(frozen=True)とします。作成後に値を変更しようとするとFrozenInstanceErrorになります。設定値や定数的なデータなど、変わってほしくない値をまとめるのに向いています。辞書のキーや集合の要素にも使えるようになります。
Qフィールドの順番に決まりはありますか?
Aデフォルト値のあるフィールドより後に、デフォルト値のないフィールドを置くことはできません。これは通常の関数の引数と同じルールです。デフォルトなしのフィールドを先に、デフォルトありを後に書いてください。

まとめ

  • @dataclass__init____repr____eq__が自動生成されます。
  • フィールドは型ヒント付きで宣言。デフォルト値も指定できます。
  • リスト・辞書の初期値はfield(default_factory=list)(直接書くとエラー)。
  • @dataclass(frozen=True)変更不可のクラスになります。
  • asdict()で辞書に変換でき、JSON化などに便利です。

dataclassは、データを持つクラスの定型コードを大幅に減らせる、モダンなPythonの便利な機能です。「可変デフォルトはfield(default_factory)」という1点さえ押さえれば、設定・座標・レコードなど、さまざまなデータを安全・簡潔に表現できます。通常のクラスで__init__を書いていた場面は、ぜひdataclassに置き換えてみてください。