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

控制流

docs.scrimba.com

你之前写的每个程序的运行方式都是一样的:从上到下,一次一行。这对简单的脚本有效,但真实的程序需要做出决策和重复工作。测验需要检查答案是否正确。游戏需要一直运行到玩家赢或输。本章介绍如何让程序分支和重复。

控制流塑造了程序采取的路径。条件(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,它问的是不同的问题(在深度探究中介绍)。

比较返回 bool,在真实代码中有两个细节很重要。首先,Python 支持链接比较:a < b < c 读作 a < b and b < c,中间值 b 只计算一次,如果它是昂贵的调用,这很重要。其次,== 检查两个值是否相等,而 is 检查恒等性(两个名称是否指向内存中的同一个对象)。它们不可互换:用 == 比较值,只在 NoneTrueFalse 中保留 is,你实际上想要那一个唯一的对象。在字符串或数字上使用 is 会导致在小输入上通过、在大输入上失败的错误。

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 翻转结果。这让你能表达真实的条件,比如"分数及格且用户活跃"。

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 翻转答案。这涵盖了你将写的大多数真实条件,比如"分数及格用户活跃"。Python 一旦知道结果就停止检查。
Juno组合条件and 在第一个假值处停止,or 在第一个真值处停止,两者都返回实际的操作数,不总是返回一个纯 True/False。这就是为什么当 x 为空时 x and x.method() 保持安全的原因。
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__ 方法来询问它是或否(一个dunder,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,不是无声错误。
Junoif / elif / else 条件按顺序惰性计算,所以首先放最便宜或最可能的测试,并排列重叠的界限是紧的在松的之前,否则一个分支消亡。缩进是结构,不是样式,制表符对空格是 TabError 而不是无声转换。

单行条件

对于简单的是/否赋值,Python 有一个称为三元表达式的紧凑单行形式:value_if_true if condition else value_if_false。仅当逻辑很小并且读起来像一个句子时才使用它。

条件表达式(三元运算符)根据条件计算为两个值之一。它是一个表达式,不是语句,所以它可以出现在预期值的任何地方:在 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单行条件value_if_true if condition else value_if_false 在单行上选择两个值之一。它对于整齐的小选择很好,比如 "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 的原因是可靠性:手动版本邀请你忘记增量或在错误的地方碰撞的错误,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() 惰性、超出计数器免费,它删除了整个"忘记增加"一个手动索引邀请的错误,所以默认使用它。每个项是一个元组,这就是为什么 `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 时执行。这是"搜索和报告如果未找到"的习惯用法模式,不需要一个单独的找到标志变量。

循环 else 仅当循环完成而没有 break 时运行,这与大多数人读取它的相反(它不是"否则,循环没有运行")。那个命名是完全为什么它困扰读者的原因,所以生产中的规则是为一件事和一件事仅使用它:搜索和未找到的模式,其中 break 标记一个命中,else 报告一个漏掉。它替代你否则会通过循环携带的 found = False 标志。其他任何地方,巧妙性成本在困惑的评论者中比它节省的更多,所以 else 上的注释值得这一行。

python
target = "Dave"
names = ["Alice", "Bob", "Charlie"]

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`。它是整洁的"搜索和报告如果未找到"模式,没有找到标志需要。保持它为那一个使用,因为名字困扰人。
Juno循环-else 它运行当循环在没有 `break` 的情况下结束时,这是词读起来的倒数。保持它为搜索和未找到情况,其中它替代 `found = False` 标志,并注释它。其他任何地方它成本比它节省的在困惑的评论者中更多。

排序

sorted() 返回一个新的排序列表并保持原始列表不变。.sort() 就地排序列表并返回 Nonekey= 参数让你按原始值以外的东西排序。例如,区分大小写地排序名称或按分数排序玩家元组。

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

Python 的排序是稳定的,意味着比较相等的项保持它们的原始顺序,这让你分阶段排序:首先按名称排序,然后按分数,有相同分数的玩家保持名称顺序。key= 函数每元素运行一次,不是每比较一次,所以一个昂贵的键被计算 n 次,不是 n 对数 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] 是一个单行函数:它取一个玩家元组并返回分数。Lambda 在Lambda 和推导章节中得到充分处理。

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

Juno排序sorted() 移交一个新的排序列表并单独保留原始,.sort() 就地重新安排列表并返回 None。传递 key= 按原始值以外的东西排序,比如不区分大小写的名称。有疑问时,使用 sorted(),它是安全的。
Juno排序sorted() 永不触及原始,.sort() 就地改变并返回 None。两者都采用 reverse=True 和一个 key= 函数在比较前应用于每个元素。默认为 sorted();仅当你意思改变列表时使用 .sort()
Juno排序 排序是稳定的,所以分阶段排序并且平局保持它们的顺序。key= 每元素运行一次,不是每比较一次,所以不要预缓存它。和 .sort() 故意返回 Nonex = 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 "在所有重试后失败")