attrs/docs/why.rst

210 lines
6.0 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. :)
…characteristic
---------------
``characteristic`` is a very similar and fairly popular project of mine.
So why the self-fork?
Basically after nearly a year of usage I ran into annoyances and regretted certain decision I made early-on to make too many people happy.
In the end, *I* wasn't happy using it anymore.
So I learned my lesson and ``attrib`` is the result of that.
Reasons For Forking
^^^^^^^^^^^^^^^^^^^
- Fixing those aforementioned annoyances would introduce more complexity.
More complexity means more bugs.
- Certain unused features make other common features complicated or impossible.
- I want it to be possible to gradually move from ``characteristic`` to ``attrs``.
A peaceful co-existence is much easier if it's separate packages altogether.
- My libraries have very strict backward-compatibility policies and it would take years to get rid of those annoyances while they shape the implementation of other features.
Main Differences
^^^^^^^^^^^^^^^^
- The attributes are defined *within* the class definition such that code analyzers know about their existence.
This is useful in IDEs like PyCharm or linters like PyLint.
- The names are held shorter and easy to both type and read.
- It is generally more opinionated towards typical uses.