任何人都可以修改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)
必须能够腌制结果对象。根据命名元组的特性,表示输出的顺序必须与构造对象时参数列表的顺序相匹配。
答案 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 解释器中会很不愉快,因为该换行符不包含正确的缩进。为了解决这个问题并使类易于复制粘贴到解释器中,我删除了每个方法定义之间的换行符。将它们重新添加到您编写的任何最终代码中。
直接跳到下面的方法 5。它简短而中肯,是迄今为止最好的选择。
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()
>>> 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
>>> 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
通过把它变成一个迭代器类,我们可以得到这样的行为:
>>> 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__()
方法。在此处阅读有关这些内容的更多信息:
解决办法如下:
__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
>>> 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)
生成器关键字使类成为可迭代的研究这些参考文献:
这是解决方案。它依赖于一种花哨的“可迭代生成器”(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)中使用的完全相同的测试代码,您将获得与上述完全相同的输出!
答案 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()