跳转到内容
新建笔记

Python 元组:不可变槽位、解包与数据传递

元组 tuple 是不可变序列:创建后不能替换、增加或删除其中的位置,但这些位置引用的对象仍可能是可变的。元组常用于把多个相关值作为一个对象传递;逗号负责打包,圆括号主要负责分组和消除语法歧义。

本文以 Python 3.11 为基线。每段程序可独立运行,示例断言需要在未启用 -O 的情况下执行。

创建、索引、连接与重复

跳转到“创建、索引、连接与重复”

空元组写作 ();单元素元组需要尾随逗号,如 (1,) 或 1,。单独的 (1) 仍是整数表达式。

empty = ()
single = (1,)
pair = 2, 3
assert type(empty) is tuple and type(single) is tuple
assert (1) == 1 and type((1)) is int
assert pair == (2, 3)
assert tuple([1, 2]) == (1, 2)
values = (1, 2, 2, 3)
assert values[0] == 1 and values[-1] == 3
assert values[1:3] == (2, 2) and values[99:] == ()
assert values.count(2) == 2 and values.index(2) == 1
assert values.index(2, 2) == 2
assert len(values) == 4
assert 2 in values and "2" not in values
assert (1, 2, 3) + ("a", "b") == (1, 2, 3, "a", "b")
assert 3 * (1, 2) == (1, 2, 1, 2, 1, 2)
assert (1, 2) * 0 == ()
assert tuple(reversed(values)) == (3, 2, 2, 1)
assert (1, 2) < (1, 3) and (1, 2) < (1, 2, 0)
for operation, expected in [
(lambda: values[99], IndexError),
(lambda: values.index(99), ValueError),
(lambda: (1, "two") < (1, 2), TypeError),
]:
try:
operation()
except expected:
pass
else:
raise AssertionError("invalid tuple operation succeeded")
print("tuple construction and sequence checks passed")

连接和重复计算结果,不会原地更改原元组。不要进一步推断“每次都分配不同身份的元组”:例如 tuple(existing_tuple) 可以直接返回原元组。元组之间的排序仍取决于对应元素是否能比较;它与列表一样使用字典序。

不可变位置与可变内容

跳转到“不可变位置与可变内容”

下面的元组不能把第一个位置替换掉,却可以通过该位置拿到列表后修改列表。哈希还要求每个成员都可哈希,因此“外层是元组”不足以保证可以作为字典键。

item = ([1, 2], "sensor")
item[0].append(3)
assert item == ([1, 2, 3], "sensor")
try:
item[0] = []
except TypeError:
pass
else:
raise AssertionError("tuple slot assignment succeeded")
try:
hash(item)
except TypeError:
pass
else:
raise AssertionError("tuple containing a list became hashable")
key = ("sensor", 3)
mapping = {key: "ready"}
assert mapping[("sensor", 3)] == "ready"
original = (1, 2)
alias = original
original += (3,)
assert original == (1, 2, 3) and alias == (1, 2)
del original
assert alias == (1, 2)
def outputs(values):
return (*values,)
packed = outputs([10, 20])
assert type(packed) is tuple and packed == (10, 20)
first, second = packed
assert first == 10 and second == 20
head, *tail = (1, 2, 3)
assert head == 1 and tail == [2, 3]
assert outputs(iter([1, 2])) == (1, 2)
print("immutability, hash and packing checks passed")

del original 删除的是名称绑定,其他引用仍可访问对象。return (*values,) 先展开可迭代对象,再构建并返回一个元组;不是向调用方返回若干彼此独立的返回对象。调用方的赋值拆包是另一项操作。

原 forward 片段中的星号返回

跳转到“原 forward 片段中的星号返回”

原笔记使用 forward(self, raw) 展示 return (*x_out,),并依次调用 m_apply、注意力块、reshape、上采样、求和和归一化激活。原片段没有提供这些网络部件的定义,不能据此还原实际模型。

下面保留相同的数据流名称,补成纯 Python 的标量演示;其中加法和乘法只是用于观察容器传递,不实现真实神经网络层,也不声称验证了原网络。

class TupleFlowDemo:
def __init__(self):
self.patch_avg = lambda value: value + 1
self.avg_map = lambda value: value * 2
self.attention = [lambda values: [value - 1 for value in values]]
self.reshape = lambda value: value
self.upconvs = lambda value: value + 10
self.bn_relu = lambda value: max(0, value)
@staticmethod
def m_apply(values, operation):
return [operation(value) for value in values]
@staticmethod
def m_sum(left, right):
return [a + b for a, b in zip(left, right, strict=True)]
def forward(self, raw):
x = self.m_apply(raw, self.patch_avg)
x = self.m_apply(x, self.avg_map)
for block in self.attention:
x = block(x)
x = [self.reshape(value) for value in x]
x = self.m_apply(x, self.upconvs)
x_out = self.m_sum(x, raw)
x_out = self.m_apply(x_out, self.bn_relu)
return (*x_out,)
model = TupleFlowDemo()
raw = [1, 2]
result = model.forward(raw)
assert result == (14, 17) and type(result) is tuple
assert raw == [1, 2]
assert model.forward([]) == ()
print("starred return flow checks passed")

此例也说明:星号可以用于任何合适的可迭代对象,不要求 x_out 事先就是元组。若输入是只能遍历一次的迭代器,而算法后面还需要再次读取它,应先明确是否保存为容器;该演示的 raw 使用列表。

功能原笔记涉及的名称正确理解
类型、文档与属性__class__、__doc__、__dir__、__getattribute__属性与方法不同;日常使用 type、dir、getattr
创建与大小__new__、__len__、__sizeof__tuple 创建对象;len 数元素;对象内存大小不递归包含所有成员
读取与遍历__getitem__、__contains__、__iter__支持索引、切片、成员判断与迭代
连接与重复__add__、__mul__、__rmul__3 乘二元素元组产生六个元素,原表少写了一组
表示与格式化__repr__、__str__、__format__普通元组不支持直接用 .2f 对每个成员格式化
比较__eq__、__ne__、__ge__、__gt__、__le__、__lt__整数 2 与字符串 "2" 不相等
哈希与重建__hash__、__getnewargs__、__reduce__、__reduce_ex__受成员哈希性及序列化协议约束,常规代码调用 hash 或 pickle
两个公开查询方法count()、index()计数与查找;index 找不到时抛出 ValueError

原表的 __reverse__ 不是正确方法名。对普通元组调用 reversed(t) 即可,它可以通过序列协议反向遍历,不要求元组自己提供 __reversed__。序列规则见 Python 元组文档。

import pickle
import sys
values = (1, 2)
assert values.__class__ is tuple and isinstance(tuple.__doc__, str)
assert "count" in dir(values)
assert values.__getattribute__("__class__") is tuple
assert values.__getnewargs__() == ((1, 2),)
assert not hasattr(values, "__reverse__")
assert format(values, "") == str(values)
try:
format(values, ".2f")
except TypeError:
pass
else:
raise AssertionError("tuple accepted per-number format")
formatted = tuple(format(value, ".2f") for value in values)
assert formatted == ("1.00", "2.00")
assert sys.getsizeof(values) >= values.__sizeof__()
assert isinstance(hash(values), int)
assert pickle.loads(pickle.dumps(values, protocol=2)) == values
print("tuple metadata and formatting checks passed")

pickle 示例只恢复本程序生成的字节,不用于不可信来源;协议细节见特殊方法与对象协议。如果需要按名称表达不同字段,可进一步考虑命名元组或数据类,而不是让读者长期记住每个位置的含义。