はじめに
こんにちは! 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