Existence of mutable named tuple in Python?
是否有人可以修改NamedDuple或提供一个替代类,以便它适用于可变对象?
主要是为了可读性,我想要类似于NamedDuple的东西,这样做:
1 2 3 4 5 6 7 8 9 10 11 | 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) |
必须可以对生成的对象进行pickle。根据命名元组的特点,在构造对象时,输出的表示顺序必须与参数列表的顺序相匹配。
有一个可变的替代方法来替代
它与
1 2 3 4 5 6 7 8 9 10 11 | 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) |
对于python 3.6和更高版本的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | 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) |
有一个更完整的例子(它还包括性能比较)。
由于0.9
似乎这个问题的答案是否定的。
下面是相当接近,但它不是技术上的可变。这将使用更新的x值创建一个新的
1 2 3 | Point = namedtuple('Point', ['x', 'y']) p = Point(0, 0) p = p._replace(x=10) |
另一方面,您可以使用
1 2 3 4 5 | class Point: __slots__ = ['x', 'y'] def __init__(self, x, y): self.x = x self.y = y |
除此之外,我认为
这里有一个相关的线程来说明内存效率——字典和对象——哪个更有效,为什么?
这个线程的答案中引用的内容是一个非常简洁的解释,说明了为什么
types.simplenamespace在python 3.3中引入,并支持请求的需求。
1 2 3 4 5 6 7 8 9 10 | from types import SimpleNamespace t = SimpleNamespace(foo='bar') t.ham = 'spam' print(t) namespace(foo='bar', ham='spam') print(t.foo) 'bar' import pickle with open('/tmp/pickle', 'wb') as f: pickle.dump(t, f) |
截至2016年1月11日,最新的NamedList 1.7通过了所有使用python 2.7和python 3.5的测试。它是纯Python实现,而
您的测试(也可参见下面的注释):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 | 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: {}, {} '.format(p.x, p.y)) print('2. String') print('p: {} '.format(p)) print('3. Representation') print(repr(p), ' ') print('4. Sizeof') print('size of p:', sys.getsizeof(p), ' ') print('5. Access by name of field') print('p: {}, {} '.format(p.x, p.y)) print('6. Access by index') print('p: {}, {} '.format(p[0], p[1])) print('7. Iterative unpacking') x, y = p print('p: {}, {} '.format(x, y)) print('8. Iteration') print('p: {} '.format([v for v in p])) print('9. Ordered Dict') print('p: {} '.format(p._asdict())) print('10. Inplace replacement (update?)') p._update(x=100, y=200) print('p: {} '.format(p)) print('11. Pickle and Unpickle') pickled = pickle.dumps(p) unpickled = pickle.loads(pickled) assert p == unpickled print('Pickled successfully ') print('12. Fields ') print('p: {} '.format(p._fields)) print('13. Slots') print('p: {} '.format(p.__slots__)) |
python 2.7上的输出
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 | 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') |
与python 3.5唯一的区别是
请注意,我已将您的测试10更改为就地更换。
作为这项任务的一个非常Python式的替代方案,由于python-3.7,您可以使用
来自PEP057:
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.
PEP-0557中介绍了此功能,您可以在提供的文档链接中了解更多详细信息。
例子:
1 2 3 4 5 6 7 8 9 10 11 12 | 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 ...: |
演示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | 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) |
下面是Python3的一个很好的解决方案:一个使用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | 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') |
例子:
1 2 3 4 5 6 7 | >>> p = Point(0, 0) >>> p.x = 10 >>> p Point(x=10, y=0) >>> p.x *= 10 >>> p Point(x=100, y=0) |
如果需要,也可以有一个方法来创建类(尽管使用显式类更透明):
1 2 3 4 5 | def namedgroup(name, members): if isinstance(members, str): members = members.split() members = tuple(members) return type(name, (NamedMutableSequence,), {'__slots__': members}) |
例子:
1 2 3 | >>> Point = namedgroup('Point', ['x', 'y']) >>> Point(6, 42) Point(x=6, y=42) |
在python 2中,您需要稍微调整它——如果您继承自
python 2中的解决方案不是从
1 | Sequence.register(NamedMutableSequence) |
让我们用动态类型创建来实现这一点:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 | 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) |
在允许操作继续之前,这将检查属性是否有效。
那么这个是可以腌制的吗?是,如果(并且仅当)您执行以下操作:
1 2 3 4 5 6 7 8 9 10 | >>> 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 |
定义必须在命名空间中,并且必须存在足够长的时间,以便pickle找到它。因此,如果您将其定义为在您的包中,那么它应该是有效的。
1 | Point = namedgroup("Point", ["x","y"]) |
如果您执行以下操作,或者将定义设为临时的,pickle将失败(例如,当函数结束时超出范围):
1 | some_point = namedgroup("Point", ["x","y"]) |
是的,它确实保留了类型创建中所列字段的顺序。
如果您希望类似于NamedDuples但可变的行为,请尝试NamedList
注意,为了可变,它不能是元组。
元组根据定义是不可变的。
但是,您可以创建一个字典子类,在该子类中可以使用点标记访问属性;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | In [1]: %cpaste Pasting code; enter '--' alone on the line to stop or use Ctrl-D. :class AttrDict(dict): : : def __getattr__(self, name): : return self[name] : : def __setattr__(self, name, value): : self[name] = value :-- In [2]: test = AttrDict() In [3]: test.a = 1 In [4]: test.b = True In [5]: test Out[5]: {'a': 1, 'b': True} |
如果性能不重要,可以使用愚蠢的黑客,比如:
1 2 3 4 | from collection import namedtuple Point = namedtuple('Point', 'x y z') mutable_z = Point(1,2,[3]) |