attrs/docs/why.rst

179 lines
4.8 KiB
ReStructuredText
Raw Normal View History

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
…tuples?
--------
Readability
^^^^^^^^^^^
What makes more sense while debugging::
2015-01-27 22:08:55 +00:00
Point(x=1, x=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.
If you've never ran into mysterious tuples you had no idea what the hell they meant while debugging, you're much smarter then I am.
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.
.. _comprehensible: http://arxiv.org/pdf/1304.5257.pdf
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?
-------------
The difference between :func:`collections.namedtuple`\ s and classes decorated by ``attrs`` is that the latter are type-sensitive and less typing aside 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):
2015-01-27 22:03:42 +00:00
... print self.a
>>> @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
…while namedtuples purpose is *explicitly* to behave like tuples:
.. 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.
2015-01-27 21:41:24 +00:00
Other than that, ``attrs`` also adds nifty features like validators or default values.
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
…hand-written classes?
----------------------
While I'm a fan of all things artisanal, writing the same nine methods all over again doesn't qualify for me.
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::
>>> class ArtisinalClass(object):
... def __init__(self, a, b):
... self.a = a
... self.b = b
...
... def __repr__(self):
2015-01-27 22:08:55 +00:00
... return "ArtisinalClass(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))
>>> ArtisinalClass(a=1, b=2)
2015-01-27 22:08:55 +00:00
ArtisinalClass(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__``?
If you don't care and like typing, I'm not gonna stop you.
But if you ever get sick of the repetitiveness, ``attrs`` will be waiting for you. :)