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__'

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

It has the same API and memory footprint as namedtuple and it supports assignments (It should be faster as well). 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)

For python 3.6 and higher recordclass (since 0.5) support typehints:

    from recordclass import recordclass, RecordClass

    class Point(RecordClass):
       x: int
       y: int

    >>> Point.__annotations__
    {'x':int, 'y':int}
    >>> 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 a more complete example (it also includes performance comparisons).

From: stackoverflow.com/q/29290359

Back to homepage or read more recommendations: