For a particular application I was wondering whether to represent a state by the absence of an attribute, or by the attribute being None. Curious, I decided to benchmark it, and the result surprised me a lot:
hasattr() is very slow on specifically a slot that has no value.
Really, very very slow. I created two classes:
class C1:
__slots__ = ('x',)
class C2:
pass
…and benchmarked hasattr(obj, 'x') for both of them in the case where x was and was not set:
C1,hasattr→False: 242 nsC1,hasattr→True: 30 nsC2,hasattr→False: 33 nsC2,hasattr→True: 31 ns
So that’s a slowdown by a factor of 7-8. Does anybody know why this is the case? Is it a bug?
Any insight would be gratefully appreciated!
Supplementary observations:
Testing was on a local build of 3.14.0 under x86_64 Ubuntu, but I replicated the phenomenon under Ubuntu’s builds of 3.13.0 and 3.12.7 .
getattr(obj, 'x', sentinel) is not sentinel is a consistent 20ns slower than hasattr(obj, 'x') across the board: the same spike is observed for a slot that has no value.
Conversely, try / getattr(obj, 'x') / catch AttributeError is slow any time the exception path is taken, for obvious reasons, but there is not an additional slowdown for a slot.
It makes very little difference what other slots the object has, whether or not it has a __dict__ slot, what other attributes there are in slots and/or __dict__, etc.
Down in the C code, the key difference seems to be whether PyObject_GetOptionalAttr() or PyObject_GetAttr() gets called. I observed that the implementation of the former looks very much more complicated than the latter, but then realised I’d gone way too far down a rabbit hole and backed out.