Keep on moving

あんまりまとまってないことを書きますよ

attrsの使いどころとdataclass

masahito.hatenablog.com

この記事は^の記事の続きです。^を読んでから読むと良いと思います。 なお、今回のサンプルコードはこちらにあります。(間違いなどありましたらご指摘ください)

github.com

TL;DR

  • attrsのつかいどころ
    • 今Python3.5 or 3.6を使っている。今後dataclassを使いたい
    • dataclassで slotsなどを使いたい場合にボイラープレートコードを避けたい
  • slotsの効果

attrsとは

www.attrs.org 簡単にいうとPython でclassを書くときのボイラープレートになるところがスッキリかけるものです。 1 Overviewで書かれているコードを軽く解説します。

>>> import attr

>>> @attr.s
... class SomeClass(object):
...     a_number = attr.ib(default=42)
...     list_of_numbers = attr.ib(factory=list)
...
...     def hard_math(self, another_number):
...         return self.a_number + sum(self.list_of_numbers) * another_number


>>> sc = SomeClass(1, [1, 2, 3])
>>> sc
SomeClass(a_number=1, list_of_numbers=[1, 2, 3])

>>> sc.hard_math(3)
19
>>> sc == SomeClass(1, [1, 2, 3])
True
>>> sc != SomeClass(2, [3, 2, 1]) # classレベルでの比較が可能です
True

>>> attr.asdict(sc) # class -> dict 変換
{'a_number': 1, 'list_of_numbers': [1, 2, 3]}

>>> SomeClass() # default値も使えます. factory=list の場合はempty listになります。
SomeClass(a_number=42, list_of_numbers=[])

>>> C = attr.make_class("C", ["a", "b"]) # methodでクラスがつくれちゃいます。
>>> C("foo", "bar")
C(a='foo', b='bar')

attrsこぼれ話

じつはこのライブラリ Python 3.7 で入った dataclass のもとになった実装だったりします。 詳しくはpepを確認してください。 www.python.org

ちなみに dataclass入れるよりattrsをPythonで標準でいれちゃえば? という声もあったみたいですがそれはやめたみたいです。(guidoがstdlibに入れるのに反対している、この辺を参照。)

github.com 「attrsはいいもんだが、これがファイナルアンサーじゃないしいいところだけ取り出そうぜ」という感じっぽい。

attrsのdataclassとの使い分け

attrsを使うべき

  • 今Python3.5 or 3.6を使っている. 2
    • 特に現在namedtupleを使っているが、3.7にupgradeするタイミングでdataclassの使用を考えている
  • dataclassで slotsなどをつかいたい(for Python3.7以降)
@dataclasses.dataclass
class B:
   __slots__ = ('b',) # __slots__ 追加
   b: int

例えばこんな感じでdataclassでも __slots__ が使えることを前回紹介した。が、 __slots__ に設定するところとプロパティを両方書かないといけないため 実行時にエラーになることが結構おおい。

これをattrsをつかうと以下のようにかける

# slots=Trueをつけるとslotsが有効に
@attr.s(auto_attribs=True, slots=True)
class AttrsMessage:
    sender: str
    recipient: str
    body: str

個人的にはdataclassを使いたくなったら無条件でattrsを使うのをおすすめしたいと思っています。 ただプロジェクトの考え方でなるべく使うライブラリを増やしたくない場合はdataclassでも問題はないかもしれません。 が、複数人で開発するときではミスをするばあいが多いため入れておいたほうが便利だと思います。

余談 __slots__ の効用

Pympler を使ってPython3.8での __slots__ を使ったとき使わないときのメモリ使用量を比べてみました。 使い方についてはこちらが詳しいです。 Asizeof モジュールの使い方 | Xoxzoの公式ブログ

# use NamedTuple
Message = NamedTuple("Message", [("sender", str), ("recipient", str), ("body", str)])

if sys.version_info.minor >=7:
    import dataclasses
    @dataclasses.dataclass
    class DataClassMessage:
        sender: str
        recipient: str
        body: str


    @dataclasses.dataclass
    class SlotMessage:
        __slots__ = ["sender", "recipient", "body"]
        sender: str
        recipient: str
        body: str


@attr.s(auto_attribs=True, slots=True, weakref_slot=False)
class AttrsMessage:
    sender: str
    recipient: str
    body: str

if __name__ == "__main__":
    pos = Message(
        sender="sender@exmaple.com",
        recipient="recipient.example.com",
        body="Hello, World!",
    )
    if sys.version_info.minor >=7:
        simple = DataClassMessage(
            sender="sender@exmaple.com",
            recipient="recipient.example.com",
            body="Hello, World!",
        )
        slotted = SlotMessage(
            sender="sender@exmaple.com",
            recipient="recipient.example.com",
            body="Hello, World!",
        )
        print(
            "Dataclass %d, Slotted dataclass %d"
            % asizeof.asizesof(simple, slotted)
        )
    attrs = AttrsMessage(
        sender="sender@exmaple.com",
        recipient="recipient.example.com",
        body="Hello, World!",
    )
Dataclass 536, Slotted dataclass 56
NamedTuple 272,  Attrs 56

こんな感じで3propertiesのclassでも __slots__ のありなしで 10倍程度メモリ使用量が違ったりします。 特に実行回数が多い関数の返り値などでdataclassを使用する場合は使用メモリのためにも使っておくことが無難です。

まとめ

本記事では以下の説明をしました。

  • attrsのつかいどころ
    • 今Python3.5 or 3.6を使っている。今後dataclassを使いたい
    • dataclassで __slots__ などを使いたい場合にボイラープレートコードを避けたい
  • __slots__ の効果

  1. Scalaのcase class, Kotlinのdata class的なものだと思ってもらうと良いと思います(プロパティはデフォルトではimmutableではないですが)

  2. https://devguide.python.org/#status-of-python-branches Python3.5/3.6もまだEOLでない