在Python中是否存在可变的命名元组?

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。根据命名元组的特点,在构造对象时,输出的表示顺序必须与参数列表的顺序相匹配。


有一个可变的替代方法来替代collections.namedtuple记录类。

它与namedtuple具有相同的API和内存占用,并且支持分配(也应该更快)。例如:

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和更高版本的recordclass(从0.5开始)支持类型提示:

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 recordclass库提供了另一种变体--recordclass.structclass工厂功能。它可以生成类,这些类的实例占用的内存比基于__slots__的实例少。对于具有属性值的实例来说,这一点很重要,因为这些实例并不打算具有引用循环。如果需要创建数百万个实例,它可能有助于减少内存使用。下面是一个示例。


似乎这个问题的答案是否定的。

下面是相当接近,但它不是技术上的可变。这将使用更新的x值创建一个新的namedtuple()实例:

1
2
3
Point = namedtuple('Point', ['x', 'y'])
p = Point(0, 0)
p = p._replace(x=10)

另一方面,您可以使用__slots__创建一个简单的类,该类对于频繁更新类实例属性很有用:

1
2
3
4
5
class Point:
    __slots__ = ['x', 'y']
    def __init__(self, x, y):
        self.x = x
        self.y = y

除此之外,我认为__slots__在这里是很好的用途,因为当您创建许多类实例时,它的内存效率很高。唯一的缺点是不能创建新的类属性。

这里有一个相关的线程来说明内存效率——字典和对象——哪个更有效,为什么?

这个线程的答案中引用的内容是一个非常简洁的解释,说明了为什么__slots__更节省内存-python插槽


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实现,而recordclass是C扩展。当然,这取决于您的需求是否首选C扩展。

您的测试(也可参见下面的注释):

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唯一的区别是namedlist变小了,大小为56(python 2.7报告64)。

请注意,我已将您的测试10更改为就地更换。namedlist有一个_replace()方法,它复制得很浅,这对我来说非常有意义,因为标准库中的namedtuple的行为方式相同。更改_replace()方法的语义会令人困惑。我认为应该使用_update()方法进行就地更新。或者我没能理解你考试的目的?


作为这项任务的一个非常Python式的替代方案,由于python-3.7,您可以使用dataclasses模块不仅表现得像一个可变的namedtuple模块,因为它们使用普通的类定义,它们还支持其他类功能。

来自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的一个很好的解决方案:一个使用__slots__Sequence抽象基类的最小类;不进行复杂的错误检测或类似的检测,但它可以工作,其行为主要类似于可变元组(除了类型检查)。

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中,您需要稍微调整它——如果您继承自Sequence,那么类将具有__dict____slots__将停止工作。

python 2中的解决方案不是从Sequence继承,而是从object继承。如果需要isinstance(Point, Sequence) == True,需要将NamedMutableSequence注册为Sequence的基类:

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])