Python Tips: dataclasses.dataclass で使える引数まとめ

Python に同梱のライブラリ dataclasses が提供するデコレータ dataclass の引数についてまとめました。

dataclasses って何だっけという方は以下のページなどでご確認ください。

確認時のバージョン

  • Python 3.10

利用可能な引数

Python 3.10 時点で dataclass() で利用可能な引数は次のとおりです。

@dataclasses.dataclass(*, init=True, repr=True, eq=True, order=False, unsafe_hash=False, frozen=False, match_args=True, kw_only=False, slots=False)
引数 導入バージョン デフォルト値 説明
init 3.7 True __init__() の自動生成を行う
repr 3.7 True __repr__() の自動生成を行う
eq 3.7 True __eq__() の自動生成を行う
order 3.7 False __lt__() __le__() __gt__() __ge__() の自動生成を行う
unsafe_hash 3.7 False __hash__() の自動生成を強制的に行う
frozen 3.7 False フィールドへの再代入をできなくする
match_args 3.10 True __match_args__ の自動生成を行う
kw_only 3.10 False __init__() の引数をキーワード専用引数にする
slots 3.10 False __slots__ の自動生成を行う

dataclasses 自体が追加された 3.7 のときからあったものと 3.10 で追加されたものに分かれます。 以下上から順に見ていきます。

init

True の場合は __init__() メソッドの自動生成を行う。 False の場合は行わない。 ただし、 True の場合でも __init__() が定義されていれば自動生成は行わない。

True:

@dataclass(init=True)
class Product:
    id: str
    name: str


# `id` `name` を引数として受け取る `__init__()` が定義されている:
p1 = Product(id='sample', name='サンプル')
assert p1.id == 'sample'
assert p1.name == 'サンプル'

False:

@dataclass(init=False)
class Product:
    id: str
    name: str


# `id` `name` を受け付ける `__init__()` メソッドが定義されてないのでエラーになる
p1 = Product(id='sample', name='サンプル')
# => TypeError: Product() takes no arguments

repr

True の場合は __repr__() メソッドの自動生成を行う。 False の場合は行わない。 ただし、 True の場合でも __repr__() が定義されていれば自動生成は行わない。

True:

@dataclass(repr=True)
class LineItem:
    product_id: str
    quantity: int
    subtotal: int


# クラス名と一連のフィールドからなる repr 文字列が出力される:
li1 = LineItem(product_id='sample', quantity=3, subtotal=300)
f'明細: {li1!r}'
# => "明細: LineItem(product_id='sample', quantity=3, subtotal=300)"

False:

@dataclass(repr=False)
class LineItem:
    product_id: str
    quantity: int
    subtotal: int

# `object` のデフォルトの repr 文字列が出力される:
li1 = LineItem(product_id='sample', quantity=3, subtotal=300)
f'明細: {li1!r}'
# => "明細: <__main__.LineItem object at 0x10783bac0>"

eq

True の場合は __eq__() メソッドの自動生成を行う。 False の場合は行わない。 ただし、 True の場合でも __eq__() が定義されていれば自動生成は行わない。

True:

@dataclass(eq=True)
class Position:
    x: int
    y: int


# アトリビュートの値で同値判定が行える:
p1, p2 = Position(x=5, y=7), Position(x=5, y=7)
assert p1 == p2

False:

@dataclass(eq=False)
class Position:
    x: int
    y: int


# アトリビュートの値で同値判定が行えない:
# (この場合はベースクラスの `object` の `__eq__()` を利用するのでオブジェクト ID での比較になる)
p1, p2 = Position(x=5, y=7), Position(x=5, y=7)
assert p1 == p2
# => AssertionError

order

True の場合は __lt__() __le__() __gt__() __ge__() の自動生成を行う。 False の場合は行わない。 ただし、 True の場合に __lt__() __le__() __gt__() __ge__() のいずれかが定義されていれば TypeError があがる。

True:

@dataclass(order=True)
class Version:
    major: int
    minor: int
    patch: int


# 大小比較ができる:
ver_x = Version(3, 10, 12)
ver_y = Version(3, 9, 18)
assert ver_x > ver_y

False:

@dataclass(order=False)
class Version:
    major: int
    minor: int
    patch: int


# 大小比較ができない:
ver_x = Version(3, 10, 12)
ver_y = Version(3, 9, 18)
assert ver_x > ver_y
# => TypeError: '>' not supported between instances of 'Version' and 'Version'

unsafe_hash

False の場合は、 eqfrozen がともに True でなおかつ __hash__() が未定義の場合のみ __hash__() の自動生成が行われる。 True の場合は、 eqfrozen の値によらず __hash__() が未定義なら __hash__() の自動生成が行われる。

True:

@dataclass(unsafe_hash=True)
class Product:
   brand_id: str
   sku_id: str


# `eq` と `frozen` がともに `True` でなくても `__hash__()` が追加される:
p1 = Product('brand a', 'sku x')
products = {p1}

False:

@dataclass(unsafe_hash=False)
class Product:
    brand_id: str
    sku_id: str


# デフォルトでは `frozen=False` なので `__hash__()` が追加されない:
p1 = Product('brand a', 'sku x')
products = {p1}
# => TypeError: unhashable type: 'Product'

ちなみに、 eq=Falseunsafe_hash=False の場合は __hash__() の自動生成は行われませんが、このときはベースクラスの __hash__() が使われるのでそのクラスのオブジェクトは hashable になります。 そのため、 __hash__() の自動生成を行わない設定にしたらそのオブジェクトが unhashable になるというわけではありません。

このあたりの仕組みは少し複雑なので、 unsafe_hash=True を利用するときはドキュメントと dataclasses のソース内のインラインコメントをよく読んで仕組みを正しく理解してから使うのがよいと思います。

frozen

True の場合はインスタンス生成後にフィールドへの再代入ができなくなる。 False の場合はフィールドへの再代入ができる。

True:

@dataclass(frozen=True)
class Coordinate:
    lon: float
    lat: float


# フィールドへの再代入が行えない:
c1 = Coordinate(lon=134.998, lat=34.643)
c1.lon = -119.5
# => FrozenInstanceError: cannot assign to field 'lon'

False:

@dataclass(frozen=False)
class Coordinate:
    lon: float
    lat: float


# フィールドへの再代入が行える:
c1 = Coordinate(lon=134.998, lat=34.643)
c1.lon = -119.5

frozen=True はいわゆる immutable なオブジェクトを作りたいときに便利です。

match_args

True の場合はクラス変数 __match_args__ の自動生成が行われる。 False の場合は __match_args__ の自動生成が行われない。

__match_args__ は Python 3.10 で導入された match 文でのサブパターンマッチに使われる。

True:

@dataclass(match_args=True)
class Rectangle:
    width: int
    height: int


shape1 = Rectangle(width=4, height=3)

# `match ~ case` 文で引数のサブパターンマッチングが行われる:
match shape1:
    case Rectangle(w, h):
        print(f'width: {w} / height: {h}')
    case _:
        pass
# => 'width: 4 / height: 3'

False:

@dataclass(match_args=False)
class Rectangle:
    width: int
    height: int


shape1 = Rectangle(width=4, height=3)

# `match ~ case` 文で引数のサブパターンマッチングが行われない:
match shape1:
    case Rectangle(w, h):
        print(f'width: {w} / height: {h}')
    case _:
        pass
# => TypeError: Rectangle() accepts 0 positional sub-patterns (2 given)

参考: Python 公式ドキュメントや PEP の __match_args__ の説明箇所:

kw_only

True の場合は、生成される __init__() においてすべての引数がキーワード専用引数( keyword-only )になる。 False の場合は、通常の引数になる。

True:

@dataclass(kw_only=True)
class Person:
    name: str


# キーワード引数は OK
p1 = Person(name='太郎')

# 位置引数は NG
p2 = Person('次郎')
# => TypeError: Person.__init__() takes 1 positional argument but 2 were given

False:

@dataclass(kw_only=False)
class Person:
    name: str


# キーワード引数・位置引数ともに OK
p1 = Person(name='太郎')
p2 = Person('次郎')

dataclass におけるキーワード専用引数の指定方法として他にも dataclasses.KW_ONLY があるので、興味がある方はそちらもチェックしておくとよいと思います。

slots

True の場合は __slots__ アトリビュートの自動生成が行われる。 False の場合は __slots__ の自動生成が行われない。 ただし、 True の場合に __slots__ が定義されていれば TypeError があがる。

True:

import sys

@dataclass(slots=True)
class Page:
    title: str
    path: str

# 定義されたフィールド以外のアトリビュートを追加できない:
p1 = Page(title='サイトについて', path='/about')
p1.body = 'このサイトは…'
# => AttributeError: 'Page' object has no attribute 'body'

# メモリ使用量が比較的小さい:
sys.getsizeof(Page(title='', path='').__slots__)
# => 56

False:

import sys

@dataclass(slots=False)
class Page:
    title: str
    path: str

# 定義されたフィールド以外のアトリビュートを動的に追加できる:
p1 = Page(title='サイトについて', path='/about')
p1.body = 'このサイトは…'
assert p1.body == 'このサイトは…'

# メモリ使用量が比較的大きい:
sys.getsizeof(Page(title='', path='').__dict__)
# => 104

__slots__ については Python 公式ドキュメントの Data model のページで説明されているので興味のある方はチェックしてみてください。

関連記事