我简直不敢相信之前没有人说过这个,但在我看来,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)()
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)
x, y = Point(1, 2)
p1 = Point(1, 2)
x, y = p1
让我们摆脱 __call__()
方法,但为了使这个类成为迭代器,我们将添加 __iter__()
和 __next__()
方法。在这里阅读更多关于这些内容的信息:
- https://treyhunner.com/2018/06/how-to-make-an-iterator-in-python/
- 如何构建基本迭代器?
- 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
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
生成器关键字
请参考以下资料:
- https://treyhunner.com/2018/06/how-to-make-an-iterator-in-python/
- "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)
x, y = Point(1, 2)
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)完全相同的测试代码,您将得到与上面完全相同的输出结果!
参考资料:
- https://docs.python.org/3/library/collections.html#collections.namedtuple(Python 3.8.10文档)
- 方法1:
- __init__和__call__之间的区别是什么?(stackoverflow.com)
- 方法2:
- https://www.tutorialspoint.com/What-does-the-repr-function-do-in-Python-Object-Oriented-Programming(tutorialspoint.com)
- __repr__方法的目的是什么?(stackoverflow.com)
- https://docs.python.org/3/reference/datamodel.html#object.__repr__(Python 3.8.10文档)
- 方法4:
- *****[精彩!] https://treyhunner.com/2018/06/how-to-make-an-iterator-in-python/(treyhunner.com)
- 如何构建基本迭代器?(realpython.com)
- https://docs.python.org/3/library/exceptions.html#StopIteration(Python 3.8.10文档)
- 方法5:
- 查看方法4中的链接,以及:
- *****[精彩!] "yield"关键字是什么意思?(stackoverflow.com)
- 对象名称前的单下划线和双下划线的含义是什么?(stackoverflow.com)
namedtuple
不同的是,似乎您无需能够通过索引引用属性,即p[0]
和p[1]
将是分别引用x
和y
的替代方式,对吗? - martineau