Python元组常被说成只读列表,但真正区别在于结构不可变带来的哈希性和函数默认值安全性。本文从创建、浅不可变、字典键/集合元素、namedtuple、性能对比和默认参数陷阱几个方面梳理可落地用法。
一、创建与单元素逗号陷阱
元组可用圆括号、逗号打包、tuple()构造。关键细节:定义元组的是逗号,不是括号。单元素必须写(42,),(42)只是整数。- t1 = (1, 2, 3)
- t2 = 1, 2, 3
- t3 = tuple([1, 2, 3])
- t4 = tuple('hello')
- t5 = tuple(range(5))
- single = (42,)
- not_tuple = (42)
- print(type(single)) # <class 'tuple'>
- print(type(not_tuple)) # <class 'int'>
复制代码 索引、切片、len、in、count、index、拼接和重复与列表类似,切片和拼接会返回新元组。
二、不可变性的边界:结构不可变,不是深不可变
对元组执行t[0]=100会报TypeError,append/remove/clear/sort/reverse会报AttributeError。元组保存的是对象引用;引用关系不能替换,但引用指向的列表内容可改。这就是浅不可变。- t = ([1, 2], [3, 4], 'hello')
- t[0].append(999)
- t[1][1] = 444
- print(t) # ([1, 2, 999], [3, 444], 'hello')
- x = [1, 2, 3]
- t2 = (x, 'hello')
- x.append(4)
- print(t2) # ([1, 2, 3, 4], 'hello')
- x = [5, 6, 7]
- print(t2) # ([1, 2, 3, 4], 'hello')
复制代码 排查时先区分是替换元组元素,还是修改元组内可变对象。前者必然失败,后者合法。
三、为什么能作为字典键和集合元素
不可变并满足哈希要求,使元组可作为复合键。列表做键会触发TypeError: unhashable type: 'list'。坐标、缓存键、访问路径集合是典型场景。- grid = {}
- grid[(0, 0)] = '起点'
- grid[(5, 3)] = '宝藏'
- print(grid[(5, 3)]) # 宝藏
- cache = {}
- def expensive_computation(x, y, z):
- key = (x, y, z)
- if key in cache:
- return cache[key]
- result = x * y + z
- cache[key] = result
- return result
- visited = set()
- def visit(x, y):
- point = (x, y)
- if point in visited:
- print(f'({x},{y})已访问过')
- else:
- visited.add(point)
- print(f'首次访问({x},{y})')
复制代码
四、函数返回多值与参数解包
return a,b,c会自动打包元组;*args收集位置参数也是元组;用*可把元组解包成位置参数。函数默认值用元组可避免可变默认参数共享。- def min_max_avg(numbers):
- return min(numbers), max(numbers), sum(numbers) / len(numbers)
- lowest, highest, average = min_max_avg([85, 92, 78, 95, 88])
- quotient, remainder = divmod(17, 5)
- def log_all(*messages):
- print(type(messages)) # <class 'tuple'>
- for msg in messages:
- print(f'[LOG] {msg}')
- def calculate(a, b, c):
- return a + b * c
- params = (2, 3, 4)
- print(calculate(*params))
复制代码 可变默认参数陷阱:def add_item_bad(item, items=[])会在多次调用间共享列表;推荐items=None并在函数内新建列表,或用元组默认值表达只读集合。原文示例add_item_good体现互不影响。
五、namedtuple:可读性和元组语义兼得
namedtuple可按属性访问、按索引访问、_asdict转字典、_replace创建新实例、_fields查看字段、_make从可迭代创建。适合数据库行、配置、颜色等记录型数据。- from collections import namedtuple
- Point = namedtuple('Point', ['x', 'y'])
- p1 = Point(3, 5)
- p2 = Point(x=10, y=20)
- print(p1.x, p1.y)
- print(p1[0], p1[1])
- print(p1._asdict())
- p3 = p1._replace(x=100)
- print(p1)
- print(p3)
- print(Point._fields)
- print(Point._make([7, 8]))
- Config = namedtuple('Config', ['host', 'port', 'debug', 'timeout'])
- dev_config = Config(host='localhost', port=8080, debug=True, timeout=30)
- prod_config = dev_config._replace(host='prod.example.com', debug=False)
复制代码
六、性能与内存:创建更快、占用更少
元组不需要为未来append预留容量,因此通常比列表创建快、内存少。可用time.perf_counter和sys.getsizeof验证。比较值用==,不要依赖is判断缓存。- import time, sys
- start = time.perf_counter()
- for _ in range(1000000):
- lst = [1, 2, 3, 4, 5]
- print(f'创建列表100万次: {time.perf_counter() - start:.3f}秒')
- start = time.perf_counter()
- for _ in range(1000000):
- tup = (1, 2, 3, 4, 5)
- print(f'创建元组100万次: {time.perf_counter() - start:.3f}秒')
- print(sys.getsizeof([1, 2, 3, 4, 5]))
- print(sys.getsizeof((1, 2, 3, 4, 5)))
复制代码
七、选择原则和转换
需要增删改、运行时长度变化、使用append/extend等方法时用列表;固定记录、常量集合、字典键/集合元素、函数多返回值、性能敏感场景用元组。列表和元组可用list()/tuple()互转,转换会创建新对象;可先用列表收集,返回前tuple(results)锁定。- ALLOWED_METHODS = ('GET', 'POST', 'PUT', 'DELETE')
- def handle_request(method):
- if method not in ALLOWED_METHODS:
- raise ValueError(f'不支持的请求方法: {method}')
- print(f'处理 {method} 请求...')
- def collect_results():
- results = []
- for i in range(10):
- results.append(i ** 2)
- return tuple(results)
复制代码 总结:元组不是列表的只读版本,而是带哈希性、低开销和意图表达能力的独立序列类型。理解浅不可变边界,能避免把可变对象放进元组后误以为内容不可改;在需要复合键和只读记录时优先元组,在需要动态增删时用列表。 |