前两天重构一段 C# 代码,需要交换两个变量的值:
var temp = a;a = b;b = temp;
写完之后突然想起来 Python 的写法:
a, b = b, a
三行变一行,连临时变量都不需要。这种"语法级的优雅",C# 到现在都没追上。
今天聊聊 Python 的元组和解包——第一阶段的收官之作。
交换变量:最经典的解包场景
C# 交换变量必须用临时变量:
int x = 1, y = 2;int temp = x;x = y;y = temp;
Python 一行搞定:
x, y = 1, 2x, y = y, x # x=2, y=1
原理是:右边先计算 y, x 得到元组 (2, 1),然后左边按位置解包赋值。
这个特性在排序、算法实现中非常常用——比如按分数排序的同时保留原始索引:
scores = [("Alice", 95), ("Bob", 87), ("Charlie", 92)]# 按分数降序排序sorted_scores = sorted(scores, key=lambda x: x[1], reverse=True)
C# 要实现同样效果,得用 Tuple 或者自定义类。
函数返回多个值:告别 out 参数
C# 的 out 参数是出了名的啰嗦:
publicstaticvoidDivide(int a, int b, outint quotient, outint remainder){ quotient = a / b; remainder = a % b;}// 调用int q, r;Divide(10, 3, out q, out r);
Python 直接 return 两个值:
defdivide(a, b):return a // b, a % bquotient, remainder = divide(10, 3)
更进一步,Python 可以返回任意数量的值:
defanalyze_scores(scores):return len(scores), sum(scores) / len(scores), max(scores), min(scores)count, avg, high, low = analyze_scores([85, 92, 78, 95, 88])
C# 要实现同样功能,得定义一个专门的返回类型,或者用元组:
public (int Count, double Avg, int High, int Low) AnalyzeScores(int[] scores){return (scores.Length, scores.Average(), scores.Max(), scores.Min());}var (count, avg, high, low) = AnalyzeScores(new[] { 85, 92, 78, 95, 88 });
Python 的 return a, b, c 更自然,像是"一句话说完所有结果"。
解包的高级用法:星号的魔力
Python 的解包不只是简单的"一一对应",星号 * 让它变得极其灵活:
# 基础解包a, b, c = 1, 2, 3# 忽略不需要的值_, name, score = 1, "Alice", 95# _ 是约定俗成的"忽略"变量# 收集多余的值first, *rest = [1, 2, 3, 4]# first=1, rest=[2, 3, 4]# 收集中间的值first, *middle, last = [1, 2, 3, 4, 5]# first=1, middle=[2, 3, 4], last=5
C# 7+ 也有解构,但没有 *rest 这种灵活语法:
// C# 解构var (a, b, c) = (1, 2, 3);var (_, name, score) = (1, "Alice", 95);// 没有 *rest 语法
这个特性在处理不定长数据时特别有用——比如从日志文件中提取首尾行:
lines = log_content.split("\n")first_line, *middle_lines, last_line = lines
C# 得写成:
var lines = logContent.Split("\n");var firstLine = lines[0];var lastLine = lines[^1];var middleLines = lines[1..^1];
嵌套解构:处理复杂数据结构
Python 支持嵌套解构——直接从嵌套的元组或列表中提取值:
# 嵌套元组解构students = [("Alice", 95), ("Bob", 87), ("Charlie", 92)]for name, score in students: print(f"{name}: {score}")# 更复杂的嵌套data = {"Alice": [95, 88, 92], "Bob": [87, 90, 85]}for name, (s1, s2, s3) in data.items(): avg = (s1 + s2 + s3) / 3 print(f"{name} 平均分: {avg}")
C# 要实现同样效果:
var students = new[] { ("Alice", 95), ("Bob", 87), ("Charlie", 92) };foreach (var (name, score) in students){ Console.WriteLine($"{name}: {score}");}
Python 的嵌套解构更灵活,可以直接从字典的值(列表)中解构。
命名元组:让元组更有可读性
普通元组用索引访问不够直观:
person = ("Alice", 25, "alice@example.com")print(person[0]) # ???这是啥?
命名元组解决了这个问题:
from collections import namedtuplePerson = namedtuple("Person", ["name", "age", "email"])p = Person("Alice", 25, "alice@example.com")print(p.name) # "Alice"print(p.age) # 25print(p[0]) # "Alice"(也可以用索引)
Python 3.6+ 还有类型注解版本:
from typing import NamedTupleclassTypedPerson(NamedTuple): name: str age: int email: str
C# 的 ValueTuple 也能命名:
var person = (Name: "Alice", Age: 25, Email: "alice@example.com");Console.WriteLine(person.Name);
但 Python 的 namedtuple 还支持索引访问——两种方式混用,灵活度更高。
*args 和 **kwargs:万能参数
C# 的 params 只能接收固定类型的可变参数:
publicstaticintSum(paramsint[] numbers){return numbers.Sum();}
Python 的 *args 和 **kwargs 更强大:
# *args:接收任意数量的位置参数deffunc(*args): print(args) # 是一个元组func(1, 2, 3) # (1, 2, 3)# **kwargs:接收任意数量的关键字参数deffunc2(**kwargs): print(kwargs) # 是一个字典func2(name="Alice", age=25) # {'name': 'Alice', 'age': 25}# 混合使用deffunc3(a, b, *args, **kwargs): print(f"a={a}, b={b}") print(f"args={args}") print(f"kwargs={kwargs}")func3(1, 2, 3, 4, x=10, y=20)# a=1, b=2# args=(3, 4)# kwargs={'x': 10, 'y': 20}
更厉害的是解包传参:
defadd(a, b, c):return a + b + cargs = [1, 2, 3]print(add(*args)) # 6kwargs = {'a': 1, 'b': 2, 'c': 3}print(add(**kwargs)) # 6
C# 没有 **kwargs 的等价物——如果你想传递动态的命名参数,得用 IDictionary<string, object> 然后手动处理。
元组是不可变的:这点要记住
Python 的元组创建后不能修改:
t = (1, 2, 3)# t[0] = 999 # TypeError: 'tuple' does not support item assignment
如果你需要修改,得创建新元组:
t = (1, 2, 3)t = (999,) + t[1:] # (999, 2, 3)
这点和 C# 的 ValueTuple 不同——C# 的 ValueTuple 是值类型,可以直接赋值,但 Python 的元组是不可变序列。
单元素元组的坑
这是 Python 新手最容易犯的错:
t1 = (1) # 这是 int,不是 tuple!t2 = (1,) # 这才是 tuplet3 = 1, # 这也是 tuple(逗号是关键)
() 在 Python 中是空元组,(1) 只是带括号的整数。必须加逗号才是元组。
C# 没有这个问题——(1) 就是元组。
我的真实感受
Python 的解包语法是我从 C# 转过来后,最舍不得放弃的特性。
x, y = y, x 交换变量、return a, b 返回多值、*args 万能参数——这些在 C# 里要么写起来啰嗦,要么根本做不到。
尤其是 *rest 收集多余值的语法,让我处理日志、解析数据时省了很多代码。
元组和解包是 Python 的"语法级优雅"——用最少的代码表达最清晰的意图。掌握它们,你的 Python 代码会简洁一个档次。
第一阶段到这里结束。下一篇进入第二阶段,聊聊函数定义——Python 的 def 比 C# 的方法声明简洁太多,但也有很多陷阱等着你。