Last active
April 10, 2025 09:15
-
-
Save wolph/02fae0b20b914354734aaac01c06d23b to your computer and use it in GitHub Desktop.
Benchmark namedtuple vs dataclass vs dict
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import sys | |
import enum | |
import math | |
import random | |
import timeit | |
import typing | |
import dataclasses | |
import collections | |
repeat = 5 | |
number = 1000 | |
N = 5000 | |
class PointTuple(typing.NamedTuple): | |
x: int | |
y: int | |
z: int | |
@dataclasses.dataclass | |
class PointDataclass: | |
x: int | |
y: int | |
z: int | |
@dataclasses.dataclass(slots=True) | |
class PointDataclassSlots: | |
x: int | |
y: int | |
z: int | |
class PointObject: | |
__slots__ = 'x', 'y', 'z' | |
x: int | |
y: int | |
z: int | |
def test_namedtuple_attr(): | |
point = PointTuple(1234, 5678, 9012) | |
for i in range(N): | |
x, y, z = point.x, point.y, point.z | |
def test_namedtuple_index(): | |
point = PointTuple(1234, 5678, 9012) | |
for i in range(N): | |
x, y, z = point | |
def test_namedtuple_unpack(): | |
point = PointTuple(1234, 5678, 9012) | |
for i in range(N): | |
x, *y = point | |
def test_dataclass(): | |
point = PointDataclass(1234, 5678, 9012) | |
for i in range(N): | |
x, y, z = point.x, point.y, point.z | |
def test_dataclass_slots(): | |
point = PointDataclassSlots(1234, 5678, 9012) | |
for i in range(N): | |
x, y, z = point.x, point.y, point.z | |
def test_dict(): | |
point = dict(x=1234, y=5678, z=9012) | |
for i in range(N): | |
x, y, z = point['x'], point['y'], point['z'] | |
def test_slots(): | |
point = PointObject() | |
point.x = 1234 | |
point.y = 5678 | |
point.z = 9012 | |
for i in range(N): | |
x, y, z = point.x, point.y, point.z | |
class PointEnum(enum.Enum): | |
x = 1 | |
y = 2 | |
z = 3 | |
def test_enum_attr(): | |
point = PointEnum | |
for i in range(N): | |
x, y, z = point.x, point.y, point.z | |
def test_enum_call(): | |
point = PointEnum | |
for i in range(N): | |
x, y, z = point(1), point(2), point(3) | |
def test_enum_item(): | |
point = PointEnum | |
for i in range(N): | |
x, y, z = point['x'], point['y'], point['z'] | |
if __name__ == '__main__': | |
tests = [ | |
test_namedtuple_attr, | |
test_namedtuple_index, | |
test_namedtuple_unpack, | |
test_dataclass, | |
test_dataclass_slots, | |
test_dict, | |
test_slots, | |
test_enum_attr, | |
test_enum_call, | |
test_enum_item, | |
] | |
print(f'Running tests {repeat} times with {number} calls.') | |
print(f'Using {N} iterations in the loop') | |
results = collections.defaultdict(lambda: math.inf) | |
for i in range(repeat): | |
# Shuffling tests to prevent skewed results due to CPU boosting or | |
# thermal throttling | |
random.shuffle(tests) | |
print(f'Run {i}:', end=' ') | |
for t in tests: | |
name = t.__name__ | |
print(name, end=', ') | |
sys.stdout.flush() | |
timer = timeit.Timer(f'{name}()', f'from __main__ import {name}') | |
results[name] = min(results[name], timer.timeit(number)) | |
print() | |
for name, result in sorted(results.items(), key=lambda x: x[::-1]): | |
print(f'{name:30} {result:.3f}s') |
Also, I don't think "test_namedtuple_unpack" needs to be included because that is more on the code that handles the *args.
While I agree, it's bound to be a question/comment if I don't ;)
And sometimes these results can be surprising so it might make a difference somehow
I personally wanted to see how enums faired, so i added them. Here is the code if you want to include it. side note: they did not fair well.
Very cool! I'll rerun the tests on Python 3.11 and update the benchmark and the answer :)
I've also added a dataclass with slots with this version
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
I got here from stack overflow: https://stackoverflow.com/questions/2646157/what-is-the-fastest-to-access-struct-like-object-in-python
I like this test, but I agree with the poster about x, y, z = point[0], point[1], point[2], though I didn't really notice a significant difference running this on python3.9. Also, I don't think "test_namedtuple_unpack" needs to be included because that is more on the code that handles the *args.
I personally wanted to see how enums faired, so i added them. Here is the code if you want to include it.
side note: they did not fair well.
Here is my py3.9 run via pycharm on win11, amd 5 ryzen 2600
Running tests 5 times with 1000 calls.
Using 5000 iterations in the loop
Run 0: test_dataclass, test_namedtuple_index, test_enum_attr, test_slots, test_enum_call, test_enum_item, test_namedtuple_attr, test_dict,
Run 1: test_namedtuple_index, test_enum_item, test_dataclass, test_slots, test_enum_attr, test_dict, test_enum_call, test_namedtuple_attr,
Run 2: test_dataclass, test_enum_item, test_enum_call, test_enum_attr, test_namedtuple_index, test_dict, test_slots, test_namedtuple_attr,
Run 3: test_namedtuple_attr, test_dataclass, test_enum_call, test_dict, test_namedtuple_index, test_slots, test_enum_item, test_enum_attr,
Run 4: test_enum_item, test_dict, test_namedtuple_index, test_dataclass, test_enum_call, test_slots, test_namedtuple_attr, test_enum_attr,
test_dataclass 0.35615059999999943
test_namedtuple_index 0.4273814000000016
test_namedtuple_attr 0.43110919999999453
test_slots 0.4442202999999978
test_dict 0.5744749999999996
test_enum_attr 1.4315308999999985
test_enum_item 2.6604619000000014
test_enum_call 7.318292999999997