2015-01-27 16:53:17 +00:00
.. _why:
2015-01-27 21:41:24 +00:00
Why not…
========
2015-01-27 16:53:17 +00:00
2015-01-27 21:41:24 +00:00
2016-08-15 11:59:04 +00:00
If you'd like third party's account why `` attrs `` is great, have a look at Glyph's `The One Python Library Everyone Needs <https://glyph.twistedmatrix.com/2016/08/attrs.html> `_ !
2015-01-27 21:41:24 +00:00
…tuples?
--------
Readability
^^^^^^^^^^^
What makes more sense while debugging::
2016-08-15 08:03:18 +00:00
Point(x=1, y=2)
2015-01-27 21:41:24 +00:00
or::
(1, 2)
?
Let's add even more ambiguity::
2015-01-27 22:08:55 +00:00
Customer(id=42, reseller=23, first_name="Jane", last_name="John")
2015-01-27 21:41:24 +00:00
or::
(42, 23, "Jane", "John")
?
Why would you want to write `` customer[2] `` instead of `` customer.first_name `` ?
Don't get me started when you add nesting.
2017-03-21 15:49:54 +00:00
If you've never ran into mysterious tuples you had no idea what the hell they meant while debugging, you're much smarter then yours truly.
2015-01-27 21:41:24 +00:00
Using proper classes with names and types makes program code much more readable and comprehensible_.
Especially when trying to grok a new piece of software or returning to old code after several months.
2016-11-24 13:56:19 +00:00
.. _comprehensible: https://arxiv.org/pdf/1304.5257.pdf
2015-01-27 21:41:24 +00:00
Extendability
^^^^^^^^^^^^^
Imagine you have a function that takes or returns a tuple.
Especially if you use tuple unpacking (eg. `` x, y = get_point() `` ), adding additional data means that you have to change the invocation of that function *everywhere* .
Adding an attribute to a class concerns only those who actually care about that attribute.
…namedtuples?
-------------
2016-08-15 08:00:23 +00:00
The difference between :func: `collections.namedtuple` \ s and classes decorated by `` attrs `` is that the latter are type-sensitive and require less typing as compared with regular classes:
2015-01-27 16:53:17 +00:00
.. doctest ::
2015-01-27 22:03:42 +00:00
>>> import attr
>>> @attr.s
2015-01-27 16:53:17 +00:00
... class C1(object):
2015-01-27 22:03:42 +00:00
... a = attr.ib()
2015-01-27 16:53:17 +00:00
... def print_a(self):
2016-02-17 11:51:02 +00:00
... print(self.a)
2015-01-27 22:03:42 +00:00
>>> @attr.s
2015-01-27 16:53:17 +00:00
... class C2(object):
2015-01-27 22:03:42 +00:00
... a = attr.ib()
2015-01-27 16:53:17 +00:00
>>> c1 = C1(a=1)
>>> c2 = C2(a=1)
2015-01-27 21:41:24 +00:00
>>> c1.a == c2.a
True
2015-01-27 16:53:17 +00:00
>>> c1 == c2
False
>>> c1.print_a()
1
2016-08-15 08:00:23 +00:00
…while a namedtuple is *explicitly* intended to behave like a tuple:
2015-01-27 16:53:17 +00:00
.. doctest ::
>>> from collections import namedtuple
>>> NT1 = namedtuple("NT1", "a")
>>> NT2 = namedtuple("NT2", "b")
>>> t1 = NT1._make([1,])
>>> t2 = NT2._make([1,])
>>> t1 == t2 == (1,)
True
This can easily lead to surprising and unintended behaviors.
2017-03-21 15:49:54 +00:00
Opinions on object immutability vary.
With `` attrs `` , the choice is yours.
Immutable classes are created by passing a `` frozen=True `` argument to the :func: `attr.s` decorator.
By default, however, classes created by `` attrs `` are regular Python classes and therefore mutable:
2017-03-21 15:40:20 +00:00
.. doctest ::
>>> import attr
>>> @attr.s
... class Customer(object):
... first_name = attr.ib()
2017-03-21 15:49:54 +00:00
>>> c1 = Customer(first_name="Kaitlyn")
2017-03-21 15:40:20 +00:00
>>> c1.first_name
'Kaitlyn'
2017-03-21 15:49:54 +00:00
>>> c1.first_name = "Katelyn"
2017-03-21 15:40:20 +00:00
>>> c1.first_name
'Katelyn'
…while classes created with :func: `collections.namedtuple` inherit from tuple and are therefore always immutable:
.. doctest ::
>>> from collections import namedtuple
2017-03-21 15:49:54 +00:00
>>> Customer = namedtuple("Customer", "first_name")
>>> c1 = Customer(first_name="Kaitlyn")
2017-03-21 15:40:20 +00:00
>>> c1.first_name
'Kaitlyn'
2017-03-21 15:49:54 +00:00
>>> c1.first_name = "Katelyn"
2017-03-21 15:40:20 +00:00
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: can't set attribute
2017-03-21 15:49:54 +00:00
Other than that, `` attrs `` also adds nifty features like validators, converters, and default values.
2015-01-27 21:41:24 +00:00
2015-01-27 16:53:17 +00:00
.. _tuple: https://docs.python.org/2/tutorial/datastructures.html#tuples-and-sequences
2015-01-27 21:41:24 +00:00
2016-09-10 18:27:44 +00:00
…dicts?
-------
Dictionaries are not for fixed fields.
If you have a dict, it maps something to something else.
You should be able to add and remove values.
Objects, on the other hand, are supposed to have specific fields of specific types, because their methods have strong expectations of what those fields and types are.
`` attrs `` lets you be specific about those expectations; a dictionary does not.
It gives you a named entity (the class) in your code, which lets you explain in other places whether you take a parameter of that class or return a value of that class.
In other words: if your dict has a fixed and known set of keys, it is an object, not a hash.
2015-01-27 21:41:24 +00:00
…hand-written classes?
----------------------
2017-03-21 15:49:54 +00:00
While we're fans of all things artisanal, writing the same nine methods all over again doesn't qualify for me.
2015-01-27 21:41:24 +00:00
I usually manage to get some typos inside and there's simply more code that can break and thus has to be tested.
To bring it into perspective, the equivalent of
.. doctest ::
2015-01-27 22:03:42 +00:00
>>> @attr.s
2015-01-27 21:41:24 +00:00
... class SmartClass(object):
2015-01-27 22:03:42 +00:00
... a = attr.ib()
... b = attr.ib()
>>> SmartClass(1, 2)
2015-01-27 22:08:55 +00:00
SmartClass(a=1, b=2)
2015-01-27 21:41:24 +00:00
is
.. doctest ::
2015-02-10 16:31:21 +00:00
>>> class ArtisanalClass(object):
2015-01-27 21:41:24 +00:00
... def __init__(self, a, b):
... self.a = a
... self.b = b
...
... def __repr__(self):
2015-02-10 16:31:21 +00:00
... return "ArtisanalClass(a={}, b={})".format(self.a, self.b)
2015-01-27 21:41:24 +00:00
...
... def __eq__(self, other):
... if other.__class__ is self.__class__:
... return (self.a, self.b) == (other.a, other.b)
... else:
... return NotImplemented
...
... def __ne__(self, other):
... result = self.__eq__(other)
... if result is NotImplemented:
... return NotImplemented
... else:
... return not result
...
... def __lt__(self, other):
... if other.__class__ is self.__class__:
... return (self.a, self.b) < (other.a, other.b)
... else:
... return NotImplemented
...
... def __le__(self, other):
... if other.__class__ is self.__class__:
... return (self.a, self.b) <= (other.a, other.b)
... else:
... return NotImplemented
...
... def __gt__(self, other):
... if other.__class__ is self.__class__:
... return (self.a, self.b) > (other.a, other.b)
... else:
... return NotImplemented
...
... def __ge__(self, other):
... if other.__class__ is self.__class__:
... return (self.a, self.b) >= (other.a, other.b)
... else:
... return NotImplemented
...
... def __hash__(self):
... return hash((self.a, self.b))
2015-02-10 16:31:21 +00:00
>>> ArtisanalClass(a=1, b=2)
ArtisanalClass(a=1, b=2)
2015-01-27 21:41:24 +00:00
which is quite a mouthful and it doesn't even use any of `` attrs `` 's more advanced features like validators or defaults values.
Also: no tests whatsoever.
And who will guarantee you, that you don't accidentally flip the `` < `` in your tenth implementation of `` __gt__ `` ?
2017-03-21 15:49:54 +00:00
If you don't care and like typing, we're not gonna stop you.
2015-01-27 21:41:24 +00:00
But if you ever get sick of the repetitiveness, `` attrs `` will be waiting for you. :)