ライブラリ: dataclasses

今回は Python 3.7 で標準ライブラリに追加された dataclasses について見ていきたいと思います。

import dataclasses

目次

  • dataclasses とは
  • dataclasses の使い方
  • dataclasses の使いどころ

dataclasses とは

dataclasses は、独自のクラスを定義するときに定型的な特殊メソッド( special methods )( __init__() 等)の記述を省略できる機能を提供するモジュールです。 Python 3.7 から標準ライブラリとして Python 本体に同梱されるようになりました。

代表的な機能は次の 2 つです。

  • dataclasses.dataclass: クラスのデコレータ。対象のクラスのクラス変数をもとに特殊メソッドを追加する。
  • dataclasses.field: クラス変数に使う。 dataclass とあわせて使うことでアトリビュートごとの挙動を細かく設定できる。

dataclasses の使い方

まずインストールについてですが、 dataclasses は Python 3.7 以降に同梱されているためインストールする必要はありません。

代表的な機能である次の 2 つから見ていきましょう。

  • dataclasses.dataclass
  • dataclasses.field

dataclasses.dataclass

dataclass はクラスに対して使うデコレータです。

dataclass でデコレートされたクラスには、 __init__()__repr__()__eq__() の 3 つの特殊メソッドが自動的に追加されます。

from dataclasses import dataclass

TAX_RATE = 0.1

@dataclass
class Book:
    title: str
    isbn13: str
    price: int

    def price_with_tax(self):
        return int(self.price * (1 + TAX_RATE))

# __init__() が自動的に定義されている:
book1 = Book('小僧の神様・城の崎にて', '978-4101030050', 562)

# クラス変数と同名のアトリビュートが利用できる:
print(book1.title)
# => '小僧の神様・城の崎にて'
print(book1.isbn13)
# => '978-4101030050'
print(book1.price)
# => 562
print(book1.price_with_tax())
# => 618

# __repr__() も自動的に定義されている:
print(repr(book1))
# => Book(title='小僧の神様・城の崎にて', isbn13='978-4101030050', price=562)

# __repr__() の出力は eval() すると新しいインスタンスが作れる形になっている:
book2 = eval(repr(book1))

# __eq__() も自動的に定義されている:
print(book1 == book2)
# => True

dataclass の最もシンプルな使い方は上のサンプルのように @dataclass という形でデコレートするものですが、 @dataclass にはオプションを渡すこともできます。オプションでは、特殊メソッドの追加の有無等を指定することができます。

from dataclasses import dataclass

@dataclass(repr=False, eq=False)
class Book:
    title: str
    isbn13: str
    price: int

# __init__() が自動的に定義されている:
book1 = Book('暗夜行路', '978-4101030074', 961)

# repr=False により __repr__() は定義されていないので object のデフォルトの出力になる:
print(repr(book1))
# => <__main__.Book object at 0x106fa94a8>

book2 = Book('暗夜行路', '978-4101030074', 961)

# eq=False により __eq__() は自動追加されていないので、デフォルトの object.__eq__() で判定される:
print(book1 == book2)
# => False

ちなみに、 dataclass デコレータが @dataclass でも @dataclass() でも同じような使い方ができるのは、 dataclass がそのように(シンプルに使えるように)工夫して作られているからであり、一般のデコレータはこのようには行きません。

dataclass デコレータがアトリビュート化する要素として認識するのは タイプアノテーションのあるクラス変数だけ です。タイプアノテーションの無いクラス変数は無視されるので、そこは注意が必要です。次のコードではタイプアノテーションの無い isbn13 は自動追加されません。

from dataclasses import dataclass

@dataclass
class Book:
    title: str
    isbn13 = ''
    price: int

# isbn13 は dataclass には無視される:
book1 = Book('和解', 464)
print(repr(book1))
# => Book(title='和解', price=464)

また、 dataclass は「タイプアノテーションがあるかどうか」という点だけを見ており、具体的な型が何なのかは見ていないという点もポイントです。つまり、 int と定義されている変数に str 型の値を代入しても何も問題ありません(バリデーション等は特に追加されません)。

# 次のコードも特に問題なし
book1 = Book('和解', '464')

尚、 Python 3.7 における dataclass の宣言部は次のようになっています。

@dataclasses.dataclass(
    *,
    init=True,
    repr=True,
    eq=True,
    order=False,
    unsafe_hash=False,
    frozen=False)

引数がすべて *, の後に並んでいるので、引数はすべてキーワードを明示的に指定した渡し方をする必要があります。

dataclassesdataclass でデコレートされたクラスとそのインスタンスに対して使える関数をいくつか用意しています。次のコードでは fields()asdict()astuple() の 3 つを試しています。

from dataclasses import dataclass, fields, asdict, astuple

@dataclass
class Book:
    title: str
    isbn13: str
    price: int

# fields() 関数はそのクラスあるいはインスタンスのフィールドの一覧をタプルで返す:
# タプルの各要素は dataclasses.Field クラスのインスタンス
print(type(fields(Book)))
print([x.name for x in fields(Book)])
# => ['title', 'isbn13', 'price']

book1 = Book('小僧の神様・城の崎にて', '978-4101030050', 562)
print([x.name for x in fields(book1)])
# => ['title', 'isbn13', 'price']

# asdict() 関数は辞書を生成する
print(asdict(book1))
# => {'title': '小僧の神様・城の崎にて', 'isbn13': '978-4101030050', 'price': 562}

# astuple() 関数は tuple を生成する
print(astuple(book1))
# => ('小僧の神様・城の崎にて', '978-4101030050', 562)

dataclasses のアイデアの概要は以上でおおよそ押さえられたかと思いますが、実際に利用する場合は詳細を確認してから使うようにしてください。

つづいて field を見てみましょう。

dataclasses.field

fielddataclass でデコレートされたクラスのアトリビュートの挙動を細かく指定するためのものです。

例えば、アトリビュートのデフォルト値に list を指定したいときには field が次のように使えます。

from dataclasses import dataclass, field
from typing import List

@dataclass
class Film:
    title: str = ''
    cast: List[str] = field(default_factory=list)

f1 = Film('ジュラシック・ワールド', ['クリス・プラット', 'ブライス・ダラス・ハワード'])
print(repr(f1))
# => Film(title='ジュラシック・ワールド', cast=['クリス・プラット', 'ブライス・ダラス・ハワード'])

f2 = Film('ジュラシック・ワールド 炎の王国', ['クリス・プラット', 'ブライス・ダラス・ハワード'])
print(repr(f2))
# => Film(title='ジュラシック・ワールド 炎の王国', cast=['クリス・プラット', 'ブライス・ダラス・ハワード'])

また、次のように書くと、一部の要素( subtitle )を __eq__() での比較の対象外にすることができます。

from dataclasses import dataclass, field

@dataclass
class Film:
    title: str
    subtitle: str = field(compare=False)

f1 = Film('007', 'ワールド・イズ・ノット・イナフ')
f2 = Film('007', '慰めの報酬')
f3 = Film('007', 'スカイフォール')

# __eq__() には subtitle が使われないので、 == 演算で title のみが比較された結果、 True が返る
print(f1 == f2 == f3)
# => True

field を使うとその他さまざまなカスタマイズを行うことができます。 Python 3.7 における field の宣言部は次のようになっているので、実際に利用するときは事前に詳細を確認するようにしてください。

dataclasses.field(
    *,
    default=MISSING,
    default_factory=MISSING,
    repr=True,
    hash=None,
    init=True,
    compare=True,
    metadata=None)

かんたんにですが、以上 dataclasses の基本的な使い方についてでした。

最後に使いどころについて。

dataclasses の使いどころ

私の認識では dataclassesnamedtuple の進化版的な位置づけのものです。

使い捨てのスクリプト作成等の場合は、単純に特殊メソッドの作成を省略したいときに気軽に使うとよさそうです。規模が大きめの開発では、アーキテクチャの境界をまたぐ情報の受け渡しに使用するシンプルなデータ構造やプレーンなドメインモデルクラス等で使うと便利かと思います。

dataclass デコレータが追加してくれる特殊メソッドのうち、 __init__()__eq__() は(自分で定義したいので)不要な場合も多そうですが、 __repr__() は多くの場合そのまま使えて便利なのではないでしょうか。

dataclasses の機能を濫用すると「 Explicit is better than implicit. 」に反するコードになりそうな気もしますが、今後 dataclasses がクラス定義における定番と言われるくらいよく使われるようになれば、( Python に不慣れな人にはわかりづらいけれど)ある程度経験のある Pythonista には implicit というほどのものではなくなってくる、のかも、しれません。

というわけで、 Python 3.7 で導入された dataclasses の説明でした。

同じく「特殊メソッドの自動定義」で人気のライブラリ attrs との違いや dataclasses が標準ライブラリに追加された経緯等について興味のある方には以下のページ等が参考になるかと思います。

関連記事

参考