Performance Implications
The additional functionality provided by spec-classes is not free, but effort is made to keep things as performant as possible. In the following we briefly investigate the relative performance of spec-classes to raw classes and data-classes.
Let's define three classes, one using dataclass, one with a trivial
__setattr__
implementation, and one using spec-classes.
from dataclasses import dataclass
from spec_classes import spec_class
@dataclass
class MyData:
"""
Basic data class. No `__setattr__` is implemented, so things stay mostly in
C.
"""
a: int = 1
b: int = 2
c: int = 3
@dataclass
class MyDataRaw:
"""
Basic data class with trivial `__setattr__` implementation.
"""
a: int = 1
b: int = 2
c: int = 3
def __setattr__(self, attr, value):
super().__setattr__(attr, value)
@spec_class(bootstrap=True)
class MySpec:
"""
Simple spec-class, which enforces types at runtime.
"""
a: int = 1
b: int = 2
c: int = 3
Without any attempt to be scientific and exact in the performance comparisons, here is a simple benchmark for instantiating these classes (on spec-classes version 1.0.1):
%timeit MyData(a=1, b=2, c=3)
# 330 ns ± 11.9 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)
%timeit MyDataRaw(a=1, b=2, c=3)
# 1.02 µs ± 44 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)
%timeit MySpec(a=1, b=2, c=3)
# 7.88 µs ± 310 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)
Roughly speaking, then, spec-classes is about 23 times slower than when using
the basic class behavior (implemented in C); or about 8 times slower than when
using the basic class behavior but with a trivial Python __setattr__
wrapper.
Obviously this overhead will increase when using other more advanced features
of spec-classes, such as the attribute preparers, but you should expect the
overhead to be roughly commensurate with the performance overhead of
implementing it outside of spec-classes.
While this overhead is non-trivial, for the use-case for which it is designed (where there is a human-sensible number of classes to mutate and configure), this overhead is acceptable. In the future, we may add support for disabling type-checking and/or other enforcements, which may allow us to reduce this overhead further.