Pythonでクラスを書いていて、同じような行を何度も打っている気がしたことはありませんか。
self.title = title、self.price = price。同じ名前を2回ずつ書く、あの作業です。
フィールドが5つあれば5行、10個あれば10行。書き写すだけの単純作業なのに、打ち間違えるとバグになります。
その手間をまるごと肩代わりしてくれるのが、Python標準のdataclassです。読み方はそのまま、データクラスといいます。
デコレータを1行つけるだけで、面倒な部分が自動で用意されます。この記事では、dataclassが何を自動化してくれるのか、どんな場面で使うと効くのかを、実際に動かしたコードを見ながら解説していきます。
dataclassとは何なのか¶
まずは正体をはっきりさせましょう。dataclassは、データを持ち運ぶためのクラスを短く書くための仕組みです。
Python 3.7から、標準ライブラリのdataclassesモジュールとして入りました。もとになった提案書はPEP 557です。
公式ドキュメントでは、init()や__repr__()といった特殊メソッドを自動で追加するデコレータと関数を提供するもの、と説明されています。
特殊メソッドという言葉が出てきました。アンダースコア2つで囲まれた、Pythonが裏側で呼び出す決まった名前のメソッドのことです。
【関連記事】Pythonの特殊メソッドとは?どんな種類があるのか?どうやって使うのか?
追加のインストールは要りません。Python 3.7以降を使っているなら、今すぐimportして試せます。
dataclassを使わないとどうなるか¶
ありがたみは、使わない場合と並べるといちばんよくわかります。本のタイトルと価格を持つクラスを、まずは普通に書いてみましょう。
class Book:
def __init__(self, title, price):
self.title = title
self.price = price
book = Book("Python入門", 2800)
print(book)
実行すると、次のような表示になります。
<__main__.Book object at 0x7f288b5c6050>
中身がまったく見えません。デバッグでprintしても、メモリ上の場所しか教えてくれないわけです。
さらに困るのが比較です。同じ内容のBookを2つ作って==で比べても、結果はFalseになります。
これを直すには、__repr__と__eq__を自分で書く必要があります。フィールドが1つ増えるたびに、3か所を直して回ることになるわけです。
私は10年ほどエンジニアとして開発に関わってきましたが、この直し忘れが原因の不具合を何度も見てきました。フィールドを追加したのに__eq__の更新だけ漏れていて、本来は違うはずのデータが同じものとして扱われてしまうのです。
dataclassで書き直してみる¶
では、同じクラスをdataclassで書いてみます。変わるのは見た目だけではありません。
from dataclasses import dataclass
@dataclass
class Book:
title: str
price: int
book = Book("Python入門", 2800)
print(book)
print(Book("A", 1) == Book("A", 1))
実行結果はこうなりました。
Book(title='Python入門', price=2800)
True
printすると中身がそのまま出ています。同じ内容どうしの比較もTrueになりました。
書いたのは、フィールドの名前と型だけです。__init__も__repr__も__eq__も、デコレータが裏側で作ってくれました。
ここで使っているstrやintは型ヒントと呼ばれる書き方です。dataclassはこの記述を目印にしてフィールドを見つけるため、型ヒントは省略できません。
【関連記事】Pythonの型ヒントとは?型ヒントの基礎を解説
フィールドに初期値をつける¶
初期値を持たせたいときは、=でつなぐだけです。引数を省略したときに、その値が使われます。
@dataclass
class Book:
title: str
price: int = 0
ひとつだけ注意があります。初期値を持つフィールドは、持たないフィールドより後ろに置かなければいけません。
可変な初期値はdefault_factoryで¶
リストや辞書を初期値にしたいときは、少し事情が変わります。素直に書くとエラーになるのです。
@dataclass
class Book:
title: str
tags: list = []
これを実行すると、次のメッセージが表示されます。
ValueError: mutable default <class 'list'> for field tags is not allowed: use default_factory
親切にも、解決方法まで書いてくれています。field関数のdefault_factoryを使いなさい、という指示です。
from dataclasses import dataclass, field
@dataclass
class Book:
title: str
tags: list = field(default_factory=list)
なぜこんな決まりがあるのでしょうか。初期値をそのまま書いてしまうと、その1つのリストをすべてのインスタンスで共有することになるからです。
1冊目に追加したタグが、なぜか2冊目にも増えている。そんな気味の悪い挙動になります。
default_factoryを使えば、インスタンスを作るたびに新しいリストが用意されます。危ない書き方を最初からエラーで止めてくれるのは、むしろ親切な設計だと思いませんか。
何が自動で作られるのかを整理する¶
自動といっても、何でも作ってくれるわけではありません。どこまでが自動なのかを表で確認しておきましょう。
| 生成されるもの | 既定の動き | 役割 |
|---|---|---|
__init__ |
作られる | フィールドを受け取って代入する |
__repr__ |
作られる | 中身が見える表示にする |
__eq__ |
作られる | 全フィールドの一致で等しさを判定する |
__lt__ などの比較 |
作られない | order=True を指定したときだけ追加される |
__hash__ |
作られない | frozen=True などの条件で追加される |
上の3つは、デコレータをつけただけで手に入ります。下の2つは、オプションで明示的に指示したときだけです。
よく使うオプションを押さえる¶
@dataclassには、かっこの中に書けるオプションがいくつか用意されています。ここでは使用頻度の高い2つを取り上げます。
frozenで書き換えを禁止する¶
frozen=Trueをつけると、作ったあとに値を変えられないクラスになります。
from dataclasses import dataclass
@dataclass(frozen=True)
class Point:
x: int
y: int
p = Point(1, 2)
p.x = 5
最後の行を実行した時点で、エラーになります。
dataclasses.FrozenInstanceError: cannot assign to field 'x'
座標や設定値のように、途中で変わってほしくないデータに向いています。うっかり書き換えを、実行時にはじいてくれるわけです。
おまけの効果もあります。frozenにするとハッシュ値が使えるようになり、辞書のキーや集合の要素として扱えるようになります。
orderで並べ替えられるようにする¶
order=Trueをつけると、大小を比べられるようになります。フィールドを上から順に見ていって判定する決まりです。
from dataclasses import dataclass
@dataclass(order=True)
class Score:
point: int
name: str
scores = [Score(70, "b"), Score(30, "a"), Score(90, "c")]
print(sorted(scores))
結果はpointの小さい順に並びます。sortedにkeyを渡さなくても済むのが、地味に気持ちのいいところです。
なお、Python 3.10からはslots=Trueというオプションも使えます。メモリ使用量を抑える__slots__を自動で用意してくれる指定です。
【関連記事】__slots__でメモリ使用量を劇的に削減する裏技テクニック
実務で効いてくる小技を2つ¶
基本を押さえたところで、現場でよく使う書き方も紹介します。どちらも一度知ると手放せなくなります。
ひとつ目は__post_init__です。__init__のあとに自動で呼ばれるメソッドで、他のフィールドから計算した値を持たせたいときに使います。
from dataclasses import dataclass
@dataclass
class Item:
price: int
tax_included: int = 0
def __post_init__(self):
self.tax_included = int(self.price * 1.1)
print(Item(1000))
表示はItem(price=1000, tax_included=1100)となります。税込価格を渡していないのに、勝手に計算されました。
ふたつ目はfield関数の細かい指定です。表示や比較の対象から、特定のフィールドだけを外せます。
from dataclasses import dataclass, field
@dataclass
class User:
name: str
password: str = field(repr=False, compare=False)
print(User("taro", "secret"))
出力はUser(name='taro')だけになります。パスワードがログに残ってしまう事故を、この1行で防げるわけです。
秘密の値がログにそのまま出ているのを見つけて冷や汗をかいた経験は、私にもあります。最初から表示されない設定にしておけば、あとから慌てずに済みます。
dictやPydanticとどう使い分けるか¶
似た役割の道具は、他にもいくつかあります。迷ったときの目安を表にまとめました。
| 道具 | 向いている場面 | 注意点 |
|---|---|---|
| dict | 形が決まらない一時的なデータ | キーの打ち間違いに気づけない |
| dataclass | 形が決まった自作のデータ | 値の中身までは検査しない |
| NamedTuple | 変更しない小さなデータの組 | あとから値を変えられない |
| Pydantic | 外部から入ってくるデータの受け口 | 追加のインストールが必要 |
ここでいちばん誤解されやすいのが、dataclassの注意点の行です。型ヒントを書いても、実行時に型が検査されるわけではありません。
Book(title=123, price="高い")と書いてもエラーは出ず、そのまま通ってしまいます。型ヒントはあくまで、人と検査ツールのための情報だからです。
外部のAPIやフォームから来るデータのように、中身が信用できない場面ではPydanticのほうが向いています。
【関連記事】Pydantic v2で堅牢なデータバリデーションをやってみよう!
自分のプログラムの中でデータを持ち運ぶだけなら、標準のdataclassで十分です。
まとめ¶
dataclassについて、ここまで見てきた内容を振り返ります。
@dataclassを1行つけるだけで、__init__と__repr__と__eq__が自動で用意されます。書く量が減るだけでなく、フィールドを増やしたときの直し忘れも起きなくなります。
リストや辞書を初期値にするときはdefault_factory、書き換えを防ぎたいときはfrozen=True。この2つを覚えておけば、たいていの場面で困りません。
一方で、値の中身までは検査してくれない点だけは忘れないでください。信用できないデータを受け取る場所では、別の道具を選ぶ判断が要ります。
短く書けるようになると、クラスを作ることへの心理的なハードルが下がります。辞書で済ませていた場所を小さなクラスに置き換えるだけでも、コードはぐっと読みやすくなるはずです。
次にデータをまとめる場面が来たら、ぜひ@dataclassを思い出してみてください。
ここまでお読みいただきありがとうございました。