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

时间:2015-03-26 22:56:46

标签: python mutable namedtuple

任何人都可以修改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)

必须能够腌制结果对象。根据命名元组的特性,表示输出的顺序必须与构造对象时参数列表的顺序相匹配。

11 个答案:

答案 0 :(得分:96)

collections.namedtuple - recordclass有一个可变的替代方案。

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

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)

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

由于0.9 recordclass库提供了另一种变体 - recordclass.structclass工厂函数。它可以生成类,其实例占用的内存少于基于__slots__的实例。这对于具有属性值的实例非常重要,属性值并非旨在具有参考周期。如果您需要创建数百万个实例,它可能有助于减少内存使用量。这是一个说明性的example

答案 1 :(得分:21)

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

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

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 - which is more efficient and why?

此主题的答案中引用的内容是一个非常简洁的解释,为什么__slots__的内存效率更高 - Python slots

答案 2 :(得分:20)

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)

答案 3 :(得分:19)

截至2016年1月11日,最新的namedlist 1.7通过了所有测试,包括Python 2.7和Python 3.5 这是一个纯粹的python实现recordclass是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. 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()方法应该用于就地更新。或者我可能无法理解你的测试10的意图?

答案 4 :(得分:9)

作为此任务的非常Pythonic替代方案,从Python-3.7开始,您可以使用 dataclasses模块不仅表现得像一个可变的NamedTuple,因为它们使用普通的类定义,它们也支持其他类的功能。

来自PEP-0557:

  

虽然它们使用了一种非常不同的机制,但数据类可以被认为是"具有默认值"的可变命名元组。由于数据类使用普通的类定义语法,因此您可以自由使用继承,元类,文档字符串,用户定义的方法,类工厂和其他Python类功能。

     

提供了一个类装饰器,它检查具有类型注释的变量的类定义,如PEP 526,"变量注释语法"中所定义。在本文档中,此类变量称为字段。使用这些字段,装饰器将生成的方法定义添加到类中以支持实例初始化,repr,比较方法以及Specification部分中描述的可选的其他方法。这样的类被称为数据类,但是对于类来说真的没有什么特别之处:装饰器将生成的方法添加到类中并返回给定的类。

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)

答案 5 :(得分:6)

以下是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中,您需要稍微调整一下 - 如果inherit from Sequence, the class will have a __dict____slots__将停止工作。

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

Sequence.register(NamedMutableSequence)

答案 6 :(得分: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"])

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

答案 7 :(得分:2)

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

请注意,为了变得可变,不能成为元组。

答案 8 :(得分:2)

我不敢相信以前没有人这么说过,但在我看来,Python 只是希望您编写自己的简单、可变的类,而不是在需要“{{1”时使用 namedtuple }}" 是可变的

重要提示:我通常会在类中的每个方法定义之间放置空的换行符,但是,这使得将这些类复制粘贴到实时 Python 解释器中会很不愉快,因为该换行符不包含正确的缩进。为了解决这个问题并使类易于复制粘贴到解释器中,我删除了每个方法定义之间的换行符。将它们重新添加到您编写的任何最终代码中。

TLDR;

直接跳到下面的方法 5。它简短而中肯,是迄今为止最好的选择。

各种详细的方法:

方法 1(好):简单、可调用的类,带有 namedtuple

以下是用于 __call__() 点的简单 Point 对象示例:

(x, y)

现在使用它:

class Point():
    def __init__(self, x, y):
        self.x = x
        self.y = y
    def __call__(self):
        """
        Make `Point` objects callable. Print their contents when they 
        are called.
        """
        print("Point(x={}, y={})".format(self.x, self.y))

这是完整的解释器输入和输出:

p1 = Point(1,2)
p1()
p1.x = 7
p1()
p1.y = 8
p1()

这与 >>> class Point(): ... def __init__(self, x, y): ... self.x = x ... self.y = y ... def __call__(self): ... """ ... Make `Point` objects callable. Print their contents when they ... are called. ... """ ... print("Point(x={}, y={})".format(self.x, self.y)) ... >>> p1 = Point(1,2) >>> p1() Point(x=1, y=2) >>> p1.x = 7 >>> p1() Point(x=7, y=2) >>> p1.y = 8 >>> p1() Point(x=7, y=8) 非常相似,除了它是完全可变的,与 namedtuple 不同。此外,namedtuple 不可调用,因此要查看其内容,只需在其后键入带括号的对象实例名称(如下例中的 namedtuple,INSTEAD OF 为 p2) .请参阅此示例并在此处输出:

p2()

方法 2(更好):使用 >>> from collections import namedtuple >>> Point2 = namedtuple("Point2", ["x", "y"]) >>> p2 = Point2(1, 2) >>> p2 Point2(x=1, y=2) >>> p2() Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: 'Point2' object is not callable >>> p2.x = 7 Traceback (most recent call last): File "<stdin>", line 1, in <module> AttributeError: can't set attribute 代替 __repr__()

我刚刚了解到您可以使用 __call__() 代替 __repr__(),以获得更多类似 __call__() 的行为。定义 namedtuple 方法允许您定义“对象的‘官方’字符串表示”(参见 official documentation here)。现在,只需调用 __repr__() 就等同于调用 p1 方法,您将获得与 __repr__() 相同的行为。这是新课程:

namedtuple

现在使用它:

class Point():
    def __init__(self, x, y):
        self.x = x
        self.y = y
    def __repr__(self):
        """
        Obtain the string representation of `Point`, so that just typing
        the instance name of an object of this type will call this method 
        and obtain this string, just like `namedtuple` already does!
        """
        return "Point(x={}, y={})".format(self.x, self.y)

这是完整的解释器输入和输出:

p1 = Point(1,2)
p1
p1.x = 7
p1
p1.y = 8
p1

方法 3(仍然更好,但使用起来有点尴尬):使其成为可调用的,返回一个 >>> class Point(): ... def __init__(self, x, y): ... self.x = x ... self.y = y ... def __repr__(self): ... """ ... Obtain the string representation of `Point`, so that just typing ... the instance name of an object of this type will call this method ... and obtain this string, just like `namedtuple` already does! ... """ ... return "Point(x={}, y={})".format(self.x, self.y) ... >>> p1 = Point(1,2) >>> p1 Point(x=1, y=2) >>> p1.x = 7 >>> p1 Point(x=7, y=2) >>> p1.y = 8 >>> p1 Point(x=7, y=8) 元组

原始海报 (OP) 也希望这样的东西起作用(请参阅我的答案下方的评论):

(x, y)

好吧,为了简单起见,让我们改为这样做:

x, y = Point(x=1, y=2)

当我们在做的时候,让我们也浓缩一下:

x, y = Point(x=1, y=2)()

# OR
p1 = Point(x=1, y=2)
x, y = p1()

...进入这个(来源where I first saw this):

self.x = x
self.y = y

以下是上述所有内容的类定义:

self.x, self.y = x, y

以下是一些测试调用:

class Point():
    def __init__(self, x, y):
        self.x, self.y = x, y
    def __repr__(self):
        """
        Obtain the string representation of `Point`, so that just typing
        the instance name of an object of this type will call this method 
        and obtain this string, just like `namedtuple` already does!
        """
        return "Point(x={}, y={})".format(self.x, self.y)
    def __call__(self):
        """
        Make the object callable. Return a tuple of the x and y components
        of the Point.
        """
        return self.x, self.y

这次我不会展示将类定义粘贴到解释器中,但以下是这些调用及其输出:

p1 = Point(1,2)
p1
p1.x = 7
x, y = p1()
x2, y2 = Point(10, 12)()
x
y
x2
y2

方法 4(目前最好,但要编写更多代码):使类也成为迭代器

通过把它变成一个迭代器类,我们可以得到这样的行为:

>>> p1 = Point(1,2)
>>> p1
Point(x=1, y=2)
>>> p1.x = 7
>>> x, y = p1()
>>> x2, y2 = Point(10, 12)()
>>> x
7
>>> y
2
>>> x2
10
>>> y2
12

让我们去掉 x, y = Point(x=1, y=2) # OR x, y = Point(1, 2) # OR p1 = Point(1, 2) x, y = p1 方法,但是为了使这个类成为迭代器,我们将添加 __call__()__iter__() 方法。在此处阅读有关这些内容的更多信息:

  1. https://treyhunner.com/2018/06/how-to-make-an-iterator-in-python/
  2. Build a basic Python iterator
  3. https://docs.python.org/3/library/exceptions.html#StopIteration

解决办法如下:

__next__()

还有一些测试调用:

class Point():
    def __init__(self, x, y):
        self.x, self.y = x, y
        self._iterator_index = 0
        self._num_items = 2  # counting self.x and self.y
    def __repr__(self):
        """
        Obtain the string representation of `Point`, so that just typing
        the instance name of an object of this type will call this method 
        and obtain this string, just like `namedtuple` already does!
        """
        return "Point(x={}, y={})".format(self.x, self.y)
    def __iter__(self):
        return self
    def __next__(self):
        self._iterator_index += 1
        if self._iterator_index == 1:
            return self.x
        elif self._iterator_index == 2:
            return self.y
        else:
            raise StopIteration

...输出:

x, y = Point(x=1, y=2)
x
y
x, y = Point(3, 4)
x
y
p1 = Point(5, 6)
x, y = p1
x
y
p1

方法 5(完美!最佳和最干净/最短的方法 - 使用这个!):使用 >>> x, y = Point(x=1, y=2) >>> x 1 >>> y 2 >>> x, y = Point(3, 4) >>> x 3 >>> y 4 >>> p1 = Point(5, 6) >>> x, y = p1 >>> x 5 >>> y 6 >>> p1 Point(x=5, y=6) 生成器关键字使类成为可迭代的

研究这些参考文献:

  1. https://treyhunner.com/2018/06/how-to-make-an-iterator-in-python/
  2. What does the "yield" keyword do?

这是解决方案。它依赖于一种花哨的“可迭代生成器”(AKA:只是“生成器”)关键字/Python 机制,称为 yield

基本上,迭代器第一次调用下一项时,它会调用yield方法,并停止并返回第一个__iter__()调用的内容(代码中的yield以下)。下一次迭代调用下一个项目时,它从上次停止的地方开始(在本例中就在第一个 self.x 之后),并寻找下一个 yield,停止并返回内容yield 调用(下面代码中的 yield)。 self.y 的每个“返回”实际上返回一个“生成器”对象,它本身是一个可迭代对象,因此您可以对其进行迭代。对下一项的每个新的可迭代调用都会继续这个过程,从上次停止的地方开始,就在最近调用的 yield 之后,直到不再存在 yield 调用,此时迭代已结束并且可迭代对象已被完全迭代。因此,一旦此可迭代对象调用了两个对象,两个 yield 调用都已用完,因此迭代器结束。最终结果是,像这样的调用完美地工作,就像它们在方法 4 中所做的那样,但是要编写的代码要少得多!

yield

这是解决方案(也可以在上面的 treyhunner.com 参考资料中找到该解决方案的一部分)。 注意这个解决方案是多么的简洁和干净!

只是类定义代码;没有文档字符串,因此您可以真正看到这是多么简短和简单:

x, y = Point(x=1, y=2)
# OR
x, y = Point(1, 2)
# OR
p1 = Point(1, 2)
x, y = p1

使用文档字符串:

class Point():
    def __init__(self, x, y):
        self.x, self.y = x, y
    def __repr__(self):
        return "Point(x={}, y={})".format(self.x, self.y)
    def __iter__(self):
        yield self.x
        yield self.y

复制并粘贴与上述方法(方法 4)中使用的完全相同的测试代码,您将获得与上述完全相同的输出!

参考:

  1. https://docs.python.org/3/library/collections.html#collections.namedtuple
  2. 方法 1:
    1. What is the difference between __init__ and __call__?
  3. 方法 2:
    1. https://www.tutorialspoint.com/What-does-the-repr-function-do-in-Python-Object-Oriented-Programming
    2. Purpose of __repr__ method?
    3. https://docs.python.org/3/reference/datamodel.html#object.__repr__
  4. 方法 4:
    1. *****[优秀!] https://treyhunner.com/2018/06/how-to-make-an-iterator-in-python/
    2. Build a basic Python iterator
    3. https://docs.python.org/3/library/exceptions.html#StopIteration
  5. 方法 5:
    1. 查看方法 4 中的链接,另外:
    2. *****[优秀!] What does the "yield" keyword do?
  6. What is the meaning of single and double underscore before an object name?

答案 9 :(得分:1)

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

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

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}

答案 10 :(得分:0)

如果性能不重要,可以使用愚蠢的黑客,如:

newInstance()