Existence of mutable named tuple in Python?

前端 未结 10 2127
旧巷少年郎
旧巷少年郎 2020-11-29 16:23

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 namedt

相关标签:
10条回答
  • 2020-11-29 17:08

    Let's implement this with dynamic type creation:

    import copy
    def namedgroup(typename, fieldnames):
    
        def init(self, **kwargs): 
            attrs = {k: None for k in self._attrs_}
            for k in kwargs:
                if k in self._attrs_:
                    attrs[k] = kwargs[k]
                else:
                    raise AttributeError('Invalid Field')
            self.__dict__.update(attrs)
    
        def getattribute(self, attr):
            if attr.startswith("_") or attr in self._attrs_:
                return object.__getattribute__(self, attr)
            else:
                raise AttributeError('Invalid Field')
    
        def setattr(self, attr, value):
            if attr in self._attrs_:
                object.__setattr__(self, attr, value)
            else:
                raise AttributeError('Invalid Field')
    
        def rep(self):
             d = ["{}={}".format(v,self.__dict__[v]) for v in self._attrs_]
             return self._typename_ + '(' + ', '.join(d) + ')'
    
        def iterate(self):
            for x in self._attrs_:
                yield self.__dict__[x]
            raise StopIteration()
    
        def setitem(self, *args, **kwargs):
            return self.__dict__.__setitem__(*args, **kwargs)
    
        def getitem(self, *args, **kwargs):
            return self.__dict__.__getitem__(*args, **kwargs)
    
        attrs = {"__init__": init,
                    "__setattr__": setattr,
                    "__getattribute__": getattribute,
                    "_attrs_": copy.deepcopy(fieldnames),
                    "_typename_": str(typename),
                    "__str__": rep,
                    "__repr__": rep,
                    "__len__": lambda self: len(fieldnames),
                    "__iter__": iterate,
                    "__setitem__": setitem,
                    "__getitem__": getitem,
                    }
    
        return type(typename, (object,), attrs)
    

    This checks the attributes to see if they are valid before allowing the operation to continue.

    So is this pickleable? Yes if (and only if) you do the following:

    >>> import pickle
    >>> Point = namedgroup("Point", ["x", "y"])
    >>> p = Point(x=100, y=200)
    >>> p2 = pickle.loads(pickle.dumps(p))
    >>> p2.x
    100
    >>> p2.y
    200
    >>> id(p) != id(p2)
    True
    

    The definition has to be in your namespace, and must exist long enough for pickle to find it. So if you define this to be in your package, it should work.

    Point = namedgroup("Point", ["x", "y"])
    

    Pickle will fail if you do the following, or make the definition temporary (goes out of scope when the function ends, say):

    some_point = namedgroup("Point", ["x", "y"])
    

    And yes, it does preserve the order of the fields listed in the type creation.

    0 讨论(0)
  • 2020-11-29 17:09

    As a very Pythonic alternative for this task, since Python-3.7, you can use dataclasses module that not only behaves like a mutable NamedTuple because they use normal class definitions they also support other classes features.

    From PEP-0557:

    Although they use a very different mechanism, Data Classes can be thought of as "mutable namedtuples with defaults". Because Data Classes use normal class definition syntax, you are free to use inheritance, metaclasses, docstrings, user-defined methods, class factories, and other Python class features.

    A class decorator is provided which inspects a class definition for variables with type annotations as defined in PEP 526, "Syntax for Variable Annotations". In this document, such variables are called fields. Using these fields, the decorator adds generated method definitions to the class to support instance initialization, a repr, comparison methods, and optionally other methods as described in the Specification section. Such a class is called a Data Class, but there's really nothing special about the class: the decorator adds generated methods to the class and returns the same class it was given.

    This feature is introduced in PEP-0557 that you can read about it in more details on provided documentation link.

    Example:

    In [20]: from dataclasses import dataclass
    
    In [21]: @dataclass
        ...: class InventoryItem:
        ...:     '''Class for keeping track of an item in inventory.'''
        ...:     name: str
        ...:     unit_price: float
        ...:     quantity_on_hand: int = 0
        ...: 
        ...:     def total_cost(self) -> float:
        ...:         return self.unit_price * self.quantity_on_hand
        ...:    
    

    Demo:

    In [23]: II = InventoryItem('bisc', 2000)
    
    In [24]: II
    Out[24]: InventoryItem(name='bisc', unit_price=2000, quantity_on_hand=0)
    
    In [25]: II.name = 'choco'
    
    In [26]: II.name
    Out[26]: 'choco'
    
    In [27]: 
    
    In [27]: II.unit_price *= 3
    
    In [28]: II.unit_price
    Out[28]: 6000
    
    In [29]: II
    Out[29]: InventoryItem(name='choco', unit_price=6000, quantity_on_hand=0)
    
    0 讨论(0)
  • 2020-11-29 17:11

    The following is a good solution for Python 3: A minimal class using __slots__ and Sequence abstract base class; does not do fancy error detection or such, but it works, and behaves mostly like a mutable tuple (except for typecheck).

    from collections import Sequence
    
    class NamedMutableSequence(Sequence):
        __slots__ = ()
    
        def __init__(self, *a, **kw):
            slots = self.__slots__
            for k in slots:
                setattr(self, k, kw.get(k))
    
            if a:
                for k, v in zip(slots, a):
                    setattr(self, k, v)
    
        def __str__(self):
            clsname = self.__class__.__name__
            values = ', '.join('%s=%r' % (k, getattr(self, k))
                               for k in self.__slots__)
            return '%s(%s)' % (clsname, values)
    
        __repr__ = __str__
    
        def __getitem__(self, item):
            return getattr(self, self.__slots__[item])
    
        def __setitem__(self, item, value):
            return setattr(self, self.__slots__[item], value)
    
        def __len__(self):
            return len(self.__slots__)
    
    class Point(NamedMutableSequence):
        __slots__ = ('x', 'y')
    

    Example:

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

    If you want, you can have a method to create the class too (though using an explicit class is more transparent):

    def namedgroup(name, members):
        if isinstance(members, str):
            members = members.split()
        members = tuple(members)
        return type(name, (NamedMutableSequence,), {'__slots__': members})
    

    Example:

    >>> Point = namedgroup('Point', ['x', 'y'])
    >>> Point(6, 42)
    Point(x=6, y=42)
    

    In Python 2 you need to adjust it slightly - if you inherit from Sequence, the class will have a __dict__ and the __slots__ will stop from working.

    The solution in Python 2 is to not inherit from Sequence, but object. If isinstance(Point, Sequence) == True is desired, you need to register the NamedMutableSequence as a base class to Sequence:

    Sequence.register(NamedMutableSequence)
    
    0 讨论(0)
  • 2020-11-29 17:12

    The latest namedlist 1.7 passes all of your tests with both Python 2.7 and Python 3.5 as of Jan 11, 2016. It is a pure python implementation whereas the recordclass is a C extension. Of course, it depends on your requirements whether a C extension is preferred or not.

    Your tests (but also see the note below):

    from __future__ import print_function
    import pickle
    import sys
    from namedlist import namedlist
    
    Point = namedlist('Point', 'x y')
    p = Point(x=1, y=2)
    
    print('1. Mutation of field values')
    p.x *= 10
    p.y += 10
    print('p: {}, {}\n'.format(p.x, p.y))
    
    print('2. String')
    print('p: {}\n'.format(p))
    
    print('3. Representation')
    print(repr(p), '\n')
    
    print('4. Sizeof')
    print('size of p:', sys.getsizeof(p), '\n')
    
    print('5. Access by name of field')
    print('p: {}, {}\n'.format(p.x, p.y))
    
    print('6. Access by index')
    print('p: {}, {}\n'.format(p[0], p[1]))
    
    print('7. Iterative unpacking')
    x, y = p
    print('p: {}, {}\n'.format(x, y))
    
    print('8. Iteration')
    print('p: {}\n'.format([v for v in p]))
    
    print('9. Ordered Dict')
    print('p: {}\n'.format(p._asdict()))
    
    print('10. Inplace replacement (update?)')
    p._update(x=100, y=200)
    print('p: {}\n'.format(p))
    
    print('11. Pickle and Unpickle')
    pickled = pickle.dumps(p)
    unpickled = pickle.loads(pickled)
    assert p == unpickled
    print('Pickled successfully\n')
    
    print('12. Fields\n')
    print('p: {}\n'.format(p._fields))
    
    print('13. Slots')
    print('p: {}\n'.format(p.__slots__))
    

    Output on Python 2.7

    1. Mutation of field values  
    p: 10, 12
    
    2. String  
    p: Point(x=10, y=12)
    
    3. Representation  
    Point(x=10, y=12) 
    
    4. Sizeof  
    size of p: 64 
    
    5. Access by name of field  
    p: 10, 12
    
    6. Access by index  
    p: 10, 12
    
    7. Iterative unpacking  
    p: 10, 12
    
    8. Iteration  
    p: [10, 12]
    
    9. Ordered Dict  
    p: OrderedDict([('x', 10), ('y', 12)])
    
    10. Inplace replacement (update?)  
    p: Point(x=100, y=200)
    
    11. Pickle and Unpickle  
    Pickled successfully
    
    12. Fields  
    p: ('x', 'y')
    
    13. Slots  
    p: ('x', 'y')
    

    The only difference with Python 3.5 is that the namedlist has become smaller, the size is 56 (Python 2.7 reports 64).

    Note that I have changed your test 10 for in-place replacement. The namedlist has a _replace() method which does a shallow copy, and that makes perfect sense to me because the namedtuple in the standard library behaves the same way. Changing the semantics of the _replace() method would be confusing. In my opinion the _update() method should be used for in-place updates. Or maybe I failed to understand the intent of your test 10?

    0 讨论(0)
提交回复
热议问题