TechFULの中の人

TechFULスタッフ・エンジニアによる技術ブログ。IT関連のことやTechFULコーディングバトルの超難問の深掘り・解説などを紹介

【Python3.7~】データクラスの速度について調べてみた

はじめに

こんにちは! 444株式会社エンジニアの白神(しらが)です。

もともと開発アルバイトとしてTechFULのジャッジ周りの開発をしていましたが、今年の4月から正社員として新卒で入社しました。まだまだ未熟ですが、先輩のエンジニアの方々に日々アドバイスを頂きながらなんとかやっていけています。

技術記事を書くのは初めてなので、至らない点もあるかと思いますが、良ければ見ていってください。


目次

概要

Python3.7からデータクラス(Data Classes)という機能が追加されました。

型ヒントが簡単に使えたり単純なコンストラクタなら書かなくて良かったりと、とても便利なのでよく使っているのですが、あるときデータクラスの生成速度は通常のクラスの数十倍遅いという記述を見つけました。(どこで見たかは忘れてしまったので、リンク等はありません。申し訳ないです。。。)

そこで、本当に遅いのか気になり、実際にどれくらい時間がかかるのか検証してみることにしました。

まずデータクラスって何ですか

このモジュールは、__init__()__repr__()を生成し、ユーザー定義のクラスに自動的に追加するデコレータや関数を提供します。

Python公式ドキュメントdataclasses より引用

つまり、__init__()などの毎回定義するのが大変な特殊メソッドをある程度自動的に追加してくれます。 使い方も簡単で、標準ライブラリとして定義されているdataclassesをインポートしてクラスにdataclassデコレータをつけるだけで使うことができます。

通常のクラスなら

# 通常のクラス
class HogeClass:
    # コンストラクタ
    def __init__(self, name: str, age: int):
        self.name = name
        self.age = age
        
        
    # 文字列表記
    def __repr__(self):
        args_text = ", ".join([f"{key}={attr}" for key, attr in self.__dict__.items()])
        return f"{self.__class__.__name__}({args_text})"
 
 
hoge = HogeClass(name="ほげふがほげ太", age=21)
 
print(hoge.name)  # 結果: ほげふがほげ太
print(hoge.age)      # 結果: 21
print(hoge)          # 結果: HogeClass(name=ほげふがほげ太, age=21)

こんな感じに定義します。

次はデータクラスで定義してみます。

from dataclasses import dataclass
 
 
# データクラス, dataclassデコレータをつける
@dataclass
class HogeClass:
    # 型ヒントがわかりやすく使える
    name: str
    age: int
  
  
hoge = HogeClass(name="ほげふがほげ太", age=21)
 
print(hoge.name) # 結果: ほげふがほげ太
print(hoge.age)      # 結果: 21
print(hoge)          # 結果: HogeClass(name='ほげふがほげ太', age=21)

とても簡単にわかりやすくクラスを定義できました。

データクラスの速度

さて、データクラスについてざっくり説明しましたが、データクラスは通常のクラスより速度が遅いです。クラス生成時にいい感じにいろいろやってくれるのがデータクラスなので遅いのはわかりますが、実際どれぐらい遅いのでしょうか?

以下のコードを実行して実際に計ってみました。

from dataclasses import dataclass
import time
 
 
LOOP = 100000
 
start_time = time.time()
for i in range(LOOP):
    class HogeClass:
        def __init__(self, name: str, age: int):
            self.name = name
            self.age = age
   
        
end_time = time.time()
 
print(f"クラスを{LOOP}回生成するのにかかった時間: {(end_time-start_time):.3f}秒")
 
 
start_time = time.time()
for i in range(LOOP):
    @dataclass
    class HogeDataClass:
        name: str
        age: int
        
        
end_time = time.time()
 
print(f"データクラスを{LOOP}回生成するのにかかった時間: {(end_time-start_time):.3f}秒")

結果

クラスを100000回生成するのにかかった時間: 0.419秒
データクラスを100000回生成するのにかかった時間: 11.250秒

この結果を見ると、データクラスはクラスの数十倍遅いというのは事実のようです。ただ、この時間はデータクラスの生成を十万回繰り返した速度なので、あまり気にするほどではないのかもしれません。

データクラスを上記検証のように大量に生成する場面はめったに無いと思います。 なぜなら、データクラスはクラスそのものの生成が遅いのであって、インスタンス化の速度が遅いわけではないからです。

ちょっとだけパフォーマンス向上

特定の場面でちょっとだけパフォーマンスを上げることができる、小手先のテクニックを紹介します。

適切な引数を渡す

dataclassデコレータにはいくつかの引数を渡すことができます。

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

各引数の詳細を詳しく知りたい方は、dataclassを参照してください。

ここでパフォーマンスを上げるために、デフォルトでTrueになっている引数で、自分が使わないものには明示的にFalseを渡しましょう。Falseになっている項目は処理されないのでその分早くなります。

from dataclasses import dataclass
import time
 
 
LOOP = 100000
 
start_time = time.time()
for i in range(LOOP):
    # __init__()だけ自動生成してほしい!
    @dataclass(repr=False, eq=False)
    class HogeDataClass:
        name: str
        age: int
        
        
end_time = time.time()
 
print(f"データクラスを{LOOP}回生成するのにかかった時間: {(end_time-start_time):.3f}秒")

結果

データクラスを100000回生成するのにかかった時間: 5.346秒

何も引数を渡さなかった場合と比べて約1/2に短縮できました。ただし、__repr__()__eq__()テストの際によく使うので、無効にする場合は気をつけてください。

データクラスを使わない

イミュータブル(再代入禁止)なデータの場合はデータクラスではなくNamedTupleを使うのもありだと思います。

from typing import NamedTuple
import time
 
 
LOOP = 100000
 
start_time = time.time()
for i in range(LOOP):
    class HogeNamedTuple(NamedTuple):
        name: str
        age: int
         
 
end_time = time.time()
 
print(f"NamedTupleを{LOOP}回生成するのにかかった時間: {(end_time-start_time):.3f}秒")

結果

NamedTupleを100000回作成するのにかかった時間: 2.603秒

どちらにも利点はあるので、用途にあった使い分けが大事だと思いました。

まとめ

  • データクラスは確かに普通のクラスよりは生成速度が遅いが、通常の使用方法ならあまり気にしなくても大丈夫。遅いのはクラスそのものの生成速度であり、インスタンス化の速度が遅いわけではない。
  • dataclassのデフォルト引数がTrueで、使用しない機能がある場合は、明示的にFalseを渡して処理をスキップさせると少しだけ早くなる。(ただしデフォルトでTrueになっているということは、よく使う事が多い機能なので無効にする際は注意)
  • 速度を求める場合は通常のクラス, Dict, NamedTupleなどを使う。

個人的な意見ですが、データクラスのいいところは、楽ができるところよりも、型ヒントのわかりやすさと、frozenを指定することで、ミュータブル(再代入可能)とイミュータブルを簡単に使い分けれるところだと思っています。

from dataclasses import dataclass
from typing import Dict, Union
  
  
# クラス
# 引数が多いとコードスタイルによっては折り返してしまい見づらくなる
class Hoge:
    def __init__(self, a: str, b: str, c: int, d: int, e: float,
        f: float, g: bool, h: bool, i: Dict[str, Union[str, int]]):
        # 大変
        self.a = a
        self.b = b
        self.c = c
        self.d = d
        self.e = e
        self.f = f
        self.g = g
        self.h = h
        self.i = i
        
        
# 辞書(型ヒント付き)
# 型ヒントがすごく分かりづらい, Dictの中にListやDictがあったりするともっと分かりづらい
hoge_dict: Dict[str, Union[str, int, float, bool, Dict[str, Union[str, int]]]] = {}
 
 
# こっちのほうが見やすい, frozenで再代入を禁止
@dataclass(frozen=True)
class Fuga:
    a: str
    b: str
    c: int
    d: int
    e: float
    f: float
    g: bool
    h: bool
    i: Dict[str, Union[str, int]]
    

あと、dataclasses.asdict()再帰的に辞書に変換できるのも好きです。

from dataclasses import dataclass, asdict
 
 
@dataclass
class HogeHoge:
    name: str
    age: int
 
 
# データクラスにデータクラスを持つ
@dataclass
class Piyo:
    name: str
    age: int
    disciple: HogeHoge
 
 
hoge_hoge = HogeHoge(name="ほげほげ", age=20)
piyo = Piyo(name="ぴよ", age=100, disciple=hoge_hoge)
    
# 再帰的に辞書に変換できる(discipleに代入されたhoge_hogeも辞書になる)
piyo_dict = asdict(piyo)
 
print(piyo_dict)
# 出力: {'name': 'ぴよ', 'age': 100, 'disciple': {'name': 'ほげほげ', 'age': 20}}

つよい。


最後に

私は普段Visual Studio Codeを使って開発をしているのですが、最近Pythonのデフォルト言語サーバがPylanceになり、簡単に型ヒントの恩恵を受けることができるようになりました。(Pythonの拡張機能を入れる必要があります) そのため、Python3.7以上で開発をしている場合は積極的にデータクラスや型ヒントを使っていくとわかりやすいコードになるのではないでしょうか。

皆さんはどう思いますか?


最後まで見ていただきありがとうございました。

次の機会があれば、私が今推している(構文が)Pythonに似た言語に関しての記事を書きたいと思っています。

なにかご意見やご指摘等があればコメントに書き込んでいただければとても助かります!

ではまた。

参考にしたサイト

Python Software Foundation: 公式ドキュメント dataclasses

Qiita: Python3.7からは「Data Classes」がクラス定義のスタンダードになるかもしれない

Qiita: Python3.7以上のデータ格納はdataclassを活用しよう

Stackoverflow: Data Classes vs typing.NamedTuple primary use cases