attrs iv: Zero-overhead Frozen attrs Classes

Here's a quick and easy one.

attrs supports frozen classes. Frozen classes are cool for a multitude of reasons, but they're a tiny bit slower to instantiate compared to non-frozen classes, because they need to perform some additional checks and avoiding these checks in the __init__ uses a tiny bit of time. (We're talking slotted classes here; which are awesome and the default in attrs nowadays.)

But there's a way to avoid this overhead and make frozen classes be the exact same speed as ordinary classes. The only caveat is: you have to use type-checking.

Create a file somewhere in your code base and put this in it:

from functools import partial
from typing import TYPE_CHECKING

if TYPE_CHECKING:
    from attrs import frozen
else:
    from attrs import define

    frozen = partial(define, unsafe_hash=True)

Now import frozen from this module and use it just like you'd use attrs.define or attrs.frozen. You're done!

This technique should also work for dataclasses, with a slight adjustment left as an exercize to the reader.

The eagle-eyed reader might notice that we're actually bamboozling the type-checker: your classes won't actually be frozen at runtime. The kicker is: they don't actually need to be.

As long as you're running one on your codebase, the typechecker is the actual thing that'll prevent you from mutating your instances. The unsafe_hash=True will make the classes hashable, and it's only unsafe if you mutate them after construction, which you won't. I guess you'll have to be careful when using a REPL or a different context where a typechecker might not hold sway, but I think that's not too big of an issue.

If you're still unconvinced, I'll leave you with two final thoughts: the memory in your computer is, ultimately, mutable. What makes immutable data structures in other languages immutable is just the amount of hoops you have to jump through to apply a mutation. This also demonstrates a basic technique statically-compiled languages use to be fast: move part of the work out of runtime into a separate, pre-runtime step. Which is exactly what we've done here.

Happy New Year!

Tin
Zagreb, Croatia