1.. _why:
2
3Why not…
4========
5
6
7…tuples?
8--------
9
10
11Readability
12^^^^^^^^^^^
13
14What makes more sense while debugging::
15
16   <Point(x=1, x=2)>
17
18or::
19
20   (1, 2)
21
22?
23
24Let's add even more ambiguity::
25
26   <Customer(id=42, reseller=23, first_name="Jane", last_name="John")>
27
28or::
29
30   (42, 23, "Jane", "John")
31
32?
33
34Why would you want to write ``customer[2]`` instead of ``customer.first_name``?
35
36Don't get me started when you add nesting.
37If 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.
38
39Using proper classes with names and types makes program code much more readable and comprehensible_.
40Especially when trying to grok a new piece of software or returning to old code after several months.
41
42.. _comprehensible: http://arxiv.org/pdf/1304.5257.pdf
43
44
45Extendability
46^^^^^^^^^^^^^
47
48Imagine you have a function that takes or returns a tuple.
49Especially 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*.
50
51Adding an attribute to a class concerns only those who actually care about that attribute.
52
53
54…namedtuples?
55-------------
56
57The difference between namedtuple_\ s and classes decorated by ``characteristic`` is that the latter are type-sensitive and less typing aside regular classes:
58
59
60.. doctest::
61
62   >>> from characteristic import Attribute, attributes
63   >>> @attributes([Attribute("a", instance_of=int)])
64   ... class C1(object):
65   ...     def __init__(self):
66   ...         if self.a >= 5:
67   ...             raise ValueError("'a' must be smaller 5!")
68   ...     def print_a(self):
69   ...         print self.a
70   >>> @attributes([Attribute("a", instance_of=int)])
71   ... class C2(object):
72   ...     pass
73   >>> c1 = C1(a=1)
74   >>> c2 = C2(a=1)
75   >>> c1.a == c2.a
76   True
77   >>> c1 == c2
78   False
79   >>> c1.print_a()
80   1
81   >>> C1(a=5)
82   Traceback (most recent call last):
83      ...
84   ValueError: 'a' must be smaller 5!
85
86
87…while namedtuple’s purpose is *explicitly* to behave like tuples:
88
89
90.. doctest::
91
92   >>> from collections import namedtuple
93   >>> NT1 = namedtuple("NT1", "a")
94   >>> NT2 = namedtuple("NT2", "b")
95   >>> t1 = NT1._make([1,])
96   >>> t2 = NT2._make([1,])
97   >>> t1 == t2 == (1,)
98   True
99
100
101This can easily lead to surprising and unintended behaviors.
102
103Other than that, ``characteristic`` also adds nifty features like type checks or default values.
104
105.. _namedtuple: https://docs.python.org/2/library/collections.html#collections.namedtuple
106.. _tuple: https://docs.python.org/2/tutorial/datastructures.html#tuples-and-sequences
107
108
109…hand-written classes?
110----------------------
111
112While I'm a fan of all things artisanal, writing the same nine methods all over again doesn't qualify for me.
113I usually manage to get some typos inside and there's simply more code that can break and thus has to be tested.
114
115To bring it into perspective, the equivalent of
116
117.. doctest::
118
119   >>> @attributes(["a", "b"])
120   ... class SmartClass(object):
121   ...     pass
122   >>> SmartClass(a=1, b=2)
123   <SmartClass(a=1, b=2)>
124
125is
126
127.. doctest::
128
129   >>> class ArtisinalClass(object):
130   ...     def __init__(self, a, b):
131   ...         self.a = a
132   ...         self.b = b
133   ...
134   ...     def __repr__(self):
135   ...         return "<ArtisinalClass(a={}, b={})>".format(self.a, self.b)
136   ...
137   ...     def __eq__(self, other):
138   ...         if other.__class__ is self.__class__:
139   ...             return (self.a, self.b) == (other.a, other.b)
140   ...         else:
141   ...             return NotImplemented
142   ...
143   ...     def __ne__(self, other):
144   ...         result = self.__eq__(other)
145   ...         if result is NotImplemented:
146   ...             return NotImplemented
147   ...         else:
148   ...             return not result
149   ...
150   ...     def __lt__(self, other):
151   ...         if other.__class__ is self.__class__:
152   ...             return (self.a, self.b) < (other.a, other.b)
153   ...         else:
154   ...             return NotImplemented
155   ...
156   ...     def __le__(self, other):
157   ...         if other.__class__ is self.__class__:
158   ...             return (self.a, self.b) <= (other.a, other.b)
159   ...         else:
160   ...             return NotImplemented
161   ...
162   ...     def __gt__(self, other):
163   ...         if other.__class__ is self.__class__:
164   ...             return (self.a, self.b) > (other.a, other.b)
165   ...         else:
166   ...             return NotImplemented
167   ...
168   ...     def __ge__(self, other):
169   ...         if other.__class__ is self.__class__:
170   ...             return (self.a, self.b) >= (other.a, other.b)
171   ...         else:
172   ...             return NotImplemented
173   ...
174   ...     def __hash__(self):
175   ...         return hash((self.a, self.b))
176   >>> ArtisinalClass(a=1, b=2)
177   <ArtisinalClass(a=1, b=2)>
178
179which is quite a mouthful and it doesn't even use any of ``characteristic``'s more advanced features like type checks or default values
180Also: no tests whatsoever.
181And who will guarantee you, that you don't accidentally flip the ``<`` in your tenth implementation of ``__gt__``?
182
183If you don't care and like typing, I'm not gonna stop you.
184But if you ever get sick of the repetitiveness, ``characteristic`` will be waiting for you. :)
185