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

167

有人能修改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)

转换出的对象必须是可被pickle序列化的。并且与命名元组的特性相符,当表示为字符串时,其顺序必须与构建对象时参数列表的顺序相匹配。


4
另请参阅:https://dev59.com/XW435IYBdhLWcg3w7kwK/。你为什么不能只使用字典呢? - senshin
2
@senshin 感谢提供链接。我不想使用字典,因为它所指出的原因。该回复还链接到http://code.activestate.com/recipes/52308-the-simple-but-handy-collector-of-a-bunch-of-named/?in=user-97991,这与我想要的非常接近。 - Alexander
1
namedtuple不同的是,似乎您无需能够通过索引引用属性,即p[0]p[1]将是分别引用xy的替代方式,对吗? - martineau
1
理想情况下,是的,可以像普通元组一样按位置索引,并且可以像元组一样解包。这个ActiveState的代码示例很接近,但我认为它使用的是普通字典而不是有序字典。http://code.activestate.com/recipes/500261/ - Alexander
11
可变的命名元组称为类。 - gbtimmon
显示剩余5条评论
14个回答

166
有一个可变的替代方案可以使用collections.namedtuple - recordclass。 可以从PyPI安装它:
pip3 install 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)

recordclass(自0.5版本起)支持类型提示:

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)

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

Recordclass库现在提供了另一种变体--recordclass.make_dataclass工厂函数。它支持类似数据类的API(有模块级函数updatemakereplace,而不是self._updateself._replaceself._asdictcls._make方法)。

from recordclass import dataobject, make_dataclass

Point = make_dataclass('Point', [('x', int), ('y',int)])
Point = make_dataclass('Point', {'x':int, 'y':int})

class Point(dataobject):
   x: int
   y: int

>>> p = Point(1, 2)
>>> p
Point(x=1, y=2)
>>> p.x = 10; p.y += 3; print(p)
Point(x=10, y=5)

`recordclass`和`make_dataclass`可以生成类,其实例占用的内存比基于`__slots__`的实例少。这对于具有属性值的实例而言可能很重要,这些实例不打算具有引用循环。如果您需要创建数百万个实例,这可能有助于减少内存使用。这里有一个说明性的example

5
我喜欢它。“这个库实际上是命名元组可变替代方案的“概念验证”。 - Alexander
1
相比Antti Haapala的配方和namedlistrecordclass速度较慢,占用更多内存,并且需要C扩展。 - GrantJ
当使用namedtuple和简单的类创建Point = namedtuple('Point', 'x y')时,Jedi可以自动完成属性,而对于recordclass则不是这种情况。如果我使用更长的创建代码(基于RecordClass),那么Jedi就能理解Point类,但不能理解它的构造函数或属性...有没有办法让recordclass与Jedi良好地配合工作? - PhilMacKay
我不太确定,但似乎可以在Jedi的一侧完成这个。在namedtuple的情况下,已经完成了这个:https://github.com/davidhalter/jedi/issues/107 - intellimath
对于 Python 新手,您可能想要添加此模块的安装说明:pip3 install recordclass。当我遇到导入错误时,我最初远离了这个解决方案,并决定首先查看 Python 自带的解决方案。其他人可能会在安装新模块等相对简单的事情上卡住。 - Gabriel Staples
显示剩余2条评论

75

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)

2
我已经寻找这样的东西多年了。这是一个很好的替代点分字典库,比如dotmap。 - axwell
3
这个需要更多的赞。它正是原帖所寻找的,而且它在标准库中,使用起来非常简单。谢谢! - Tom Zych
6
-1 OP在他的测试中非常清楚地表明了他需要什么,而SimpleNamespace未通过测试6-10(通过索引访问、迭代解包、迭代、有序字典、原地替换)和12、13(字段、插槽)。请注意,文档(您在答案中提供的链接)明确指出:“SimpleNamespace可能作为class NS: pass的替代品很有用。但是,对于结构化记录类型,请使用namedtuple()。” - Ali
3
同样地,SimpleNamespace 创建的是对象而不是类构造函数,并且不能替代 namedtuple。类型比较将无法工作,而内存占用会更高。 - RedGlyph

73
自Python-3.7以来,dataclasses模块作为对此任务的Python替代方案,不仅表现得像可变的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)

2
在 OP 中的测试非常清楚地表明了需要什么,而 dataclass 在 Python 3.7.1 中未通过测试6-10(通过索引访问、迭代解包、迭代、有序字典、原地替换)和12、13(字段、插槽)。 - Ali
10
虽然这可能不是原帖作者要求的具体内容,但它确实对我有所帮助 :) - Martin CR

26
截止到2016年1月11日,最新版本的 namedlist 1.7 已经通过了您对Python 2.7和Python 3.5的所有测试。与之相比,recordclass 是一种基于 C 扩展的实现,而这是一个纯 Python 实现。当然,是否需要使用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: Point(x=10, y=12)
3. 表示 Point(x=10, y=12)
4. 大小 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: Point(x=100,y=200)
11. Pickle 和 Unpickle 成功进行了 Pickle 12. 字段 p: ('x','y')
13. Slots p: ('x','y')

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

请注意我已更改您的测试 10,用于原地替换。 namedlist 有一个 _replace() 方法,它执行浅复制,这对我来说很合理,因为标准库中的 namedtuple 表现方式相同。更改 _replace() 方法的语义会令人困惑。在我看来,应该使用 _update() 方法进行原地更新。或者也许我没有理解您的测试 10 的意图?


这里有一个重要的细微差别。 namedlist 存储在列表实例中的值。问题在于,cpythonlist 实际上是一个动态数组。按设计,它分配比必要更多的内存,以使列表的变异更加便宜。 - intellimath
1
@intellimath namedlist 是一个有点误导的名称。它实际上并不继承自list,默认情况下使用__slots__优化。当我测量时,Python 2.7上6个字段的内存使用量比recordclass少:96字节与104字节。 - GrantJ
2
匿名的负评对任何人都没有帮助。回答有什么问题?为什么要点踩? - Ali
我喜欢types.SimpleNamespace相对于打错字提供的安全性。不幸的是,pylint不喜欢它 :-( - xverges
@xverges,根据他的测试,OP非常清楚他需要什么,而SimpleNamespace在测试6-10(通过索引访问、迭代展开、迭代、有序字典、原地替换)和12、13(字段、插槽)中失败了。请注意,文档(链接在该答案中)明确表示:“SimpleNamespace可能作为class NS: pass的替代品很有用。但是,对于结构化记录类型,请使用namedtuple()。”我接受你的使用情况下SimpleNamespace是一个不错的解决方案,但它不是OP的答案;recordclassnamedlist才是。 - Ali
显示剩余7条评论

24

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

下面的方法非常接近,但它并不是严格可变的。这将使用更新后的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__是很好的选择,因为它在内存效率方面表现良好。唯一的缺点是不能创建新的类属性。
以下是一个相关的线程,说明了内存效率 - 字典 vs 对象 - 哪个更有效率,为什么? 在这个线程的回答中引用的内容是关于__slots__更节省内存的一个非常简明的解释 - Python slots

1
接近,但不够优雅。假设我想要进行 a += 赋值操作,那么我需要这样做:p._replace(x = p.x + 10),而不是 p.x += 10。 - Alexander
1
是的,它并不真正改变现有的元组,而是创建了一个新实例。 - kennes

10
以下是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)

在继续操作之前,它会检查属性是否有效。

那么这个可以进行pickle吗?是的,但只有在你执行以下操作时才可以:

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

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


如果您添加了一个带有 for k in self._attrs_: yield getattr(self, k)__iter__ 方法,那么它将支持像元组一样的解包。 - snapshoe
添加 __len____getitem____setitem__ 方法以支持通过索引获取值(如 p[0])也非常容易。有了这些最后的部分,这似乎是最完整和正确的答案(至少对我来说是这样)。 - snapshoe
__len____iter__很好。__getitem____setitem__可以映射到self.__dict__.__setitem__self.__dict__.__getitem__ - MadMan2064

3

元组是不可变的。

然而,您可以创建一个字典子类,在这个子类中,您可以使用点符号访问属性;

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}

3

如果你想要类似于namedtuple但是可变的行为,试试使用namedlist

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


谢谢提供链接。这似乎是目前为止最接近的,但我需要更详细地评估它。顺便说一下,我完全知道元组是不可变的,这就是为什么我正在寻找类似于命名元组的解决方案。 - Alexander

2

我简直不敢相信之前没有人说过这个,但在我看来,Python只是想让你编写自己的简单可变类,而不是在需要"namedtuple"可变时使用namedtuple

快速总结

直接跳到下面的方法5。它简短明了,远比其他选项好。

各种详细方法:

方法1(好):带有__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()

这是输出结果:

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

这与namedtuple非常相似,但它是完全可变的,不像namedtuple。此外,namedtuple不可调用,因此要查看其内容,只需在对象实例名称后面不带括号(如下面示例中的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的行为。定义__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

这是输出结果:

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

方法三(更好,但使用起来有点别扭):将其变为可调用函数,返回一个 (x, y) 元组

原帖作者(OP)还希望像这样的方法也能够工作(请参见他在我的回答下方的评论):

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

好吧,为了简单起见,我们只需让它起作用:

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

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

顺便说一下,让我们也把这个压缩一下:

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__() 方法。在这里阅读更多关于这些内容的信息:

  1. https://treyhunner.com/2018/06/how-to-make-an-iterator-in-python/
  2. 如何构建基本迭代器?
  3. https://docs.python.org/3/library/exceptions.html#StopIteration

以下是解决方案:

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

方法五(使用此方法)(完美!- 最佳和最干净/最短的方法):将类作为可迭代对象,使用 yield 生成器关键字

请参考以下资料:

  1. https://treyhunner.com/2018/06/how-to-make-an-iterator-in-python/
  2. "yield" 关键字是什么?

这里是解决方案。它依赖于一种高级的“可迭代生成器”(也称为“生成器”)关键字/Python 机制,称为 yield

基本上,当可迭代对象第一次调用下一个项目时,它会调用__iter__()方法,并停止并返回第一个yield调用的内容(在下面的代码中为self.x)。下一次可迭代对象调用下一个项目时,它将从上次离开的地方继续(在这种情况下是第一个yield之后),并查找下一个yield,停止并返回该yield调用的内容(在下面的代码中为self.y)。每个yield的“返回”实际上返回一个“生成器”对象,它本身就是一个可迭代对象,因此您可以对其进行迭代。每个新的可迭代调用下一个项目都会继续这个过程,在最近调用的yield之后重新启动,直到没有更多的yield调用存在,此时迭代结束并且可迭代已完全迭代。因此,一旦这个可迭代对象调用了两个对象,两个yield调用都已用完,因此迭代器结束。最终结果是,像Approach 4中一样,这样的调用可以完美地工作,但要写的代码要少得多!

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

这是解决方案(部分解决方案也可以在上面的treyhunner.com引用中找到)。请注意解决方案是多么简短和干净!

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

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

使用描述性的文档字符串:

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 __iter__(self):
        """
        Make this `Point` class an iterable. When used as an iterable, it will
        now return `self.x` and `self.y` as the two elements of a list-like, 
        iterable object, "generated" by the usages of the `yield` "generator" 
        keyword.
        """
        yield self.x
        yield self.y

复制并粘贴与前面方法(方法4)完全相同的测试代码,您将得到与上面完全相同的输出结果!

参考资料:

  1. https://docs.python.org/3/library/collections.html#collections.namedtuple(Python 3.8.10文档)
  2. 方法1:
    1. __init__和__call__之间的区别是什么?(stackoverflow.com)
  3. 方法2:
    1. https://www.tutorialspoint.com/What-does-the-repr-function-do-in-Python-Object-Oriented-Programming(tutorialspoint.com)
    2. __repr__方法的目的是什么?(stackoverflow.com)
    3. https://docs.python.org/3/reference/datamodel.html#object.__repr__(Python 3.8.10文档)
  4. 方法4:
    1. *****[精彩!] https://treyhunner.com/2018/06/how-to-make-an-iterator-in-python/(treyhunner.com)
    2. 如何构建基本迭代器?(realpython.com)
    3. https://docs.python.org/3/library/exceptions.html#StopIteration(Python 3.8.10文档)
  5. 方法5:
    1. 查看方法4中的链接,以及:
    2. *****[精彩!] "yield"关键字是什么意思?(stackoverflow.com)
  6. 对象名称前的单下划线和双下划线的含义是什么?(stackoverflow.com)

1
这个解决方案与@kennes在2015年发布的解决方案类似。原始问题经过多年的大幅编辑,但其中一个要求是元组拆包,例如x,y = Point(x = 1,y = 2)。另外,使用__repr__而不是__call__是否更简单? - Alexander
@Alexander,感谢您指出__repr__()方法。我以前不熟悉它。我已将其添加到我的答案中。我大大改进和扩展了我的答案,添加了第2到第5种方法,以解决您的元组拆包要求。第5种方法是最好的。根据我的测试,它和第4种方法现在都可以完美地实现,就我所知。 - Gabriel Staples
@Alexander,我看到您现在比提问时多了6年的Python经验,并且总体上对Python有很多经验,而我仍然需要学习更多关于Python的东西。您现在对这个问题的规范解决方案是什么?当您需要一个可变的namedtuple时,您的首选解决方案是什么?您认为我的答案中第5种方法怎么样? - Gabriel Staples
我会先查看recordclass https://pypi.org/project/recordclass/。本周稍后我会尝试更详细地审查您的回复。 - Alexander

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接