Skip to content
This page has been auto-translated and may contain errors.View in English

控制流

你写到现在的每个程序都是这样执行的:从上到下,一行一行地执行。这对简单脚本没问题,但真实的程序需要做决策和重复工作。一个测验需要判断答案对不对。一个游戏需要一直运行到玩家赢或输。这章讲的就是怎么让程序分支和循环。

控制流决定了程序的执行路径。条件语句(if/elif/else)在分支间选择;循环(whilefor)重复一个代码块。Python 的 for 一次遍历一项而不是按索引计数,所以它能干净地遍历很多东西。这章讲什么时候用哪个工具,以及每个工具容易出的问题。

Python 给你三个控制流工具:if/elif/else 用来分支,whilefor 用来循环。for 循环遍历一个可迭代对象(任何可以一次取一项的东西,比如列表或文件),当项用完了就自动停止,所以你很少需要手动管理索引。while 循环只要条件还是真就一直运行。breakcontinue 只会影响它们周围最近的那个循环,循环的 else 子句(后面会讲)只在循环正常结束而不是被 break 打断时才执行。这章剩下的部分讲的是怎么选择合适的工具,以及避免每个工具容易出的问题。

比较

在做决策之前,你需要比较东西。比较操作符返回 TrueFalse。最重要的一个要早点搞对:= 赋值,== 检查两个值是否相等。搞混这两个是初学者最常见的错误之一。

比较操作符返回一个 bool,Python 还让你能链式比较:0 < x < 10 的意思是 0 < x and x < 10,大多数其他语言从左到右读会读错。字符串是按字典序逐字符比较的,所以大写字母排在小写字母前面。用 == 来比较值;你几乎永远不想用 is,它问的是另一个问题(Deep Dive 里会讲)。

比较返回一个 bool,实际代码里有两个细节要注意。首先,Python 支持链式比较:a < b < c 读作 a < b and b < c,中间的值 b 只会算一次,如果这是个昂贵的调用就值得知道。其次,== 检查两个值是否相等,而 is 检查恒等性(两个名字是否指向内存里同一个对象)。它们不能互换:用 == 比较值,把 is 留给 NoneTrueFalse,那些时候你真的想要那唯一的一个对象。拿 is 去比较字符串或数字会导致 bug,在小输入上能过但大输入上就失败了。

python
5 > 3     # True
5 < 3     # False
5 == 5    # True   (注意:双等号;= 是赋值,== 是比较)
5 != 3    # True   ("不等于")
5 >= 5    # True   ("大于等于")
5 <= 4    # False  ("小于等于")

=== 这个区别会把几乎所有人都绊倒。赋值(=)存储一个值;比较(==)检查两个值是否相同。

你也可以比较字符串。Python 按字母顺序逐字符比较它们,字符串章会更详细地讲:

python
"apple" == "apple"   # True
"apple" < "banana"   # True  (a 在 b 前面)
"apple" == "Apple"   # False (区分大小写)
Juno比较= 存储一个值,== 检查两个值是否相等。我在累的时候还是会停下来反复检查这个,所以如果你被绊倒了你并不孤独。比较返回 TrueFalse,对字符串也一样有效。
Juno比较 比较返回一个 bool。像 0 < x < 10 这样的链式形式读作 0 < x and x < 10,大多数其他语言会读错。字符串按 Unicode 顺序比较,所以是区分大小写的排序。
Juno比较== 比较值,用 is 比较恒等性,把 is 留给 NoneTrueFalse。陷阱是在字符串或整数上用 is:在小输入上能过但大输入上就失败了。链式比较只会算一次中间项,当它很昂贵时很有用。

组合条件

andornot 组合比较。and 要求两边都是真。or 要求至少一边是真。not 翻转结果。这些让你能表达真实的条件,比如"分数及格 AND 用户活跃"。

andor 短路求值:and 在第一个假值处停止,or 在第一个真值处停止。它们返回那个实际的操作数,不一定是单纯的 TrueFalse,这就是为什么 name or "guest" 是提供备选值的干净方式。not 总是给你回一个纯 bool

andor 不返回 TrueFalse,它们返回其中一个操作数。and 返回第一个假值,或者如果都是真的话返回最后一个;or 返回第一个真值,或者如果都是假的话返回最后一个。一旦左边决定了结果,右边就不会被计算,这个短路保证(Python 一旦知道答案就停止)是你能依靠的东西:user and user.name 只在 user 存在时才读 user.name,所以在缺少值时不会出错。反面是个真正的失败模式:如果右边有个你指望执行的副作用,比如一个做日志或写入的函数,当左边短路了它就会悄悄地不运行。

python
age = 25
score = 88

age >= 18 and score >= 80    # True  (两边都要是真)
age < 18 or score >= 80      # True  (至少一边要是真)
not age >= 18                # False (翻转结果)

and 要求两边。or 要求至少一边。not 反转。

Juno组合条件and 要两边都真,or 要至少一边真,not 翻转答案。这覆盖了你会写的大多数真实条件,比如"分数及格 and 用户活跃"。Python 一旦知道结果就停止检查。
Juno组合条件and 在第一个假值处停止,or 在第一个真值处停止,两者都返回那个实际的操作数,不一定是单纯的 True/False。短路就是为什么 x and x.method()x 为空时还是安全的。
Juno组合条件and/or 返回一个操作数,不是 bool,一旦左边决定了结果右边就被跳过。在像 user and user.name 这样的保护语句上用它。留意另一个边界:右边的副作用在短路时会悄悄地不执行。

真值和假值

Python 里每个值都有一个布尔解读,即使它不是 TrueFalse。空字符串、零、空列表和 None 在条件里的表现都像 False。其他一切都表现像 True。这意味着 if results: 检查列表是否非空,不用写 if len(results) > 0:

Python 的假值是 False00.0""[](){}set()None。其他一切都是真值,所以空容器是假的非空的是真。这就是为什么 if results: 是惯用的空检查:这样写而不是 if len(results) > 0:

当你在条件里放一个对象,Python 通过调用它的 __bool__ 方法(一个魔术方法,Python 在后台调用的双下划线方法)来问它是非即真。如果一个类型没有定义 __bool__,Python 退而求其次用 __len__,所以任何长度为零的东西都算假。标准假值是 False00.0""、空容器([](){}set())和 None;其他一切都是真。要注意的陷阱:if results: 对于空列表和 None 都是假,所以如果你特别需要把"空"和"还没设置"区分开来,测试 if results is None: 而不是依靠真值性。

python
# 这些在条件里都表现得像 False:
False, 0, 0.0, "", [], {}, (), None

# 其他一切都表现得像 True

这意味着 if results: 是个自然的说法表示"如果列表里有东西",而 if name: 检查字符串是否有内容。

Juno真值和假值 空东西和零在条件里算 False0""[]None。其他一切都算 True。这就是为什么 if results: 读起来那么干净,好像"如果列表里有东西",不需要 len()
Juno真值和假值 假值是 False、零、空字符串、空容器和 None;其他一切都是真值。所以空列表是假的满的列表是真的。这就是为什么 if my_list: 作为空检查有效,不需要 len()
Juno真值和假值 条件调用 __bool__,然后 __len__ 如果前面的没定义,所以你自己的类可以选择它们的真值性。陷阱是:if results: 分不出空列表和 None。当那个区别很重要时,用 is None

if / elif / else

if 语句只在它的条件是 True 时才运行一个代码块。elif 在第一个条件是假时添加更多条件来检查。else 捕获所有没有匹配任何条件的情况。Python 用缩进而不是花括号来定义什么属于每个块。

if/elif/else 从上到下计算条件并运行第一个匹配的块。Python 用缩进(按惯例 4 个空格)来定义块范围;缩进不一致是个 SyntaxError。只有一个分支会运行:一旦一个条件匹配了,所有后续的 elifelse 都被跳过。

Python 从上到下检查条件并运行恰好一个分支:第一个条件为真的,或者如果都不匹配的话运行 else。每个条件都是延迟计算的,意思是 Python 只在上面的条件都假时才测试它,所以顺序是逻辑的一部分。把最便宜的或最可能的测试放在前面,对重叠的范围要把更紧的界限放在更宽松的前面,否则后面的分支就变不可达了。缩进是块结构,不是风格选择,所以混用制表符和空格是个 TabError 而不是寂静地改变含义。

python
score = 87

if score >= 90:
    print("A 级")
elif score >= 80:
    print("B 级")
elif score >= 70:
    print("C 级")
else:
    print("C 以下")

规则:

  • if 是必需的并且总是在前面
  • elif("else if" 的简写)是可选的,你可以想要多少就多少
  • else 是可选的,处理所有没有匹配的东西,放在最后
  • Python 用缩进(4 个空格)来标记属于每个块的东西;没有花括号

缩进不是可选的或只是装饰。Python 用它来定义结构。缩进不一致是个语法错误。

Junoif / elif / elseif 在条件是真时运行它的块,elif 检查更多情况,else 捕获剩下的。只有第一个匹配运行。我早期被绊倒的东西:缩进是告诉 Python 每个块从哪儿开始和结束的,所以保持一致。
Junoif / elif / else 条件从上到下检查,只有第一个匹配运行,所以顺序很重要:把更窄的范围放在更宽松的前面。缩进(4 个空格)标记块,搞混了是个 SyntaxError,不是寂静的 bug。
Junoif / elif / else 条件延迟计算,从上到下,所以把最便宜的或最可能的测试放在前面,对重叠的界限要紧-在-前。缩进是结构,不是风格,制表符对空格是个 TabError 而不是寂静改变。

单行条件

对于简单的是/否赋值,Python 有个叫三元表达式的紧凑单行形式:值_如果真 if 条件 else 值_如果假。只在逻辑很小读起来像句子时才用它。

条件表达式(三元操作符)基于一个条件计算为两个值之一。它是一个表达式,不是语句,所以它能出现在任何期望值的地方:在 f 字符串里、作为函数参数、在赋值里。用它处理简单的是/否情况;对任何涉及 elif 的东西,写完整的版本。

条件表达式 x if condition else y 计算条件并返回一边而不运行另一边,所以在其中一个分支会出错的地方用它是安全的。因为它是个表达式,它能插进值出现的任何地方:f 字符串、函数参数、默认值。纪律问题是知道什么时候停止。它不能装一个 elif 而把它们链起来(a if p else b if q else c)读起来比它替换的块还糟,所以一旦你有超过一个决策,回到完整的 if/elif/else。维护代码的人以后会感谢你。

python
label = "pass" if score >= 50 else "fail"

这是个三元表达式;它读起来像句子。在逻辑很小时用它。对任何涉及 elif 的东西,写完整的版本。

Juno单行条件值_如果真 if 条件 else 值_如果假 在单行里挑两个值之一。对整洁的小选择很美,比如 "pass" if score >= 50 else "fail"。逻辑一长就回到常规 if 块,它读起来会更好。
Juno单行条件 三元是个表达式,所以它适合任何值出现的地方:f 字符串、参数、赋值。用它处理简单的二选一。任何涉及 elif 的东西都属于完整块。
Juno单行条件x if cond else y 只运行它挑的那边,所以在一个分支会抛异常的地方是安全的,而且它能插进任何表达式位置。别链接它们:堆积的三元读起来比它替换的块还糟,所以过了一个决策,写出来。

while 循环

while 循环只要它的条件是 True 就重复它的块。用它当你不知道循环应该运行多少次时,比如等待有效输入或重试直到一个任务成功。

while 在每次迭代前计算它的条件,只在条件是真值时才运行块。用它处理循环其中退出条件取决于循环内部改变的东西。当迭代次数已知或你在遍历一个集合时,for 通常更干净。

while 在每次通过前重新测试它的条件,块体就是改变那个条件的东西,所以失败模式是结构性的:忘记向退出前进(递减计数器、消费队列)你会得到一个永不结束的循环和一个挂起的进程。惯用形状是当退出必须在块中间或末尾决定时 while True 加一个内部 break,这样把条件保留在它实际发生的地方而不是强行放进头部。对任何遍历集合或运行已知次数的东西,for 循环更干净并去掉了计数器的一个差一错误的机会。

python
lives = 3

while lives > 0:
    print(f"剩余生命数:{lives}")
    lives -= 1

print("游戏结束")

while 在你不知道循环会运行多少次时最好。当你知道时,或当你在遍历一个集合时,for 更干净。

Junowhile 循环while 只要条件保持真就一直运行它的块。在你不知道需要多少轮时用它,比如等待有效输入。确保循环内部有东西向退出前进,不然它会永远运行,这是我做过不止一次的错误。
Junowhile 循环while 在每次通过前检查它的条件,块体就是改变它的。用它当退出取决于循环内发生的东西。如果你在计数或遍历一个集合,for 是更干净的选择。
Junowhile 循环 块体必须向退出前进或你会挂起,所以那是第一件要检查的。当退出落在块中间时 while True 加内部 break 是干净的形状。对已知计数或集合,优先用 for 并跳过一个差一错误风险。

break 和 continue

break 立即退出循环,不管还有多少次迭代剩余。continue 跳过当前迭代的剩余部分并跳回条件检查。两者都只影响它们所在的最内层循环。

break 终止最近的包围循环,将控制转移到它之后的第一条语句。continue 跳过当前循环体的剩余部分并从条件检查重启(或在 for 循环里从下一次迭代)。两者都只影响最近的包围循环。

break 立刻离开循环并跳过它的 else 子句如果有的话;continue 直接跳回循环头为了下一遍。两者绑定到最近的包围循环,Python 没有标记的 break,所以它们不能从一个内层循环到达外层循环。当你需要一次逃出几层时,干净的做法是把嵌套的循环提起来变成一个函数并 return,它读起来比用一个标志变量穿过两个的要好。标志有效,但每个额外的 if found: 你加上去来躲避缺失的标记都是个地方未来的编辑能搞错。

break 立即退出循环:

python
target = 5
num = 0

while True:
    num += 1
    if num == target:
        print(f"找到了 {target}")
        break   # 停止循环

while True: 加上 break 在退出条件很复杂或需要在循环体末尾发生时是个有效的常见模式。

continue 跳过当前迭代的剩余部分并回到条件检查:

python
num = 0

while num < 10:
    num += 1
    if num % 2 == 0:
        continue    # 跳过偶数
    print(num)      # 只有奇数打印:1, 3, 5, 7, 9
Junobreak 和 continuebreak 立刻跳出循环,continue 跳过这一轮的剩余并回到条件检查。两者都只影响它们直接在里面的循环。while True:break 初看时读起来很奇怪,但它是一个循环直到什么发生的常规方式。
Junobreak 和 continuebreak 离开最近的循环,continue 从条件重启它。都不能到达外层循环。要从嵌套循环逃出,设置一个标志或把内层循环拉进一个函数并 return
Junobreak 和 continue 两者绑定到最近的循环,没有标记的 break,所以逃出几层意味着一个标志或,更干净,一个有 return 的函数。break 也跳过循环的 else,这正是搜索模式依靠的。

for 循环

for 循环一次遍历一个序列中的一项:一个列表、一个字符串、一个数字范围。你在 for 之后命名的变量一个接一个地接收每一项。你不用管理计数器或自己检查长度。

for 一次要求它给定的东西提供一项,当项用完时停止,而不是通过索引位置计数。这就是为什么它能在远不止列表上工作:字符串、字典、范围和文件对象都以同样的方式循环,不用长度检查或计数器在你这边。在遍历集合时优先用 for 而不是 while

for 不计索引,它要求给定的东西提供一个迭代器(一个一次交付一项并记得停在哪儿的对象)并拉取项直到它们用完。实际的回报是 for 在远不止列表上工作:字符串、字典、范围、文件对象和延迟源比如生成器(按需计算的序列,永远不会全部保在内存里)都以同样的方式循环。最后那群就是一个陷阱所在的地方:一个延迟迭代器是一次用,所以一旦一个 for 循环排干了一个生成器或文件,再循环它给你什么都没有。如果你需要项两次,先把它们变成列表或再打开源。

python
players = ["张三", "李四", "王五"]

for player in players:
    print(f"你好,{player}!")

for 循环也在字符串上工作(一次迭代一个字符)和其他任何序列类型上。

Junofor 循环for item in sequence 一次遍历每一项,你永远不碰一个计数器或长度自己。它在列表、字符串、范围上工作,很多东西。从其他语言来的,不管理索引一开始感觉很奇怪,然后感觉像礼物。
Junofor 循环for 一次拉一项直到它们用完,所以它不受限于索引序列。列表、字符串、字典、范围、文件:对所有都是同样的循环。无索引簿记,无长度检查。
Junofor 循环for 驱动任何迭代器,所以列表、文件和生成器都一样循环。陷阱是延迟的:生成器或文件迭代器是一次用,一遍后就排干了。需要项两次?先拉进列表或重新打开源。

range()

range() 为你生成一个数字序列来循环遍历。range(5) 给你 0, 1, 2, 3, 4。你可以控制起点、终点和步长。用它当你需要循环运行特定次数时。

range(start, stop, step)start 生成整数直到(但不包括)stop,以 step 步长。它是一个延迟序列:它不创建列表,它按需生成数字。这使 range(10_000_000) 内存高效。所有三种形式接受负参数用于反向计数。

range 从不建立数字列表,它只存储起点、终点和步长,运算每个值时进行。那使 range(10_000_000) 成本与 range(10) 一样多的内存,所以当你只需要迭代时,直接循环 range 而不是把它包在 list() 里和支付一千万个储存的整数。它还是表现得像一个序列它计算的地方:你能索引它、切片它、问 len(),用 in 测试成员,全部便宜。调用 list(range(n)) 只有在你真的需要一个真实的、可重用的列表来持有时才值得。

python
for i in range(5):
    print(i)    # 0, 1, 2, 3, 4

range() 有三种形式:

调用它生成什么
range(5)0, 1, 2, 3, 4
range(2, 6)2, 3, 4, 5
range(0, 10, 2)0, 2, 4, 6, 8(步长为 2)
range(5, 0, -1)5, 4, 3, 2, 1(倒数)

range() 不创建列表。它一次生成一个数字,对非常大的范围也有效。

Junorange()range(5) 给你 0, 1, 2, 3, 4,你可以设置起点、终点和步长。在你想循环运行设定次数时用它。它不建立列表,每轮给你一个数字。
Junorange()range(start, stop, step)start 计数直到但不包括 stop。它是个延迟序列,所以按需生成数字而不建立列表,这就是为什么 range(10_000_000) 成本几乎什么都没有。负步长倒计数。
Junorange()range 只存储起点、终点和步长,所以不管多大都是平坦内存。直接循环它而不是 list(range(n)) 除非你真的需要一个可重用列表。它还是廉价地索引、切片和支持 inlen()

enumerate()

enumerate() 在你循环时同时给你索引和值,所以你不需要分开追踪计数器。i, player 部分自动在每次迭代接收一对值。

enumerate(iterable, start=0) 包装任何迭代器并产生 (index, value) 元组。start 参数偏移计数器但不改变基础索引。优先用 enumerate() 而不是管理一个计数器变量;它更干净也更少出错。

enumerate 包装任何可迭代对象并交付 (count, value) 对,for 头通过解包(Python 一步把元组的每个部分赋给它自己的名字)把它们分成两个名字。它是延迟的且除了计数器什么都不添加,所以成本和裸循环一样。总是去找它而不是手管理的 index = 0; index += 1 的原因是可靠性:手版本邀请了你在哪儿忘了增量或在错的地方碰它的 bug,而 enumerate 移除了整个这类错误。当你为人编号输出时传 start=1,他们从一计数,不是零。

python
players = ["张三", "李四", "王五"]

for i, player in enumerate(players):
    print(f"{i + 1}. {player}")
# 1. 张三
# 2. 李四
# 3. 王五

i, player 语法叫解包。Python 自动把 (index, value) 对分成两个名字。

默认 enumerate() 从 0 开始。传一个起值改变那个:

python
for i, player in enumerate(players, start=1):
    print(f"{i}. {player}")    # 从 1 开始
Junoenumerate()enumerate() 同时给你位置和值,所以无单独计数器要保持同步。i, player 部分一次抓两个。当你给某人编号列表时传 start=1
Junoenumerate()enumerate(iterable, start=0) 产生 (index, value) 对在 for 头里直接解包。优先用它而不是计数器变量,它更干净而且你不能忘记增量它。start 只改变显示数字,不是真索引。
Junoenumerate() 延迟、除了计数器免费,而且它删除了整个"忘记增量"bug 手索引邀请的,所以默认去找它。每项是个元组,这就是为什么 for i, v 解包有效。人类数字 start=1

嵌套循环

你能把一个循环放在另一个循环里。内循环对外循环的每一次迭代都完整运行。这是怎么处理网格、组合或任何两层结构的数据的。

嵌套循环的迭代次数是外层长度 m 乘以内层长度 n 的 O(m × n)。嵌套循环内的 breakcontinue 只影响最内层循环。要从多层打破出来,用一个标志变量或重构成函数。

嵌套循环相乘:长度 m 的外循环围着长度 n 的内循环运行体 m 乘以 n 次,所以成本快速攀升,一对大输入上的循环是慢脚本背后的常见罪魁。在达到第三层前,问问数据结构是否是真正的修复(字典查询能折叠一个内搜索循环)。当你只需要两个序列的每个组合(笛卡尔积,一个与另一个的每个配对),标准库里的 itertools.product 读起来比嵌套更好。记住 break 只退出内循环:要离开两个,提起它们到函数并 return

python
rows = [1, 2, 3]
cols = ["A", "B"]

for row in rows:
    for col in cols:
        print(f"{col}{row}", end=" ")
    print()   # 每行后换行
# A1 B1
# A2 B2
# A3 B3

嵌套循环内的 breakcontinue 只影响最内层循环。

Juno嵌套循环 一个循环在循环里外循环的每一轮完整运行内循环。这就是怎么处理网格和组合的。breakcontinue 只作用在它们坐着的循环,这里是内循环。
Juno嵌套循环 内循环在每个外步完整运行,所以计数是 m 乘以 n,在大输入上留意。breakcontinue 只到达最内层循环。要逃出两个,用一个标志或拉它们到函数并 return
Juno嵌套循环 成本相乘,所以大输入上嵌套循环是常见慢点;有时字典查询完全替换内循环。对每个配对,itertools.product 比嵌套更好。break 只到内循环,所以有 return 的函数是干净的两个都出。

循环-else

Python 循环能有一个 else 子句只在循环没碰到 break 时运行。它不常用,但是写"搜索列表,如果什么都没找到,做这个"的最干净方式。

forwhile 循环的 else 在循环正常完成(排干可迭代对象或条件变假)时执行,没碰到 break。它是"搜索并报告如果没找到"的惯用模式,不需要单独的 found-标志变量。

循环 else 运行只当循环没 break 就结束,这与大多数人读它的相反(它不是"else,循环没运行")。那命名就是为什么它迷惑读者,所以生产里的规则是为一个东西只用它一个东西:搜索-和-没找到模式,其中 break 标记一个点击 else 报告失误。它替换 found = False 标志你否则会携带穿过循环。其他地方,聪明的东西花费在困惑的审查者上超过它节省的,所以 else 上的一条注释值得那一行。

python
target = "王五"
names = ["张三", "李四", "赵六"]

for name in names:
    if name == target:
        print(f"找到了 {target}")
        break
else:
    print(f"{target} 不在列表里")   # 运行因为 break 永不触发

如果 break 运行,else 被跳过。如果循环排干序列,else 运行。它是个利基模式但比标志变量更干净。

Juno循环-else 循环能有个 else 只在循环无 break 时结束运行。不常用,但最整洁的说法是"搜索这个列表,如果什么都没匹配,做这个"。名字有点误导,所以短评论有帮助。
Juno循环-else 循环 else 在无 break 循环正常结束时触发。它是整洁的"搜索并报告如果没找到"模式,无 found-标志需要。保它到那一个用,名字让人丢向。
Juno循环-else 在循环无 break 时运行,这与字的读法反向。保它到搜索-和-未找到情况,那儿它替换 found = False 标志,并注释它。其他地方它花费比它节省的更多在困惑的审查者。

排序

sorted() 返回一个新排序列表并保留原列表不变。.sort() 就地排序列表并返回 Nonekey= 参数让你按其他于原始值的东西来排序。比如排序名字不区分大小写或按分数排序玩家元组。

sorted() 是安全的默认:它永不修改原始。.sort() 就地修改并返回 None。两者都接受 reverse=True 为降序。key= 参数接受一个在比较前应用到每个元素的函数。这从数据分离排序条件。

Python 的排序是稳定的,意思是比较相等的项保持它们的原始顺序,这就是让你分阶段排序的东西:首先按名字排序,然后按分数排序,分数相同的玩家保持名字顺序。key= 函数运行一次每个元素,不是一次每次比较,所以一个昂贵的 key 被计算 n 次不是 n log n 次,而且你很少需要自己缓存它。.sort() 返回 None 故意,来阻止你写 scores = scores.sort() 并悄悄抹掉你的列表,所以就地调用必须自己站一行。默认去找 sorted(),为实际想要变更原始保持 .sort()

python
scores = [87, 42, 96, 55, 71]

ranked = sorted(scores)           # [42, 55, 71, 87, 96] (新列表)
scores.sort()                     # 排序原始列表,返回 None
scores.sort(reverse=True)         # [96, 87, 71, 55, 42]

两者都接受 key= 参数:一个在比较前应用到每项的函数:

python
names = ["Charlie", "Alice", "Bob"]
sorted(names, key=str.lower)       # 不区分大小写排序

players = [("Alice", 87), ("Bob", 96), ("Charlie", 55)]
sorted(players, key=lambda p: p[1])   # 按分数排序

lambda p: p[1] 是一个单行函数:它接受一个玩家元组并返回分数。Lambdas 在Lambdas 和列表推导章得到充分对待。

对简单情况,用 sorted()。对想就地修改的列表,用 .sort()

Juno排序sorted() 交付一个新排序列表并保留原列表,.sort() 就地重排列表并返回 None。传 key= 来按其他于原值的东西排序,比如名字不区分大小写。不确定时,去找 sorted(),它是安全的。
Juno排序sorted() 永不碰原始,.sort() 就地变更并返回 None。两者取 reverse=True 和一个在比较前应用到每个元素的 key= 函数。默认到 sorted();只在你意思是改变列表时用 .sort()
Juno排序 排序是稳定的,所以分阶段排序和平手保持顺序。key= 一次每个元素运行,不是每次比较,所以别预缓存它。而且 .sort() 返回 None 故意:x = x.sort() 抹掉你的列表,所以保就地调用它自己一行。

实践中

循环遍历分数,累计一个总数,计数及格级别,打印一个总结:

python
raw_scores = [87, 42, 96, 55, 71, 63]

total = 0
passing = 0

for score in raw_scores:
    total += score
    if score >= 60:
        passing += 1

average = total / len(raw_scores)
print(f"平均:{average:.1f}")
print(f"及格:{passing}/{len(raw_scores)}")
print(f"最高分:{sorted(raw_scores, reverse=True)[0]}")

按排序顺序处理一个文件列表,跳过太大的那些,报告多少被跳过:

python
files = [
    {"name": "report_jan.csv", "size_mb": 12},
    {"name": "report_feb.csv", "size_mb": 850},
    {"name": "report_mar.csv", "size_mb": 7},
]

MAX_SIZE = 100
skipped = 0

for f in sorted(files, key=lambda x: x["name"]):
    if f["size_mb"] > MAX_SIZE:
        print(f"跳过 {f['name']} ({f['size_mb']} MB,太大)")
        skipped += 1
    else:
        print(f"处理 {f['name']}...")

print(f"\n完成。{skipped} 个文件被跳过。")

扫描一个请求日志找错误,然后用一个重试循环成功时或达到尝试限制时退出:

python
requests = [
    {"method": "GET", "path": "/users", "status": 200},
    {"method": "POST", "path": "/users", "status": 201},
    {"method": "GET", "path": "/broken", "status": 500},
]

errors = []

for req in requests:
    if req["status"] >= 400:
        errors.append(req)

if errors:
    print(f"请求日志有 {len(errors)} 个错误:")
    for err in errors:
        print(f"  {err['method']} {err['path']} -> {err['status']}")
else:
    print("所有请求成功")

attempts = 0
max_retries = 3
success = False

while attempts < max_retries and not success:
    attempts += 1
    print(f"尝试 {attempts}...")
    success = attempts >= 2   # 模拟第二次尝试成功

print("已连接" if success else "所有重试后失败")