查看: 55|回复: 3

Python元组不可变特性与namedtuple实战

[复制链接]
发表于 半小时前 | 显示全部楼层 |阅读模式
Python元组常被说成只读列表,但真正区别在于结构不可变带来的哈希性和函数默认值安全性。本文从创建、浅不可变、字典键/集合元素、namedtuple、性能对比和默认参数陷阱几个方面梳理可落地用法。

一、创建与单元素逗号陷阱
元组可用圆括号、逗号打包、tuple()构造。关键细节:定义元组的是逗号,不是括号。单元素必须写(42,),(42)只是整数。
  1. t1 = (1, 2, 3)
  2. t2 = 1, 2, 3
  3. t3 = tuple([1, 2, 3])
  4. t4 = tuple('hello')
  5. t5 = tuple(range(5))
  6. single = (42,)
  7. not_tuple = (42)
  8. print(type(single))  # <class 'tuple'>
  9. print(type(not_tuple))  # <class 'int'>
复制代码
索引、切片、len、in、count、index、拼接和重复与列表类似,切片和拼接会返回新元组。

二、不可变性的边界:结构不可变,不是深不可变
对元组执行t[0]=100会报TypeError,append/remove/clear/sort/reverse会报AttributeError。元组保存的是对象引用;引用关系不能替换,但引用指向的列表内容可改。这就是浅不可变。
  1. t = ([1, 2], [3, 4], 'hello')
  2. t[0].append(999)
  3. t[1][1] = 444
  4. print(t)  # ([1, 2, 999], [3, 444], 'hello')
  5. x = [1, 2, 3]
  6. t2 = (x, 'hello')
  7. x.append(4)
  8. print(t2)  # ([1, 2, 3, 4], 'hello')
  9. x = [5, 6, 7]
  10. print(t2)  # ([1, 2, 3, 4], 'hello')
复制代码
排查时先区分是替换元组元素,还是修改元组内可变对象。前者必然失败,后者合法。

三、为什么能作为字典键和集合元素
不可变并满足哈希要求,使元组可作为复合键。列表做键会触发TypeError: unhashable type: 'list'。坐标、缓存键、访问路径集合是典型场景。
  1. grid = {}
  2. grid[(0, 0)] = '起点'
  3. grid[(5, 3)] = '宝藏'
  4. print(grid[(5, 3)])  # 宝藏
  5. cache = {}
  6. def expensive_computation(x, y, z):
  7.     key = (x, y, z)
  8.     if key in cache:
  9.         return cache[key]
  10.     result = x * y + z
  11.     cache[key] = result
  12.     return result
  13. visited = set()
  14. def visit(x, y):
  15.     point = (x, y)
  16.     if point in visited:
  17.         print(f'({x},{y})已访问过')
  18.     else:
  19.         visited.add(point)
  20.         print(f'首次访问({x},{y})')
复制代码

四、函数返回多值与参数解包
return a,b,c会自动打包元组;*args收集位置参数也是元组;用*可把元组解包成位置参数。函数默认值用元组可避免可变默认参数共享。
  1. def min_max_avg(numbers):
  2.     return min(numbers), max(numbers), sum(numbers) / len(numbers)
  3. lowest, highest, average = min_max_avg([85, 92, 78, 95, 88])
  4. quotient, remainder = divmod(17, 5)
  5. def log_all(*messages):
  6.     print(type(messages))  # <class 'tuple'>
  7.     for msg in messages:
  8.         print(f'[LOG] {msg}')
  9. def calculate(a, b, c):
  10.     return a + b * c
  11. params = (2, 3, 4)
  12. print(calculate(*params))
复制代码
可变默认参数陷阱:def add_item_bad(item, items=[])会在多次调用间共享列表;推荐items=None并在函数内新建列表,或用元组默认值表达只读集合。原文示例add_item_good体现互不影响。

五、namedtuple:可读性和元组语义兼得
namedtuple可按属性访问、按索引访问、_asdict转字典、_replace创建新实例、_fields查看字段、_make从可迭代创建。适合数据库行、配置、颜色等记录型数据。
  1. from collections import namedtuple
  2. Point = namedtuple('Point', ['x', 'y'])
  3. p1 = Point(3, 5)
  4. p2 = Point(x=10, y=20)
  5. print(p1.x, p1.y)
  6. print(p1[0], p1[1])
  7. print(p1._asdict())
  8. p3 = p1._replace(x=100)
  9. print(p1)
  10. print(p3)
  11. print(Point._fields)
  12. print(Point._make([7, 8]))
  13. Config = namedtuple('Config', ['host', 'port', 'debug', 'timeout'])
  14. dev_config = Config(host='localhost', port=8080, debug=True, timeout=30)
  15. prod_config = dev_config._replace(host='prod.example.com', debug=False)
复制代码

六、性能与内存:创建更快、占用更少
元组不需要为未来append预留容量,因此通常比列表创建快、内存少。可用time.perf_counter和sys.getsizeof验证。比较值用==,不要依赖is判断缓存。
  1. import time, sys
  2. start = time.perf_counter()
  3. for _ in range(1000000):
  4.     lst = [1, 2, 3, 4, 5]
  5. print(f'创建列表100万次: {time.perf_counter() - start:.3f}秒')
  6. start = time.perf_counter()
  7. for _ in range(1000000):
  8.     tup = (1, 2, 3, 4, 5)
  9. print(f'创建元组100万次: {time.perf_counter() - start:.3f}秒')
  10. print(sys.getsizeof([1, 2, 3, 4, 5]))
  11. print(sys.getsizeof((1, 2, 3, 4, 5)))
复制代码

七、选择原则和转换
需要增删改、运行时长度变化、使用append/extend等方法时用列表;固定记录、常量集合、字典键/集合元素、函数多返回值、性能敏感场景用元组。列表和元组可用list()/tuple()互转,转换会创建新对象;可先用列表收集,返回前tuple(results)锁定。
  1. ALLOWED_METHODS = ('GET', 'POST', 'PUT', 'DELETE')
  2. def handle_request(method):
  3.     if method not in ALLOWED_METHODS:
  4.         raise ValueError(f'不支持的请求方法: {method}')
  5.     print(f'处理 {method} 请求...')
  6. def collect_results():
  7.     results = []
  8.     for i in range(10):
  9.         results.append(i ** 2)
  10.     return tuple(results)
复制代码
总结:元组不是列表的只读版本,而是带哈希性、低开销和意图表达能力的独立序列类型。理解浅不可变边界,能避免把可变对象放进元组后误以为内容不可改;在需要复合键和只读记录时优先元组,在需要动态增删时用列表。
回复

使用道具 举报

发表于 26 分钟前 | 显示全部楼层

Re: Python元组不可变特性与namedtuple实战

感谢分享,整体梳理得很系统,尤其是“结构不可变不等于深不可变”这一点,很多教程一笔带过,真踩过坑才知道排查时多容易懵。字典键和集合元素那部分也很实用,复合键缓存是元组的经典场景。 补充两个小点:namedtuple 在 3.7+ 支持 defaults 参数,可以给字段设默认值,写配置类时更方便;如果更在意类型标注和静态检查,也可以看看 typing.NamedTuple。另外性能对比那块,循环里直接 time.perf_counter 容易受解释器优化和循环开销影响,用 timeit 对比会更稳一些。 想请教一下,实际项目里你一般怎么权衡 namedtuple 和 dataclass 的选择?是看数据是否需要可变、还是要不要序列化语义?
回复 支持 反对

使用道具 举报

发表于 26 分钟前 | 显示全部楼层

Re: Python元组不可变特性与namedtuple实战

感谢楼主分享,这篇梳理得很系统,尤其是把“结构不可变”和“浅不可变”区分开,对实际排查问题很有帮助。单元素逗号陷阱、元组作为字典键和集合元素、函数返回多值与解包、可变默认参数陷阱,以及 namedtuple 的 _replace 和 _asdict 用法都挺落地。个人觉得默认参数那部分特别值得反复提醒,用 None 或元组默认值确实能避免很多隐蔽的共享状态问题。性能对比和 sys.getsizeof 的思路也很实用,回头可以照着验证一下。
回复 支持 反对

使用道具 举报

发表于 26 分钟前 | 显示全部楼层

Re: Python元组不可变特性与namedtuple实战

这篇梳理得很实用,尤其是把“结构不可变”和“浅不可变”分开讲,能避免很多误用。单元素逗号陷阱、元组做复合键、函数返回多值解包、namedtuple 的 _replace 这些点都很落地。 有个小地方可以补充:元组能做字典键或集合元素,前提是元组里每个元素本身都可哈希。像 ([1,2], 'x') 这种虽然元组结构不可变,但因为包含列表,执行 hash 时仍会报 unhashable type。这个边界和第二节的浅不可变正好呼应,实际排查缓存键时挺容易遇到。 另外第三节缓存示例里“cache = result”看起来像是排版漏了,按上下文应该是 cache = result,不然缓存不会生效。默认参数那部分建议也很对,items=None 在函数内新建列表通常最稳,元组默认值更适合表达只读集合。性能部分似乎被截断了,创建速度和内存占用这块如果后续展开,应该会很有参考价值。
回复 支持 反对

使用道具 举报

您需要登录后才可以回帖 登录 | 注册

本版积分规则

指导单位

江苏省公安厅

江苏省通信管理局

浙江省台州刑侦支队

DEFCON GROUP 86025

Hacking Group 021A

旗下站点

态势感知中心

应急响应中心

红盟安全

联系我们

官方QQ群:112851260

官方邮箱:security#ihonker.org(#改成@)

官方核心成员

关注微信公众号

Archiver|手机版|小黑屋| ( 沪ICP备2021026908号 )

GMT+8, 2026-11-9 03:31 , Processed in 0.022842 second(s), 28 queries , Gzip On.

Powered by ihonker.com

Copyright © 2015-现在.

  • 返回顶部