37. 内置函数与实用工具
Python 提供了丰富的内置函数(built-in functions),它们始终可用,无需导入任何模块。这些函数构成了日常 Python 编程的基础,帮助你高效地处理数据、序列和集合。在本章中,我们将探索 Python 最有用的内置工具,并学习如何利用它们编写更简洁、更具表达力的代码。
理解 Python 的类型系统
在深入具体的内置函数之前,先了解 Python 如何组织其数据类型会很有帮助。这些知识将帮助你预测哪些操作适用于哪些类型,并在出错时理解错误信息。
Python 的数据类型可以从两个互补的角度来理解:
类型层级:类型之间如何关联
这展示了 Python 如何根据类型“是什么”将它们组织成不同家族。
基于能力的视角:类型能做什么
对于内置函数而言,更重要的是类型“能做什么”:
关键能力:
- 可迭代(iterable):可以用于
for循环 → 可用于sum()、any()、all()、sorted() - 集合(collection):带有
len()的可迭代对象 → 可用于len()和in运算符 - 序列(sequence):支持索引的集合 → 支持
[index]和切片[start:end]
为什么这很重要
内置函数需要特定的能力:
| 函数 | 需要 | 适用于 |
|---|---|---|
len() | 集合 | str, list, dict, set, tuple |
sum() | 数字可迭代对象 | list, tuple, set, range, generator |
sorted() | 可迭代对象 | str, list, dict, set, tuple |
[index] | 序列 | str, list, tuple, range |
理解这些类别可以帮助你:
- 预测哪些函数适用于哪些类型
- 理解诸如 “object is not iterable” 这类错误信息
- 知道什么时候可以索引(
\[0]) vs 什么时候只能循环(for)
37.1) 常见内置函数(len, sum, min, max, abs, round)
Python 最常用的内置函数能帮助你对数据执行常见操作,而无需编写循环或复杂逻辑。这些函数经过优化、可读性强,并构成 Pythonic 代码的基础。
37.1.1) 使用 len() 测量长度
len() 函数返回集合中的元素个数。它适用于字符串、列表(list)、元组(tuple)、字典(dict)、集合(set)以及任何其他集合类型。
# 统计字符串中的字符数
message = "Hello, World!"
print(len(message)) # Output: 13
# 统计列表中的元素数
scores = [85, 92, 78, 90, 88]
print(len(scores)) # Output: 5
# 统计字典中的键值对数量
student = {"name": "Bob", "age": 21, "major": "CS"}
print(len(student)) # Output: 3
# 统计集合中的唯一元素数量
unique_ids = {101, 102, 103, 101, 102} # 重复项会被移除
print(len(unique_ids)) # Output: 3当你在处理数据前需要知道其大小时,len() 尤其有用:
# 根据数据规模处理数据
data = [12, 45, 23, 67, 89, 34]
if len(data) < 5:
print("Not enough data for analysis")
else:
print(f"Analyzing {len(data)} data points") # Output: Analyzing 6 data points
average = sum(data) / len(data)
print(f"Average: {average}") # Output: Average: 45.037.1.2) 使用 sum() 计算总和
sum() 函数会把一个可迭代对象(iterable)中的所有数字加起来。它比编写一个循环来累加值更简洁。
# 对数字列表求和
prices = [19.99, 24.50, 15.75, 32.00]
total = sum(prices)
print(f"Total: ${total}") # Output: Total: $92.24
# 对元组求和
daily_steps = (8500, 10200, 7800, 9500, 11000)
weekly_total = sum(daily_steps)
print(f"Total steps this week: {weekly_total}") # Output: Total steps this week: 47000
# 对 range 求和
total_1_to_100 = sum(range(1, 101))
print(total_1_to_100) # Output: 5050一个将 sum() 和 len() 结合来计算平均值的实用示例:
# 计算测试平均分
test_scores = [88, 92, 79, 85, 90, 87]
total_score = sum(test_scores)
num_tests = len(test_scores)
average_score = total_score / num_tests
print(f"Average score: {average_score:.1f}") # Output: Average score: 86.8重要限制:sum() 只能用于数字。你不能用它来拼接字符串或合并列表:
# 这会引发 TypeError
words = ["Hello", " ", "World"]
# sentence = sum(words) # TypeError: unsupported operand type(s)37.1.3) 使用 min() 和 max() 查找极值
min() 和 max() 函数会找到一个可迭代对象中的最小值和最大值。它们适用于数字、字符串,以及任何可比较的对象。
# 查找最小和最大数字
temperatures = [72, 68, 75, 70, 73, 69]
coldest = min(temperatures)
warmest = max(temperatures)
print(f"Temperature range: {coldest}°F to {warmest}°F")
# Output: Temperature range: 68°F to 75°F
# 查找最小和最大字符串(按字母顺序)
names = ["Zoe", "Alice", "Bob", "Charlie"]
first_alphabetically = min(names)
last_alphabetically = max(names)
print(f"First: {first_alphabetically}, Last: {last_alphabetically}")
# Output: First: Alice, Last: Zoe你也可以直接传入多个参数,而不是一个集合:
# 比较单个值
lowest = min(45, 23, 67, 12, 89)
highest = max(45, 23, 67, 12, 89)
print(f"Lowest: {lowest}, Highest: {highest}")
# Output: Lowest: 12, Highest: 89
# 适合比较少量的特定值
price1 = 19.99
price2 = 24.50
price3 = 15.75
cheapest = min(price1, price2, price3)
print(f"Cheapest option: ${cheapest}") # Output: Cheapest option: $15.7537.1.4) 使用 abs() 获取绝对值
abs() 函数返回一个数字的绝对值——它到 0 的距离,始终为正。这在你关心数值大小而不关心方向时很有用。
# 负数的绝对值
print(abs(-42)) # Output: 42
print(abs(-3.14)) # Output: 3.14
# 正数的绝对值(不变)
print(abs(42)) # Output: 42
print(abs(3.14)) # Output: 3.14
# 0 的绝对值
print(abs(0)) # Output: 0一个常见用法是计算方向无关的差值:
# 计算温度变化(只关心幅度)
morning_temp = 65
evening_temp = 72
temperature_change = abs(evening_temp - morning_temp)
print(f"Temperature changed by {temperature_change}°F")
# Output: Temperature changed by 7°F37.1.5) 使用 round() 对数字进行四舍五入
round() 函数会把数字四舍五入到指定的小数位数。如果不传第二个参数,它会四舍五入到最接近的整数。
# 四舍五入到最接近的整数
print(round(3.7)) # Output: 4
print(round(3.2)) # Output: 3
print(round(3.5)) # Output: 4
print(round(4.5)) # Output: 4 (rounds to nearest even number)
# 四舍五入到指定小数位数
price = 19.876
print(round(price, 2)) # Output: 19.88 (2 decimal places)
print(round(price, 1)) # Output: 19.9 (1 decimal place)
# 四舍五入到负小数位(四舍五入到十位、百位等)
population = 1234567
print(round(population, -3)) # Output: 1235000 (nearest thousand)
print(round(population, -4)) # Output: 1230000 (nearest ten thousand)关于刚好在中间的值的说明:当对一个恰好处于两个整数中间的数字进行舍入时,Python 有一个特殊规则。例如,2.5 恰好位于 2 和 3 的中间。你可能期望它进位到 3,但 Python 会舍入到相邻的偶数——在这个例子中是 2。
这称为“银行家舍入法”或“round half to even”。它是 IEEE 754 标准的一部分,有助于在大量舍入操作中减少偏差。
# 恰好在中间的值会舍入到最近的偶数
print(round(0.5)) # Output: 0 (0 is even)
print(round(1.5)) # Output: 2 (2 is even)
print(round(2.5)) # Output: 2 (2 is even)
print(round(3.5)) # Output: 4 (4 is even)
print(round(4.5)) # Output: 4 (4 is even)37.2) 使用 enumerate() 枚举序列
在遍历序列(sequence)时,你经常既需要元素本身,也需要它的位置。enumerate() 函数同时提供两者,从而无需手动维护计数器变量。
37.2.1) 手动计数器的问题
在学习 enumerate() 之前,程序员常用计数器变量来跟踪位置:
# 手动计数器方式(可行但不理想)
fruits = ["apple", "banana", "cherry", "date"]
index = 0
for fruit in fruits:
print(f"{index}: {fruit}")
index += 1
# Output:
# 0: apple
# 1: banana
# 2: cherry
# 3: date这种方式有几个缺点:
- 需要管理额外变量(
index) - 容易忘记递增计数器
37.2.2) 使用 enumerate() 同时获取位置和值
enumerate() 函数优雅地解决了这个问题。它接受一个可迭代对象,并返回 (index, element) 这样的成对数据:
# 使用 enumerate() - 更简洁、更 Pythonic
fruits = ["apple", "banana", "cherry", "date"]
for index, fruit in enumerate(fruits):
print(f"{index}: {fruit}")
# Output:
# 0: apple
# 1: banana
# 2: cherry
# 3: date语法 for index, fruit in enumerate(fruits) 使用了元组解包(tuple unpacking)(正如我们在第 15 章学到的)。每次迭代,enumerate() 会提供一个如 (0, "apple") 的元组,然后被解包到变量 index 和 fruit 中。
下面是 enumerate() 实际产生的内容:
# 直接查看 enumerate 的输出
fruits = ["apple", "banana", "cherry"]
enumerated = list(enumerate(fruits))
print(enumerated)
# Output: [(0, 'apple'), (1, 'banana'), (2, 'cherry')]37.2.3) 从不同的数字开始枚举
默认情况下,enumerate() 从 0 开始计数。你可以用 start 参数指定不同的起始数字:
# 从 1 开始计数(用于展示很有用)
tasks = ["Write code", "Test code", "Deploy code"]
for number, task in enumerate(tasks, start=1):
print(f"Step {number}: {task}")
# Output:
# Step 1: Write code
# Step 2: Test code
# Step 3: Deploy code当向用户显示编号列表时,这尤其有用,因为用户通常期望从 1 开始计数:
# 带编号选项的菜单
menu_items = ["New Game", "Load Game", "Settings", "Quit"]
print("Main Menu:")
for number, item in enumerate(menu_items, start=1):
print(f"{number}. {item}")
# Output:
# Main Menu:
# 1. New Game
# 2. Load Game
# 3. Settings
# 4. Quit37.2.4) enumerate() 与字符串及其他可迭代对象
enumerate() 函数适用于任何可迭代对象,而不仅仅是列表:
# 枚举字符串中的字符
word = "Python"
for position, letter in enumerate(word):
print(f"Letter {position}: {letter}")
# Output:
# Letter 0: P
# Letter 1: y
# Letter 2: t
# Letter 3: h
# Letter 4: o
# Letter 5: n
# 枚举元组
coordinates = (10, 20, 30, 40)
for index, value in enumerate(coordinates):
print(f"Coordinate {index}: {value}")
# Output:
# Coordinate 0: 10
# Coordinate 1: 20
# Coordinate 2: 30
# Coordinate 3: 40enumerate() 让代码更可读,也更不容易出错。只要你在循环(loop)中既需要位置又需要值,就应该用 enumerate(),而不是手动管理计数器。
37.3) 使用 zip() 组合序列
zip() 函数会按元素位置把多个可迭代对象逐项组合,生成成对(或元组)的对应元素。当你需要同时处理来自不同序列的相关数据时,这个函数非常有价值。
37.3.1) 理解 zip() 的工作方式
zip() 函数接受两个或更多可迭代对象,并返回一个由元组构成的迭代器(iterator),其中每个元组包含来自每个输入可迭代对象的一个元素:
# 组合两个列表
names = ["Alice", "Bob", "Charlie"]
ages = [25, 30, 35]
combined = list(zip(names, ages))
print(combined)
# Output: [('Alice', 25), ('Bob', 30), ('Charlie', 35)]“zip” 这个名字来源于衣服上的拉链——它将两侧结构按元素逐个合并成一个整体。
下面是 zip() 如何配对元素的可视化表示:
37.3.2) 在循环中使用 zip()
zip() 最常见的用法是在 for 循环中,当你需要同时遍历多个序列时:
# 处理并行数据
students = ["Alice", "Bob", "Charlie", "Diana"]
scores = [92, 85, 88, 95]
for student, score in zip(students, scores):
print(f"{student} scored {score}")
# Output:
# Alice scored 92
# Bob scored 85
# Charlie scored 88
# Diana scored 95这比使用索引要简洁得多:
# 不使用 zip() - 更复杂且更容易出错
students = ["Alice", "Bob", "Charlie", "Diana"]
scores = [92, 85, 88, 95]
for i in range(len(students)):
print(f"{students[i]} scored {scores[i]}")
# Same output, but more code and potential for index errors37.3.3) 处理长度不同的序列
当输入序列长度不同时,zip() 会在最短序列耗尽时停止:
# 长度不相等的序列
names = ["Alice", "Bob", "Charlie", "Diana"]
ages = [25, 30] # Only 2 ages
for name, age in zip(names, ages):
print(f"{name} is {age} years old")
# Output:
# Alice is 25 years old
# Bob is 30 years old
# (Charlie and Diana are not processed)这种行为可以防止错误,但如果你不小心,可能会导致静默的数据丢失。务必验证你的序列长度是否符合预期:
# 检查长度不匹配
names = ["Alice", "Bob", "Charlie"]
ages = [25, 30]
if len(names) != len(ages):
print(f"Warning: {len(names)} names but {len(ages)} ages")
print("Only processing the first", min(len(names), len(ages)), "entries")
# Output: Warning: 3 names but 2 ages
# Output: Only processing the first 2 entries
# 继续使用 zip() - 它会在最短序列处停止
for name, age in zip(names, ages):
print(f"{name} is {age} years old")37.3.4) 压缩两个以上的序列
zip() 函数可以组合任意数量的可迭代对象:
# 组合三个序列
products = ["Laptop", "Mouse", "Keyboard"]
prices = [999.99, 24.99, 79.99]
quantities = [5, 20, 15]
print("Inventory Report:")
for product, price, quantity in zip(products, prices, quantities):
total_value = price * quantity
print(f"{product}: ${price} × {quantity} = ${total_value:.2f}")
# Output:
# Inventory Report:
# Laptop: $999.99 × 5 = $4999.95
# Mouse: $24.99 × 20 = $499.80
# Keyboard: $79.99 × 15 = $1199.8537.3.5) 使用 zip() 创建字典
一个强大的模式是用 zip() 从分别的键和值序列创建字典:
# 从两个列表创建字典
keys = ["name", "age", "city"]
values = ["Alice", 25, "Boston"]
person = dict(zip(keys, values))
print(person)
# Output: {'name': 'Alice', 'age': 25, 'city': 'Boston'}37.4) 使用 any() 和 all() 进行布尔聚合
any() 和 all() 函数会在整个可迭代对象上测试条件,返回单个布尔值(boolean)结果。它们是基于多个条件进行验证与决策的强大工具。
37.4.1) 理解 any():只要至少一个元素为 True 就返回 True
any() 函数在可迭代对象中只要至少有一个元素为真值(truthy)(求值为 True),就返回 True。如果所有元素都为假值(falsy),则返回 False:
# any() 基本示例
print(any([True, False, False])) # Output: True (at least one True)
print(any([False, False, False])) # Output: False (all False)
print(any([False, True, True])) # Output: True (multiple True values)
# 空可迭代对象
print(any([])) # Output: False (no elements to be True)any() 使用 Python 的真值规则(正如我们在第 7 章学到的)。非零数字、非空字符串、非空集合都是真值:
# any() 与不同的 truthy/falsy 值
print(any([0, 0, 1])) # Output: True (1 is truthy)
print(any([0, 0, 0])) # Output: False (all zeros are falsy)
print(any(["", "", "text"])) # Output: True ("text" is truthy)
print(any(["", "", ""])) # Output: False (empty strings are falsy)37.4.2) any() 的实用用法
示例:检查是否满足任意一个条件
# 检查是否存在不及格分数(低于 60)
scores = [75, 82, 55, 90, 88]
has_failing_grade = any(score < 60 for score in scores)
if has_failing_grade:
print("Warning: At least one failing grade")
# Output: Warning: At least one failing grade
else:
print("All grades are passing")37.4.3) 理解 all():只有全部元素为 True 才返回 True
all() 函数只有在可迭代对象中所有元素都为真值(truthy)时才返回 True。只要有任意一个元素为假值(falsy),它就返回 False:
# all() 基本示例
print(all([True, True, True])) # Output: True (all True)
print(all([True, False, True])) # Output: False (one False)
print(all([True, True, False])) # Output: False (one False)
# 空可迭代对象
print(all([])) # Output: True (vacuous truth - no False elements)对于空可迭代对象的行为可能看起来令人意外:all([]) 返回 True。这称为空真(vacuous truth)——当没有任何元素反驳时,“所有元素都为 True” 这个说法在技术上确实为真。
# all() 与不同的 truthy/falsy 值
print(all([1, 2, 3])) # Output: True (all non-zero)
print(all([1, 0, 3])) # Output: False (0 is falsy)
print(all(["a", "b", "c"])) # Output: True (all non-empty)
print(all(["a", "", "c"])) # Output: False (empty string is falsy)37.4.4) all() 的实用用法
示例:验证是否满足所有条件
# 检查是否所有分数都及格(60 或以上)
scores = [75, 82, 68, 90, 88]
all_passing = all(score >= 60 for score in scores)
if all_passing:
print("Congratulations! All grades are passing")
# Output: Congratulations! All grades are passing
else:
print("Some grades need improvement")37.4.5) any() 与 all() 中的短路求值
any() 和 all() 都使用短路求值(short-circuit evaluation)(正如我们在第 9 章学到的)。一旦结果已经确定,它们就会停止检查:
# 被调用时打印信息的函数(用于展示执行过程)
def is_positive(n):
print(f"Checking {n}")
return n > 0
# any() 在遇到第一个 True 时停止
print("Testing any():")
numbers = [0, 0, 1, 2, 3]
result = any(is_positive(n) for n in numbers)
# Output:
# Testing any():
# Checking 0
# Checking 0
# Checking 1
# (Stops here - doesn't check 2 or 3)
print(f"Result: {result}") # Output: Result: True
print("\nTesting all():")
numbers = [1, 2, 0, 3, 4]
result = all(is_positive(n) for n in numbers)
# Output:
# Testing all():
# Checking 1
# Checking 2
# Checking 0
# (Stops here - doesn't check 3 or 4)
print(f"Result: {result}") # Output: Result: False这使得 any() 和 all() 很高效——它们不会在结果确定之后浪费时间继续检查元素。
37.5) 使用 sorted() 与自定义键进行排序
sorted() 函数会从任意可迭代对象创建一个新的已排序列表(list)。与 .sort() 方法(仅适用于列表并原地修改)不同,sorted() 适用于任何可迭代对象,并且总是返回一个新列表。
37.5.1) 使用 sorted() 进行基础排序
sorted() 函数默认按升序排列元素:
# 对数字排序
numbers = [42, 17, 93, 8, 55]
sorted_numbers = sorted(numbers)
print(sorted_numbers) # Output: [8, 17, 42, 55, 93]
# 原始列表不变
print(numbers) # Output: [42, 17, 93, 8, 55]
# 对字符串排序(按字母顺序)
names = ["Charlie", "Alice", "Bob", "Diana"]
sorted_names = sorted(names)
print(sorted_names) # Output: ['Alice', 'Bob', 'Charlie', 'Diana']sorted() 适用于任何可迭代对象,而不仅仅是列表:
# 对元组排序(返回列表)
coordinates = (5, 2, 8, 1, 9)
sorted_coords = sorted(coordinates)
print(sorted_coords) # Output: [1, 2, 5, 8, 9]
# 对字符串排序(返回字符列表)
word = "python"
sorted_letters = sorted(word)
print(sorted_letters) # Output: ['h', 'n', 'o', 'p', 't', 'y']
# 对集合排序(返回已排序列表)
unique_numbers = {5, 8, 2, 1}
sorted_unique = sorted(unique_numbers)
print(sorted_unique) # Output: [1, 2, 5, 8]37.5.2) 反向排序
使用 reverse=True 参数来按降序排序:
# 数字降序
scores = [85, 92, 78, 95, 88]
highest_first = sorted(scores, reverse=True)
print(highest_first) # Output: [95, 92, 88, 85, 78]
# 字符串降序(反字母顺序)
names = ["Charlie", "Alice", "Bob", "Diana"]
reverse_alpha = sorted(names, reverse=True)
print(reverse_alpha) # Output: ['Diana', 'Charlie', 'Bob', 'Alice']37.5.3) 理解 key 参数
key 参数是 sorted() 真正强大的地方。它改变了 Python 在排序时比较元素的方式。
什么是 key 参数?
key 参数接受一个函数(function)。Python 会对每个元素调用该函数以提取“比较键(comparison key)”,然后根据这些键进行排序,而不是根据原始元素排序。
它如何逐步工作:
- Python 对每个元素调用 key 函数
- Python 收集所有键
- Python 通过比较这些键来排序
- Python 按新顺序返回原始元素
# 示例:按长度排序
words = ["python", "is", "awesome"]
# Step 1: Python calls len() on each word
# len("python") → 6
# len("is") → 2
# len("awesome") → 7
# Step 2: Python has these keys: [6, 2, 7]
# Step 3: Python sorts the keys: [2, 6, 7]
# Step 4: Python returns words in that order: ["is", "python", "awesome"]
result = sorted(words, key=len)
print(result) # Output: ['is', 'python', 'awesome']将 key 函数可视化:
key 函数可以是什么?
key 函数必须:
- 接受一个参数(被排序的元素)
- 返回一个 Python 可比较的值(数字、字符串、元组等)
# 内置函数非常适合作为 key 函数
sorted(numbers, key=abs) # 按绝对值排序
sorted(words, key=len) # 按长度排序
sorted(names, key=str.lower) # 不区分大小写排序
# 你自己的函数
def first_letter(word):
return word[0]
sorted(words, key=first_letter) # 按首字母排序
# lambda 函数(第 23 章)
sorted(words, key=lambda w: w[-1]) # 按最后一个字母排序重要:key 函数对每个元素只调用一次
# 演示 key 函数在何时被调用
def show_key(word):
print(f"Getting key for: {word}")
return len(word)
words = ["cat", "elephant", "dog"]
result = sorted(words, key=show_key)
# Output:
# Getting key for: cat
# Getting key for: elephant
# Getting key for: dog
print(result) # Output: ['cat', 'dog', 'elephant']重要:key 函数对每个元素只调用一次
注意,show_key 对每个单词恰好只调用一次,而不是在比较期间反复调用。Python 很高效——它会先提取所有键、缓存它们,然后使用缓存的键进行排序。
把 key 看作在回答:“我应该比较哪个方面?”
key=len→ “按长度比较”key=abs→ “按绝对值比较”key=str.lower→ “按全部转为小写后的结果比较”key=lambda x: x[1]→ “按第二个元素比较”
key 参数让你可以按元素的任意属性进行排序,使 sorted() 极其通用。
37.5.4) 使用内置函数作为 key 进行排序
Python 的内置函数非常适合作为 key 函数:
# 按绝对值排序
numbers = [-5, 2, -8, 1, -3, 7]
sorted_by_magnitude = sorted(numbers, key=abs)
print(sorted_by_magnitude) # Output: [1, 2, -3, -5, 7, -8]
# 字符串不区分大小写排序
names = ["alice", "Bob", "CHARLIE", "diana"]
sorted_case_insensitive = sorted(names, key=str.lower)
print(sorted_case_insensitive) # Output: ['alice', 'Bob', 'CHARLIE', 'diana']37.5.5) 排序复杂数据结构
当排序由元组或列表组成的列表时,你可以用索引来指定按哪个元素排序:
# 按元组的第二个元素排序
students = [
("Alice", 92),
("Bob", 85),
("Charlie", 88),
("Diana", 95)
]
# 按分数排序(第二个元素)
by_score = sorted(students, key=lambda student: student[1])
print(by_score)
# Output: [('Bob', 85), ('Charlie', 88), ('Alice', 92), ('Diana', 95)]
# 按分数降序排序
by_score_desc = sorted(students, key=lambda student: student[1], reverse=True)
print(by_score_desc)
# Output: [('Diana', 95), ('Alice', 92), ('Charlie', 88), ('Bob', 85)]注意:这里我们使用了 lambda(正如我们在第 23 章学到的)。lambda 是一种小型匿名函数。表达式 lambda student: student[1] 创建了一个函数,它接收一个学生元组并返回其第二个元素(分数)。
37.5.6) 多级排序
你可以通过让 key 函数返回一个元组来按多个条件排序。Python 会从左到右逐个比较元组的元素:
元组比较如何工作:
当 Python 比较两个元组时,会遵循以下规则:
- 先比较第一个元素。如果不同,比较结束。
- 如果第一个元素相同,比较第二个元素。
- 如果第二个元素相同,比较第三个元素。
- 持续下去,直到找到差异或元素用完。
# 元组比较示例
print((1, 'a') < (2, 'z')) # Output: True (1 < 2, so True immediately)
print((1, 'z') < (1, 'a')) # Output: False (1 == 1, so compare 'z' < 'a')
print((1, 'a') < (1, 'a')) # Output: False (both tuples are equal)
print((1, 2, 9) < (1, 3, 1)) # Output: True (1 == 1, then 2 < 3)这使得元组非常适合多级排序——Python 会自动为你处理“先比较第一条件,再比较第二条件,再比较第三条件”的逻辑:
# 按多个条件排序
students = [
("Alice", "Smith", 92),
("Bob", "Jones", 85),
("Alice", "Brown", 88),
("Charlie", "Smith", 85)
]
# 先按名,再按姓排序
by_name = sorted(students, key=lambda s: (s[0], s[1]))
print("By name:")
for student in by_name:
print(f" {student}")
# Output:
# By name:
# ('Alice', 'Brown', 88)
# ('Alice', 'Smith', 92)
# ('Bob', 'Jones', 85)
# ('Charlie', 'Smith', 85)
# 先按分数降序,再按名字升序
by_score_then_name = sorted(students, key=lambda s: (-s[2], s[0]))
print("\nBy score (high to low), then name:")
for student in by_score_then_name:
print(f" {student}")
# Output:
# By score (high to low), then name:
# ('Alice', 'Smith', 92)
# ('Alice', 'Brown', 88)
# ('Bob', 'Jones', 85)
# ('Charlie', 'Smith', 85)注意:要实现一个条件降序、另一个条件升序,我们对数值取负(-s[2])。这是可行的,因为取负会反转数字的排序顺序。在上面的例子中,-s[2] 会让分数从高到低排序,而 s[0] 会让名字从 A 到 Z 排序。
37.5.7) 为复杂 key 使用辅助函数
当排序逻辑变得复杂时,定义一个辅助函数会让代码更可读、更易维护。然后你可以在 key 函数中使用这个辅助函数。
示例:按扩展名排序文件
假设你想按文件扩展名(.csv、.jpg、.pdf 等)对文件分组,并在每个分组内按文件名的字母顺序排序。key 函数需要提取扩展名,这需要一些字符串处理。
# 先按扩展名排序,再按名称排序
files = [
"report.pdf",
"data.csv",
"image.jpg",
"notes.txt",
"backup.csv",
"photo.jpg"
]
# 提取扩展名用于排序
def get_extension(filename):
"""从文件名中提取文件扩展名。"""
return filename.split(".")[-1] # 按 "." 分割并取最后一部分
# 在 key 中使用辅助函数
sorted_files = sorted(files, key=lambda f: (get_extension(f), f))
print("Files sorted by extension, then name:")
for file in sorted_files:
print(f" {file}")
# Output:
# Files sorted by extension, then name:
# backup.csv # csv files first (alphabetically)
# data.csv # csv files first (alphabetically)
# image.jpg # jpg files next
# photo.jpg # jpg files next
# report.pdf # pdf files next
# notes.txt # txt files last它如何工作:
- key 函数
lambda f: (get_extension(f), f)为每个文件名返回一个元组 - 对于 "report.pdf",它返回
("pdf", "report.pdf") - 对于 "data.csv",它返回
("csv", "data.csv") - Python 先按元组的第一个元素(扩展名)排序,再按第二个元素(完整文件名)排序
- 这样就会按扩展名分组,并在每组内按字母顺序排序
为什么要用辅助函数?
对比可读性:
# 不使用辅助函数 - 更难理解
sorted_files = sorted(files, key=lambda f: (f.split(".")[-1], f))
# 使用辅助函数 - 意图更清晰
sorted_files = sorted(files, key=lambda f: (get_extension(f), f))辅助函数让你的代码自解释。任何人看到 get_extension(f) 都能立刻明白发生了什么,而 f.split(".")[-1] 则需要在脑中进行解析。
37.5.8) sorted() vs .sort():何时使用哪个
Python 提供两种排序方式:
sorted()- 返回一个新排序列表的函数.sort()- 原地排序的列表方法
# sorted() - 创建新列表,原始列表不变
numbers = [3, 1, 4, 1, 5]
sorted_numbers = sorted(numbers)
print(f"Original: {numbers}") # Output: Original: [3, 1, 4, 1, 5]
print(f"Sorted: {sorted_numbers}") # Output: Sorted: [1, 1, 3, 4, 5]
# .sort() - 原地修改列表,返回 None
numbers = [3, 1, 4, 1, 5]
result = numbers.sort()
print(f"Modified: {numbers}") # Output: Modified: [1, 1, 3, 4, 5]
print(f"Return value: {result}") # Output: Return value: None何时使用 sorted():
- 你需要保留原始顺序
- 你要排序的对象不是列表(元组、字符串、集合等)
- 你想在一个表达式里完成排序并赋值
何时使用 .sort():
- 你有一个列表并且不需要保留原始顺序
- 你想节省内存(不创建新列表)
- 你需要为效率原地排序一个大列表
sorted() 函数是 Python 最通用的工具之一。结合 key 参数,它几乎可以处理任何排序需求:从简单的数字排序,到复杂的多条件排序,再到对嵌套数据结构进行排序。
本章为你配备了 Python 的关键内置函数与工具。你已经学会了如何:
- 理解 Python 的类型层级,并预测哪些操作适用于哪些类型
- 使用
len()、sum()、min()、max()、abs()和round()等基础函数完成常见操作 - 使用
enumerate()在迭代时获取位置信息 - 使用
zip()同时处理并行序列 - 使用
any()与all()在集合上做整体决策 - 使用
sorted()与自定义 key 函数灵活排序数据
这些工具构成了地道(idiomatic) Python 代码的基础。它们高效、可读,并能正确处理边界情况。随着你继续编程,你会发现自己会不断使用这些函数——它们是让 Python 代码优雅且富有表达力的构建块。
在下一章中,我们将探索装饰器(decorators),它能让你以强大的方式修改和增强函数行为,并在我们第 23 章学到的一等函数(first-class function)概念之上继续构建。