Overview¶
In order to fullfil its ambitious goal of bringing back the joy to writing classes, it gives you a class decorator and a way to declaratively define the attributes on that class:
>>> import attr
>>> @attr.s
... class C(object):
... x = attr.ib(default=42)
... y = attr.ib(default=attr.Factory(list))
...
... def hard_math(self, z):
... return self.x * self.y * z
>>> i = C(x=1, y=2)
>>> i
C(x=1, y=2)
>>> i.hard_math(3)
6
>>> i == C(1, 2)
True
>>> i != C(2, 1)
True
>>> attr.asdict(i)
{'y': 2, 'x': 1}
>>> C()
C(x=42, y=[])
>>> C2 = attr.make_class("C2", ["a", "b"])
>>> C2("foo", "bar")
C2(a='foo', b='bar')
After declaring your attributes attrs
gives you:
- a concise and explicit overview of the class’s attributes,
- a nice human-readable
__repr__
, - a complete set of comparison methods,
- an initializer,
- and much more,
without writing dull boilerplate code again and again and without runtime performance penalties.
This gives you the power to use actual classes with actual types in your code instead of confusing tuple
s or confusingly behaving namedtuple
s.
Which in turn encourages you to write small classes that do one thing well.
Never again violate the single responsibility principle just because implementing __init__
et al is a painful drag.
Philosophy¶
- It’s about regular classes.
attrs
for creating well-behaved classes with a type, attributes, methods, and everything that comes with a class. It can be used for data-only containers likenamedtuple
s ortypes.SimpleNamespace
but they’re just a sub-genre of whatattrs
is good for.- The class belongs to the users.
- You define a class and
attrs
adds static methods to that class based on the attributes you declare. The end. It doesn’t add meta classes. It doesn’t add classes you’ve never heard of to your inheritance tree. Anattrs
class in runtime is indistiguishable from a regular class: because it is a regular class with a few boilerplate-y methods attached. - Be light on API impact.
- As convenient as it seems at first,
attrs
will not tack on any methods to your classes save the dunder ones. Hence all the useful tools that come withattrs
live in functions that operate on top of instances. Since they take anattrs
instance as their first argument, you can attach them to your classes with one line of code. - Performance matters.
attrs
runtime impact is very close to zero because all the work is done when the class is defined. Once you’re instantiating it,attrs
is out of the picture completely.- No surprises.
attrs
creates classes that arguably work the way a Python beginner would reasonably expect them to work. It doesn’t try to guess what you mean because explicit is better than implicit. It doesn’t try to be clever because software shouldn’t be clever.
Check out How Does It Work? if you’d like to know how it achieves all of the above.
What attrs
Is Not¶
attrs
does not invent some kind of magic system that pulls classes out of its hat using meta classes, runtime introspection, and shaky interdependencies.
All attrs
does is take your declaration, write dunder methods based on that information, and attach them to your class.
It does nothing dynamic at runtime, hence zero runtime overhead.
It’s still your class.
Do with it as you please.
On the attr.s
and attr.ib
Names¶
The attr.s
decorator and the attr.ib
function aren’t any obscure abbreviations.
They are a concise and highly readable way to write attrs
and attrib
with an explicit namespace.
At first, some people have a negative gut reaction to that; resembling the reactions to Python’s significant whitespace. And as with that, once one gets used to it, the readability and explicitness of that API prevails and delights.
For those who can’t swallow that API at all, attrs
comes with serious business aliases: attr.attrs
and attr.attrib
.
Therefore, the following class definition is identical to the previous one:
>>> from attr import attrs, attrib, Factory
>>> @attrs
... class C(object):
... x = attrib(default=42)
... y = attrib(default=Factory(list))
>>> C()
C(x=42, y=[])
Use whichever variant fits your taste better.