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


121

任何人都可以修改namedtuple或提供替代类,以使其适用于可变对象吗?

主要是为了提高可读性,我想要执行类似于namedtuple的操作:

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)

腌制所得物体必须是可能的。并且根据命名元组的特征,在表示对象时输出的顺序必须与构造对象时参数列表的顺序相匹配。


3
另请参阅:stackoverflow.com/q/5131044。您不能仅使用字典是有原因的吗?
senshin 2015年

@senshin感谢您的链接。由于其中指出的原因,我更不想使用字典。该响应还链接到code.activestate.com/recipes/…,与我所追求的非常接近。
亚历山大

namedtuples 不同,似乎您不需要通过索引引用属性,即这样p[0]p[1]这将是分别引用xy正确的替代方法?
martineau

理想情况下,是的,除了名称外,还可以像普通元组一样按位置索引,并且像元组一样解包。这个ActiveState配方很接近,但是我相信它使用常规词典而不是OrderedDict。code.activestate.com/recipes/500261
亚历山大

2
可变的namedtuple称为类。
gbtimmon

Answers:


132

还有就是一个可变的替代方案collections.namedtuple- recordclass

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

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开始)支持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)

有一个更完整的示例(还包括性能比较)。

由于0.9 recordclass库提供了另一个变体- recordclass.structclass工厂功能。它可以产生类,其实例比__slots__基于实例的实例占用更少的内存。这对于具有属性值的实例非常重要,该属性值不打算具有参考周期。如果您需要创建数百万个实例,则可能有助于减少内存使用。这是一个说明性的例子


4
喜欢它。“这个图书馆实际上是命名元组的“可变”替代问题的“概念证明”。–
亚历山大

1
recordclass与Antti Haapala的食谱和相比,它速度慢,占用更多内存并需要C扩展名namedlist
GrantJ

recordclass是其的可变版本collection.namedtuple,继承了它的api,内存占用量,但支持分配。 namedlist实际上是带有插槽的python类的实例。如果您不需要按索引快速访问它的字段,它会更有用。
intellimath

recordclass例如(python 3.5.2)的属性访问比namedlist
Intelli3

在使用namedtuple和简单类创建时Point = namedtuple('Point', 'x y'),Jedi可以自动完成属性,而情况并非如此recordclass。如果我使用较长的创建代码(基于RecordClass),那么Jedi可以理解Point该类,但不能理解其构造函数或属性...是否可以recordclass与Jedi更好地合作?
PhilMacKay

34

types.SimpleNamespace在Python 3.3中引入,并支持所要求的要求。

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)

1
多年来,我一直在寻找类似的东西。像dotmap这样的点缀字典库的绝佳替代品
axwell,2018年

1
这需要更多的支持。这正是OP所需要的,它在标准库中,使用起来再简单不过了。谢谢!
Tom Zych '18

3
-1 OP通过他的测试非常清楚地说明了他需要什么,但SimpleNamespace失败了6-10(按索引访问,迭代解压缩,迭代,有序dict,就地替换)和12、13(字段,插槽)。请注意,该文档(您在答案中链接的文档)专门说SimpleNamespace可能有用class NS: pass。但是,对于结构化记录类型,请namedtuple()改用。”
阿里

1
-1也会SimpleNamespace创建一个对象,而不是类构造函数,并且不能替换namedtuple。类型比较将不起作用,并且内存占用量会更高。
RedGlyph

26

作为此任务的一种非常Pythonic的替代方法,从Python-3.7开始,您可以使用 dataclasses不仅行为可变的模块,NamedTuple因为它们使用常规的类定义,而且还支持其他类功能。

从PEP-0557:

尽管它们使用了非常不同的机制,但是可以将数据类视为“具有默认值的可变命名元组”。因为数据类使用普通的类定义语法,所以您可以自由使用继承,元类,文档字符串,用户定义的方法,类工厂和其他Python类功能。

提供了一个类装饰器,该类装饰器检查类定义中具有类型注释的变量,如PEP 526 “变量注释的语法”中所定义。在本文档中,此类变量称为字段。装饰器使用这些字段将生成的方法定义添加到类中,以支持实例初始化,repr,比较方法以及(可选)规范部分中描述的其他方法。这样的类称为数据类,但该类实际上没有什么特别的:装饰器将生成的方法添加到该类中并返回给定的相同类。

PEP-0557中引入了此功能,您可以在提供的文档链接上详细了解它。

例:

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
    ...:    

演示:

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)

1
OP中的测试非常清楚需要什么dataclass,但在Python 3.7中失败了6-10(按索引访问,迭代拆包,迭代,有序dict,就地替换)和12、13(字段,插槽)测试失败.1。
阿里

1
尽管这可能不是OP所特有的,但它肯定对我有帮助:)
Martin CR

25

截至2016 1月11日,最新的namedlist 1.7通过了Python 2.7和Python 3.5的所有测试它是纯python实现,recordclassC是C扩展。当然,是否需要C扩展名取决于您的要求。

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

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

在Python 2.7上输出

1.字段值的突变  
p:10、12

2.字符串  
p:点(x = 10,y = 12)

3.陈述  
点(x = 10,y = 12) 

4. Sizeof  
p的大小:64 

5.按字段名称访问  
p:10、12

6.按索引访问  
p:10、12

7.迭代拆包  
p:10、12

8.迭代  
p:[10、12]

9.有序词典  
p:OrderedDict([['x',10),('y',12)])

10.就地更换(更新?)  
p:点(x = 100,y = 200)

11.泡菜和腌菜  
腌制成功

12.领域  
p:('x','y')

13.插槽  
p:('x','y')

与Python 3.5的唯一区别是namedlist变得更小,大小为56(Python 2.7报告64)。

请注意,我已将您的测试10更改为就地更换。namedlist_replace()哪些做了浅拷贝的方法,这使我感觉良好,因为namedtuple在标准库的工作方式。更改_replace()方法的语义会造成混乱。我认为该_update()方法应用于就地更新。还是我无法理解您的测试10的意图?


有重要的细微差别。namedlist列表实例中的存储值。问题是,cpythonlist实际上是一个动态数组。通过设计,它分配了比必要数量更多的内存,以使列表的更改更便宜。
intellimath'6

1
@intellimath namedlist有点用词不当。它实际上并没有继承优化,list并且默认情况下使用__slots__优化。当我测量时,内存使用量少于recordclass:96字节vs Python 2.7上六个字段的104字节
GrantJ 2016年

@GrantJ是的。recorclass使用更多的内存,因为它是一个类似tuple的对象,具有可变的内存大小。
intellimath

2
匿名小票对任何人都没有帮助。答案有什么问题?为什么要下票?
阿里

我喜欢它提供的防止错别字的安全性types.SimpleNamespace。不幸的是,pylint不喜欢它:-(
xverges

23

看来这个问题的答案是否定的。

下面的内容非常接近,但从技术上讲并不是可变的。这将创建一个namedtuple()具有更新的x值的新实例:

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

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

class Point:
    __slots__ = ['x', 'y']
    def __init__(self, x, y):
        self.x = x
        self.y = y

为了补充这个答案,我认为__slots__在这里很好用,因为当您创建许多类实例时,它的内存使用效率很高。唯一的缺点是您不能创建新的类属性。

这是一个说明内存效率的相关线程 -Dictionary vs Object-效率更高,为什么?

该线程答案中引用的内容非常简洁地解释了为什么__slots__内存效率更高-Python插槽


1
靠近,但笨拙。假设我想做一个+ =分配,然后我需要做:p._replace(x = px + 10)vs. px + = 10
亚历山大

1
是的,它并没有真正改变现有的元组,而是创建了一个新实例
kennes

7

以下是适用于Python 3的良好解决方案:最小类使用__slots__Sequence抽象基类;不会执行类似的错误检测,但它可以工作,并且其行为基本上类似于可变元组(类型检查除外)。

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

例:

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

如果需要,您也可以使用一种方法来创建类(尽管使用显式类更为透明):

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

例:

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

Sequence.register(NamedMutableSequence)

3

让我们通过动态类型创建来实现这一点:

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)

这将在允许操作继续之前检查属性以查看它们是否有效。

那么,这是可腌制的吗?是(且仅当您执行以下操作时):

>>> 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可以找到它。因此,如果您将其定义在包中,则应该可以使用。

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

如果您执行以下操作,或者将定义设为临时定义,则Pickle将失败(例如,函数结束时超出范围):

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

是的,它确实保留了类型创建中列出的字段的顺序。


如果使用添加__iter__方法for k in self._attrs_: yield getattr(self, k),它将支持像元组那样的解压缩。
snapshoe 2015年

添加和方法也很容易__len__,以支持按索引获取值,例如。有了这些最后一点,这似乎是最完整和正确的答案(无论如何对我来说)。__getitem____setiem__p[0]
snapshoe 2015年

__len__而且__iter__很好。 __getitem____setitem__能真正被映射到self.__dict__.__setitem__self.__dict__.__getitem__
MadMan2064

2

根据定义,元组是不可变的。

但是,您可以创建一个字典子类,在其中可以使用点符号访问属性。

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}

2

如果您想要与namedtuples类似的行为但可变,请尝试namedlist

注意,为了可变,它不能是元组。


感谢您的链接。这看起来是到目前为止最接近的,但是我需要对其进行更详细的评估。顺便说一句,我完全知道元组是不可变的,这就是为什么我在寻找诸如 namedtuple之的解决方案。
亚历山大

0

如果性能并不重要,则可以使用如下愚蠢的方法:

from collection import namedtuple

Point = namedtuple('Point', 'x y z')
mutable_z = Point(1,2,[3])

1
这个答案不是很好解释。如果您不了解列表的可变性质,则可能会造成混淆。---在这个例子......重新分配z,你必须调用mutable_z.z.pop(0),然后mutable_z.z.append(new_value)。如果弄错了,您将得到不止1个元素,并且程序将异常运行。
byxor

1
@byxor,或者您可以:mutable_z.z[0] = newValue。如上所述,这确实是一个hack。
Srg '18年

哦,是的,我很惊讶我错过了更明显的重新分配方式。
byxor

我喜欢,真正的技巧。
WebOrCode
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.