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