A question I answered on Stack Overflow asked for a mutable named tuple.
The naive answer would probably tell them they just want to define a custom object.
However, named tuples take up a lot less space than custom objects - unless those objects use __slots__
.
Further, a namedtuple is semantically just a record of data (maybe with some functionality associated with it.) So what if we really want something like a mutable "namedtuple" then?
I present, for your edification, a MutableNamedTuple (lifted and somewhat modified from my answer):
from collections import Sequence
class MutableNamedTuple(Sequence):
"""Abstract Base Class for objects as efficient as mutable
namedtuples.
Subclass and define your named fields with __slots__.
"""
__slots__ = ()
def __init__(self, *args):
for slot, arg in zip(self.__slots__, args):
setattr(self, slot, arg)
def __repr__(self):
return type(self).__name__ + repr(tuple(self))
# more direct __iter__ than Sequence's
def __iter__(self):
for name in self.__slots__:
yield getattr(self, name)
# Sequence requires __getitem__ & __len__:
def __getitem__(self, index):
return getattr(self, self.__slots__[index])
def __len__(self):
return len(self.__slots__)
And here's some examples of usage:
class MNT(MutableNamedTuple):
"""demo mutable named tuple with metasyntactic names"""
__slots__ = 'foo bar baz quux'.split()
>>> mnt = MNT(*'abcd')
>>> mnt
MNT('a', 'b', 'c', 'd')
>>> mnt.foo
'a'
>>> mnt.bar
'b'
>>> mnt.baz
'c'
>>> mnt.quux
'd'
It is indexable just like a tuple:
>>> foo, bar, baz, quux = mnt
>>> foo
'a'
>>> bar
'b'
>>> baz
'c'
>>> quux
'd'
>>> mnt[0]
'a'
>>> mnt[3]
'd'
It even raises IndexError
where apropos:
>>> mnt[4]
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 18, in __getitem__
IndexError: list index out of range
>>> for c in mnt: print(c)
...
a
b
c
d
Help isn't too bad if you give a decent docstring:
>>> help(MNT)
Help on class MNT in module __main__:
class MNT(MutableNamedTuple)
| demo mutable named tuple with metasyntactic names
...
Thanks to the ABC's mixin methods we can reverse it, check its length, and check for membership:
>>> reversed(mnt)
<generator object Sequence.__reversed__ at 0x7ff46c64b678>
>>> list(reversed(mnt))
['d', 'c', 'b', 'a']
>>> sorted(mnt)
['a', 'b', 'c', 'd']
>>> len(mnt)
4
>>> 'a' in mnt
True
>>> 'e' in mnt
False
Because we don't define __setitem__
or __delitem__
we get the same behavior (TypeError
, similar message) as tuples when we attempt to mutate it by index:
>>> del mnt[3]
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: 'MNT' object doesn't support item deletion
>>> mnt[3] = 'e'
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: 'MNT' object does not support item assignment
Perhaps this implementation of a mutable named tuple is even Liskov substitutable? We are not allowed to subclass tuple
, because we use non-empty __slots__
. If we just look at methods tuples have that MNT
doesn't have, we only see a few methods we don't implement:
>>> set(dir(tuple)) - set(dir(MNT))
{'__rmul__', '__add__', '__getnewargs__', '__mul__'}
__mul__
, __rmul__
, and __add__
all don't really make semantic sense for a fixed length data structure.
And __getnewargs__
relates to pickling, but the object pickles and unpickles just fine:
>>> import pickle
>>> pickle.loads(pickle.dumps(mnt))
MNT('a', 'b', 'c', 'd')
I think this is rather clever - so I'm putting this out for your scrutiny and amusement.
Roast me! Review my code! And suggest improvements! (Maybe I could improve on the constructor? Maybe subclass the ABCMeta metaclass? Can I enforce a subclass to create a docstring? Should I assert that the number of arguments is the same as the number of slots? How should I unittest it? Etc...)