Alexander Alexander - 1 month ago 16
Python Question

Existence of mutable named tuple in Python?

Can anyone amend namedtuple or provide an alternative class so that it works for mutable objects?

Primarily for readability, I would like something similar to namedtuple that does this:

from Camelot import namedgroup

Point = namedgroup('Point', ['x', 'y'])
p = Point(0, 0)
p.x = 10

>>> p
Point(x=10, y=0)

>>> p.x *= 10
Point(x=100, y=0)


It must be possible to pickle the resulting object. And per the characteristics of named tuple, the ordering of the output when represented must match the order of the parameter list when constructing the object.

RESPONSE:
Thanks to everyone who submitted suggestions. I believe that the recordclass referred by @intellimath is the best solution (also see here).


recordclass 0.4
Mutable variant of collections.namedtuple, which supports assignments

recordclass is MIT Licensed python library. It implements the type
memoryslots and factory function recordclass in order to create
record-like classes.

memoryslots is tuple-like type, which supports assignment operations.
recordclass is a factory function that create a “mutable” analog of
collection.namedtuple. This library actually is a “proof of concept”
for the problem of “mutable” alternative of namedtuple.


I also ran some tests against all of the suggestions. Not all of these features were requested, so the comparison isn't really fair. The tests are here just to point out the usability of each class.

# Option 1 (p1): @kennes913
# Option 2 (p2): @MadMan2064
# Option 3 (p3): @intellimath
# Option 4 (p4): @Roland Smith
# Option 5 (p5): @agomcas
# Option 6 (p6): @Antti Haapala


# TEST: p1 p2 p3 p4 p5 p6
# 1. Mutation of field values | x | x | x | x | x | x |
# 2. String | | x | x | x | | x |
# 3. Representation | | x | x | x | | x |
# 4. Sizeof | x | x | x | ? | ?? | x |
# 5. Access by name of field | x | x | x | x | x | x |
# 6. Access by index. | | | x | | | |
# 7. Iterative unpacking. | | x | x | | | x |
# 8. Iteration | | x | x | | | x |
# 9. Ordered Dict | | | x | | | |
# 10. Inplace replacement | | | x | | | |
# 11. Pickle and Unpickle | | | x | | | |
# 12. Fields* | | | yes | | yes | |
# 13. Slots* | yes | | | | yes | |

# *Note that I'm not very familiar with slots and fields, so please excuse
# my ignorance in reporting their results. I have included them for completeness.

# Class/Object creation.
p1 = Point1(x=1, y=2)

Point2 = namedgroup("Point2", ["x", "y"])
p2 = Point2(x=1, y=2)

Point3 = recordclass('Point3', 'x y') # ***
p3 = Point3(x=1, y=2)

p4 = AttrDict()
p4.x = 1
p4.y = 2

p5 = namedlist('Point5', 'x y')

Point6 = namedgroup('Point6', ['x', 'y'])
p6 = Point6(x=1, y=2)

point_objects = [p1, p2, p3, p4, p5, p6]

# 1. Mutation of field values.
for n, p in enumerate(point_objects):
try:
p.x *= 10
p.y += 10
print('p{0}: {1}, {2}'.format(n + 1, p.x, p.y))
except Exception as e:
print('p{0}: Mutation not supported. {1}'.format(n + 1, e))

p1: 10, 12
p2: 10, 12
p3: 10, 12
p4: 10, 12
p5: 10, 12
p6: 10, 12


# 2. String.
for n, p in enumerate(point_objects):
print('p{0}: {1}'.format(n + 1, p))
p1: <__main__.Point1 instance at 0x10c72dc68>
p2: Point2(x=10, y=12)
p3: Point3(x=10, y=12)
p4: {'y': 12, 'x': 10}
p5: <class '__main__.Point5'>
p6: Point6(x=10, y=12)


# 3. Representation.
[('p{0}'.format(n + 1), p) for n, p in enumerate(point_objects)]

[('p1', <__main__.Point1 instance at 0x10c72dc68>),
('p2', Point2(x=10, y=12)),
('p3', Point3(x=10, y=12)),
('p4', {'x': 10, 'y': 12}),
('p5', __main__.Point5),
('p6', Point6(x=10, y=12))]


# 4. Sizeof.
for n, p in enumerate(point_objects):
print("size of p{0}:".format(n + 1), sys.getsizeof(p))

size of p1: 72
size of p2: 64
size of p3: 72
size of p4: 280
size of p5: 904
size of p6: 64


# 5. Access by name of field.
for n, p in enumerate(point_objects):
print('p{0}: {1}, {2}'.format(n + 1, p.x, p.y))

p1: 10, 12
p2: 10, 12
p3: 10, 12
p4: 10, 12
p5: 10, 12
p6: 10, 12


# 6. Access by index.
for n, p in enumerate(point_objects):
try:
print('p{0}: {1}, {2}'.format(n + 1, p[0], p[1]))
except:
print('p{0}: Unable to access by index.'.format(n+1))

p1: Unable to access by index.
p2: Unable to access by index.
p3: 10, 12
p4: Unable to access by index.
p5: Unable to access by index.
p6: Unable to access by index.


# 7. Iterative unpacking.
for n, p in enumerate(point_objects):
try:
x, y = p
print('p{0}: {1}, {2}'.format(n + 1, x, y))
except:
print('p{0}: Unable to unpack.'.format(n + 1))

p1: Unable to unpack.
p2: 10, 12
p3: 10, 12
p4: y, x
p5: Unable to unpack.
p6: 10, 12


# 8. Iteration
for n, p in enumerate(point_objects):
try:
print('p{0}: {1}'.format(n + 1, [v for v in p]))
except:
print('p{0}: Unable to iterate.'.format(n + 1))

p1: Unable to iterate.
p2: [10, 12]
p3: [10, 12]
p4: ['y', 'x']
p5: Unable to iterate.
p6: [10, 12]
In [95]:


# 9. Ordered Dict
for n, p in enumerate(point_objects):
try:
print('p{0}: {1}'.format(n + 1, p._asdict()))
except:
print('p{0}: Unable to create Ordered Dict.'.format(n + 1))

p1: Unable to create Ordered Dict.
p2: Unable to create Ordered Dict.
p3: OrderedDict([('x', 10), ('y', 12)])
p4: Unable to create Ordered Dict.
p5: Unable to create Ordered Dict.
p6: Unable to create Ordered Dict.


# 10. Inplace replacement
for n, p in enumerate(point_objects):
try:
p_ = p._replace(x=100, y=200)
print('p{0}: {1} - {2}'.format(n + 1, 'Success' if p is p_ else 'Failure', p))
except:
print('p{0}: Unable to replace inplace.'.format(n + 1))

p1: Unable to replace inplace.
p2: Unable to replace inplace.
p3: Success - Point3(x=100, y=200)
p4: Unable to replace inplace.
p5: Unable to replace inplace.
p6: Unable to replace inplace.


# 11. Pickle and Unpickle.
for n, p in enumerate(point_objects):
try:
pickled = pickle.dumps(p)
unpickled = pickle.loads(pickled)
if p != unpickled:
raise ValueError((p, unpickled))
print('p{0}: {1}'.format(n + 1, 'Pickled successfully', ))
except Exception as e:
print('p{0}: {1}; {2}'.format(n + 1, 'Pickle failure', e))

p1: Pickle failure; (<__main__.Point1 instance at 0x10c72dc68>, <__main__.Point1 instance at 0x10ca631b8>)
p2: Pickle failure; (Point2(x=10, y=12), Point2(x=10, y=12))
p3: Pickled successfully
p4: Pickle failure; '__getstate__'
p5: Pickle failure; Can't pickle <class '__main__.Point5'>: it's not found as __main__.Point5
p6: Pickle failure; (Point6(x=10, y=12), Point6(x=10, y=12))


# 12. Fields.
for n, p in enumerate(point_objects):
try:
print('p{0}: {1}'.format(n + 1, p._fields))
except Exception as e:
print('p{0}: {1}; {2}'.format(n + 1, 'Unable to access fields.', e))

p1: Unable to access fields.; Point1 instance has no attribute '_fields'
p2: Unable to access fields.; 'Point2' object has no attribute '_fields'
p3: ('x', 'y')
p4: Unable to access fields.; '_fields'
p5: ('x', 'y')
p6: Unable to access fields.; 'Point6' object has no attribute '_fields'


# 13. Slots.
for n, p in enumerate(point_objects):
try:
print('p{0}: {1}'.format(n + 1, p.__slots__))
except Exception as e:
print('p{0}: {1}; {2}'.format(n + 1, 'Unable to access slots', e))

p1: ['x', 'y']
p2: Unable to access slots; 'Point2' object has no attribute '__slots__'
p3: ()
p4: Unable to access slots; '__slots__'
p5: ('x', 'y')
p6: Unable to access slots; 'Point6' object has no attribute '__slots__'

Answer

There is a mutable alternative to collections.namedtuple - recordclass.

It has same API and memory footprint as namedtuple (actually it also faster). It also support assignments. For example:

from recordclass import recordclass

Point = recordclass('Point', 'x y')

>>> p = Point(1, 2)
>>> p
Point(x=1, y=2)
>>> print(p.x, p.y)
1 2
>>> p.x += 2; p.y += 3; print(p)
Point(x=3, y=5)

There is more complete example (it also include performance comparisons).

Comments