Skip to content

Latest commit

 

History

History
808 lines (588 loc) · 48.7 KB

File metadata and controls

808 lines (588 loc) · 48.7 KB

三、迭代和决策

"Insanity: doing the same thing over and over again and expecting different results." – Albert Einstein

在上一章中,我们研究了 Python 的内置数据类型。现在您已经熟悉了各种形式和形状的数据,现在是时候开始研究程序如何使用它了。

根据维基百科:

In computer science, control flow (or alternatively, flow of control) refers to the specification of the order in which the individual statements, instructions or function calls of an imperative program are executed or evaluated.

为了控制程序流,我们有两个主要武器:条件编程(也称为分支)和循环。我们可以在许多不同的组合和变体中使用它们,但在本章中,我不想以文档的方式讨论这两种结构的所有可能形式,而是给你一些基本知识,然后我将与你一起编写一些小脚本。在第一个例子中,我们将看到如何创建一个基本的素数生成器,而在第二个例子中,我们将看到如何根据优惠券向客户应用折扣。这样,您就可以更好地了解如何使用条件编程和循环。

在本章中,我们将介绍以下内容:

  • 条件规划
  • Python 中的循环
  • 快速浏览 itertools 模块

条件规划

条件编程,或分支,是你每天、每时每刻都在做的事情。关于评估条件:*如果灯是绿色的,那么我可以通过;*如果下雨,我就带伞;如果我上班迟到,我会打电话给我的经理

主要工具是if语句,它有不同的形式和颜色,但基本上它对表达式求值,并根据结果选择要执行的代码部分。像往常一样,让我们看一个例子:

# conditional.1.py
late = True 
if late: 
    print('I need to call my manager!') 

这可能是最简单的例子:当输入到if语句时,late充当一个条件表达式,在布尔上下文中进行计算(与调用bool(late)完全相同)。如果评估结果为True,则我们在if语句之后立即输入代码主体。请注意,print指令是缩进的:这意味着它属于if子句定义的范围。执行此代码将产生:

$ python conditional.1.py
I need to call my manager!

因为lateTrue,所以执行了print语句。让我们扩展一下这个例子:

# conditional.2.py
late = False 
if late: 
    print('I need to call my manager!')  #1 
else: 
    print('no need to call my manager...')  #2 

这次我设置了late = False,所以当我执行代码时,结果不一样:

$ python conditional.2.py
no need to call my manager...

根据评估late表达式的结果,我们可以输入 block#1或 block#2,但不能同时输入 block。当late求值为True时执行块#1,当late求值为False时执行块#2。尝试将False/True值分配给late名称,并查看此代码的输出如何相应地更改。

前面的示例还引入了else子句,当我们想提供一组替代指令,当表达式在if子句中的计算结果为False时,它会变得非常方便。else 子句是可选的,通过比较前面两个示例可以看出这一点。

一个专门的 elif

有时,你所需要做的就是在满足某个条件的情况下做一些事情(一个简单的if子句)。在其他情况下,如果条件为Falseif/else子句),您需要提供一个替代方案,但在某些情况下,您可能有两个以上的路径可供选择,因此,由于调用管理器(或不调用它们)是一种二进制类型的示例(您调用或不调用),让我们改变示例的类型并继续扩展。这一次,我们决定税率。如果我的收入少于 10000 美元,我将不纳税。如果在 1 万到 3 万美元之间,我要付 20%的税。如果在 3 万到 10 万美元之间,我将支付 35%的税款,如果超过 10 万美元,我将(高兴地)支付 45%的税款。让我们把这些都写进漂亮的 Python 代码中:

# taxes.py
income = 15000 
if income < 10000: 
    tax_coefficient = 0.0  #1 
elif income < 30000: 
    tax_coefficient = 0.2  #2 
elif income < 100000: 
    tax_coefficient = 0.35  #3 
else: 
    tax_coefficient = 0.45  #4 

print('I will pay:', income * tax_coefficient, 'in taxes') 

执行前面的代码产生:

$ python taxes.py
I will pay: 3000.0 in taxes

让我们逐行检查示例:我们首先设置收入值。在这个例子中,我的收入是 15000 美元。我们加入if条款。请注意,这一次我们还引入了elif子句,它是else-if的收缩,它不同于裸else子句,因为它也有自己的条件。因此,income < 10000if表达式计算为False,因此不执行块#1

控制传递给下一个条件评估器:elif income < 30000。这一个求值为True,因此执行块#2,正因为如此,Python 在整个if/elif/elif/else子句(从现在起我们可以称之为if子句)之后恢复执行。在if条款之后只有一条指示,print电话,告诉我们今年我将支付3000.0税款(1500020%*。请注意,顺序是强制性的:if首先出现,然后(可选)根据需要添加尽可能多的elif子句,然后(可选)添加else子句。

很有趣,对吧?无论每个块中有多少行代码,当其中一个条件的计算结果为True时,将执行关联的块,然后在整个子句之后继续执行。如果没有一个条件评估为True(例如,income = 200000,则将执行else子句的主体(方框#4。这个例子扩展了我们对else子句行为的理解。当前面的if/elif//../elif表达式均未计算为True时,执行其代码块。

尝试修改income的值,直到您可以随意轻松地执行所有块(当然,每次执行一个)。然后尝试边界。这是至关重要的,无论何时,只要你有表示为等式不等式==!=<><=>=的条件,这些数字代表边界。必须彻底测试边界。我应该允许你 18 岁还是 17 岁开车?我是在用age < 18还是age <= 18检查你的年龄?您无法想象我有多少次不得不修复由于使用错误的运算符而产生的细微错误,所以请继续使用前面的代码进行实验。将某些<更改为<=,并将收入设置为边界值之一(10000、30000、100000)以及介于两者之间的任何值。查看结果如何变化,并在继续之前对其进行充分了解。

现在我们来看另一个例子,它向我们展示了如何嵌套if子句。假设您的程序遇到错误。如果警报系统是控制台,则打印错误。如果警报系统是电子邮件,我们会根据错误的严重程度发送警报。如果警报系统不是控制台或电子邮件,我们不知道该做什么,因此我们什么也不做。让我们将其转化为代码:

# errorsalert.py
alert_system = 'console'  # other value can be 'email' 
error_severity = 'critical'  # other values: 'medium' or 'low' 
error_message = 'OMG! Something terrible happened!' 

if alert_system == 'console': 
    print(error_message)  #1 
elif alert_system == 'email': 
    if error_severity == 'critical': 
        send_email('admin@example.com', error_message)  #2 
    elif error_severity == 'medium': 
        send_email('support.1@example.com', error_message)  #3 
    else: 
        send_email('support.2@example.com', error_message)  #4 

前面的例子很有趣,因为它很愚蠢。它向我们展示了两个嵌套的if子句(外部内部。它还告诉我们,外部的if子句没有任何else,而内部的子句有。注意缩进是如何允许我们将一个子句嵌套在另一个子句中的。

如果执行了alert_system == 'console',则执行了主体#1,并且没有其他事情发生。另一方面,如果alert_system == 'email',那么我们进入另一个if子句,我们称之为内部。在内部if条款中,根据error_severity,我们向管理员、一级支持或二级支持发送电子邮件(阻止#2#3#4。本例中未定义send_email函数,因此尝试运行该函数会导致错误。在这本书的源代码中(你可以从网站上下载),我加入了一个技巧,将调用重定向到一个常规的print函数,这样你就可以在控制台上进行实验,而不必发送电子邮件。尝试更改这些值,看看它们是如何工作的。

三元算子

在继续下一个主题之前,我想向大家展示的最后一件事是三元运算符或者,用外行的话说,if/else子句的简短版本。当名称的值要根据某种条件赋值时,有时使用三元运算符而不是适当的if子句更容易、更可读。在以下示例中,两个代码块的作用完全相同:

# ternary.py
order_total = 247  # GBP 

# classic if/else form 
if order_total > 100: 
    discount = 25  # GBP 
else: 
    discount = 0  # GBP 
print(order_total, discount) 

# ternary operator 
discount = 25 if order_total > 100 else 0 
print(order_total, discount) 

对于这种简单的情况,我发现能够用一行而不是四行来表达这种逻辑是非常好的。请记住,作为一名程序员,阅读代码的时间比编写代码的时间要多得多,因此 Python 的简洁性是非常宝贵的。

你清楚三元运算符是如何工作的吗?基本上,name = something if condition else something-else。因此,如果condition计算为True,则分配name,如果condition计算为False,则分配something-else

既然您已经了解了控制代码路径的所有内容,那么让我们进入下一个主题:循环

循环

如果您有在其他编程语言中进行循环的经验,您会发现 Python 的循环方式有点不同。首先,什么是循环?循环意味着根据给定的循环参数,能够重复执行代码块不止一次。有不同的循环结构,它们服务于不同的目的,Python 将所有这些结构提炼为两个,您可以使用它们来实现所需的一切。这些是forwhile语句。

虽然使用它们中的任何一个都可以做任何你需要的事情,但它们有不同的用途,因此它们通常在不同的上下文中使用。我们将在本章中彻底探讨这一差异。

for 循环

for循环用于在序列(如列表、元组或对象集合)上循环。让我们从一个简单的示例开始,并扩展这个概念,看看 Python 语法允许我们做什么:

# simple.for.py
for number in [0, 1, 2, 3, 4]: 
    print(number) 

这个简单的代码片段在执行时会打印出从04的所有数字。向for循环输入列表[0, 1, 2, 3, 4],并在每次迭代时,从序列中给number一个值(按顺序迭代),然后执行循环体(打印行)。number值在每次迭代时都会发生变化,根据序列中下一个值的变化而变化。当序列耗尽时,for循环终止,循环后的代码恢复正常执行。

在一个范围内迭代

有时我们需要迭代一系列的数字,如果必须在某个地方对列表进行硬编码,这将是非常不愉快的。在这种情况下,range功能起到了救援作用。让我们看看与前面代码片段等效的代码:

# simple.for.py
for number in range(5): 
    print(number) 

当涉及到创建序列时,range函数在 Python 程序中被广泛使用:您可以通过传递一个值来调用它,该值充当stop(从0开始计数),或者您可以传递两个值(startstop),甚至三个值(startstopstep)。请查看以下示例:

>>> list(range(10))  # one value: from 0 to value (excluded)
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
>>> list(range(3, 8))  # two values: from start to stop (excluded)
[3, 4, 5, 6, 7]
>>> list(range(-10, 10, 4))  # three values: step is added
[-10, -6, -2, 2, 6]

目前,请忽略我们需要将range(...)包装在list中。range对象有点特殊,但在本例中,我们只是想了解它将返回给我们什么值。您可以看到,该交易与切片相同:start被包含,stop被排除,您可以选择添加一个step参数,默认为1

试着在我们的simple.for.py代码中修改range()调用的参数,看看它打印了什么。适应它。

在序列上迭代

现在我们有了所有迭代序列的工具,因此让我们以该示例为基础:

# simple.for.2.py
surnames = ['Rivest', 'Shamir', 'Adleman'] 
for position in range(len(surnames)): 
    print(position, surnames[position]) 

前面的代码为游戏增加了一点复杂性。执行将显示以下结果:

$ python simple.for.2.py
0 Rivest
1 Shamir
2 Adleman

让我们用由内而外的技术来分解它,好吗?我们从我们试图理解的最深处开始,向外扩展。所以,len(surnames)surnames列表的长度:3。因此,range(len(surnames))实际上转化为range(3)。这给了我们范围[0,3],它基本上是一个序列(012)。这意味着for循环将运行三次迭代。在第一次迭代中,position将取值0,而在第二次迭代中,它将取值1,最后在第三次也是最后一次迭代中取值2。这是什么(012),如果不是surnames列表的可能索引位置?在0位置,我们会找到'Rivest'1'Shamir'位置和2'Adleman'位置。如果您对这三个人一起创造的东西感到好奇,请将print(position, surnames[position])更改为print(surnames[position][0], end=''),添加一个最终版本print()在循环之外,再次运行代码。

现在,这种循环方式实际上更接近于诸如 java 或 C++的语言。在 Python 中,很少看到这样的代码。您可以只对任何序列或集合进行迭代,因此无需在每次迭代时获取位置列表并从序列中检索元素。它很贵,不必要的贵。让我们将示例更改为更具 Python 风格的形式:

# simple.for.3.py
surnames = ['Rivest', 'Shamir', 'Adleman'] 
for surname in surnames: 
    print(surname) 

这才是关键!实际上是英语。for循环可以迭代surnames列表,并在每次交互时按顺序返回每个元素。运行此代码将打印三个姓氏,一次打印一个。它更容易阅读,对吗?

如果你想把职位也印出来呢?或者如果你真的需要它呢?你应该回到range(len(...))表格吗?不可以,您可以使用enumerate内置功能,如下所示:

# simple.for.4.py
surnames = ['Rivest', 'Shamir', 'Adleman'] 
for position, surname in enumerate(surnames): 
    print(position, surname) 

这段代码也很有趣。请注意,enumerate 在每次迭代时都会返回一个两元组(position, surname),但它仍然比range(len(...))示例更具可读性(也更高效)。您可以使用start参数调用enumerate,例如enumerate(iterable, start),它将从start开始,而不是从0开始。这只是另一件小事,它向您展示了在设计 Python 时付出了多少心思,从而使您的生活更加轻松。

您可以使用一个for循环来迭代列表、元组,以及通常 Python 称之为 iterable 的任何内容。这是一个非常重要的概念,让我们再多谈一点。

迭代器与可重用性

根据 Python 文档(https://docs.python.org/3/glossary.html ),一个 iterable 是:

An object capable of returning its members one at a time. Examples of iterables include all sequence types (such as list, str, and tuple) and some non-sequence types like dict, file objects, and objects of any classes you define with an iter() or getitem() method. Iterables can be used in a for loop and in many other places where a sequence is needed (zip(), map(), ...). When an iterable object is passed as an argument to the built-in function iter(), it returns an iterator for the object. This iterator is good for one pass over the set of values. When using iterables, it is usually not necessary to call iter() or deal with iterator objects yourself. The for statement does that automatically for you, creating a temporary unnamed variable to hold the iterator for the duration of the loop.

简单地说,当您编写for k in sequence: ... body ...时,会发生的情况是for循环向sequence请求下一个元素,它得到一些东西,它调用这些东西k,然后执行它的主体。然后,for循环再次询问sequence下一个元素,它再次调用它k,并再次执行主体,依此类推,直到序列用尽。空序列将导致主体的零执行。

有些数据结构在迭代时会按顺序生成元素,如列表、元组和字符串,而有些则不会,如集合和字典(在 Python 3.6 之前)。Python 使我们能够使用一种称为迭代器的对象类型,在 iterables 上进行迭代。

根据官方文件(https://docs.python.org/3/glossary.html ),迭代器是:

An object representing a stream of data. Repeated calls to the iterator's next() method (or passing it to the built-in function next()) return successive items in the stream. When no more data are available a StopIteration exception is raised instead. At this point, the iterator object is exhausted and any further calls to its next() method just raise StopIteration again. Iterators are required to have an iter() method that returns the iterator object itself so every iterator is also iterable and may be used in most places where other iterables are accepted. One notable exception is code which attempts multiple iteration passes. A container object (such as a list) produces a fresh new iterator each time you pass it to the iter() function or use it in a for loop. Attempting this with an iterator will just return the same exhausted iterator object used in the previous iteration pass, making it appear like an empty container.

如果你没有完全理解前面的所有法律术语,不要担心,到时候你会明白的。我把它放在这里作为将来的参考。

实际上,整个 iterable/迭代器机制在某种程度上隐藏在代码后面。除非出于某种原因需要编写自己的 iterable 或迭代器,否则您不必太担心这个问题。但了解 Python 如何处理控制流的这一关键方面非常重要,因为它将塑造您编写代码的方式。

在多个序列上迭代

让我们看另一个例子,如何迭代两个相同长度的序列,以便成对地处理它们各自的元素。假设我们有一份人员名单和一份代表第一份名单中人员年龄的数字名单。我们希望在一行中为所有人打印一对人/年龄。让我们从一个例子开始,逐步完善它:

# multiple.sequences.py
people = ['Conrad', 'Deepak', 'Heinrich', 'Tom']
ages = [29, 30, 34, 36]
for position in range(len(people)):
    person = people[position]
    age = ages[position]
    print(person, age)

到目前为止,这段代码应该非常简单易懂。我们需要迭代位置列表(0123,因为我们想要从两个不同的列表中检索元素。执行它,我们得到以下结果:

$ python multiple.sequences.py
Conrad 29
Deepak 30
Heinrich 34
Tom 36

这段代码既低效又不通俗。这是低效的,因为检索给定位置的元素可能是一个昂贵的操作,我们在每次迭代中都从头开始。邮递员不会每次送信都回到路的起点,对吧?他们从一家搬到另一家。从一个到下一个。让我们尝试使用enumerate使其更好:

# multiple.sequences.enumerate.py
people = ['Conrad', 'Deepak', 'Heinrich', 'Tom']
ages = [29, 30, 34, 36]
for position, person in enumerate(people):
    age = ages[position]
    print(person, age)

这更好,但仍然不完美。而且还是有点难看。我们在people上正确地迭代,但我们仍然使用位置索引获取age,我们也希望丢失它。不用担心,Python 给了你zip函数,记得吗?让我们使用它:

# multiple.sequences.zip.py
people = ['Conrad', 'Deepak', 'Heinrich', 'Tom']
ages = [29, 30, 34, 36]
for person, age in zip(people, ages):
    print(person, age)

啊!!好多了!再次将前面的代码与第一个示例进行比较,欣赏 Python 的优雅。我想展示这个例子的原因有两个。一方面,我想让您了解 Python 中较短的代码如何与语法不允许您轻松迭代序列或集合的其他语言相比较。另一方面,更重要的是,请注意,for循环询问zip(sequenceA, sequenceB)下一个元素时,它会返回一个元组,而不仅仅是一个对象。它返回一个元组,元组中的元素数量与我们提供给zip函数的序列数量相同。让我们以两种方式对前面的示例进行一点扩展,使用显式赋值和隐式赋值:

# multiple.sequences.explicit.py
people = ['Conrad', 'Deepak', 'Heinrich', 'Tom']
ages = [29, 30, 34, 36]
nationalities = ['Poland', 'India', 'South Africa', 'England']
for person, age, nationality in zip(people, ages, nationalities):
    print(person, age, nationality)

在前面的代码中,我们添加了国籍列表。现在我们将三个序列反馈给zip函数,for 循环在每次迭代中返回一个三元组。请注意,元组中元素的位置与zip调用中序列的位置有关。执行代码将产生以下结果:

$ python multiple.sequences.explicit.py
Conrad 29 Poland
Deepak 30 India
Heinrich 34 South Africa
Tom 36 England

有时,由于在前一个简单示例中可能不清楚的原因,您可能希望分解for循环体中的元组。如果这是你的愿望,那么完全有可能做到:

# multiple.sequences.implicit.py
people = ['Conrad', 'Deepak', 'Heinrich', 'Tom']
ages = [29, 30, 34, 36]
nationalities = ['Poland', 'India', 'South Africa', 'England']
for data in zip(people, ages, nationalities):
    person, age, nationality = data
    print(person, age, nationality)

它基本上是在做for循环自动为您做的事情,但在某些情况下,您可能希望自己做。这里,来自zip(...)的三元组datafor循环体中分解为三个变量:personagenationality

while 循环

在前面的几页中,我们看到了for循环的作用。当您需要在序列或集合上循环时,它非常有用。当您需要能够区分使用哪种循环构造时,需要记住的关键点是当您必须迭代有限数量的元素时,for循环会晃动。这可能是一个巨大的数目,但仍然是在某个时刻结束的。

然而,还有其他一些情况,当您只需要循环直到满足某些条件,或者甚至无限期地循环直到应用程序停止,例如我们实际上没有什么东西可以迭代,因此for循环将是一个糟糕的选择。但不用担心,在这些情况下,Python 为我们提供了while循环。

while循环类似于for循环,因为它们都是循环的,并且在每次迭代中都执行一系列指令。它们之间的不同之处在于while循环不会在序列上循环(可以,但您必须手动写入逻辑,这没有任何意义,您只需要使用for循环),相反,只要满足特定条件,它就会循环。当条件不再满足时,循环结束。

像往常一样,让我们看一个例子,它将为我们澄清一切。我们要打印正数的二进制表示。为了做到这一点,我们可以使用一个简单的算法,收集除以2的余数(按相反顺序),结果是数字本身的二进制表示:

6 / 2 = 3 (remainder: 0) 
3 / 2 = 1 (remainder: 1) 
1 / 2 = 0 (remainder: 1) 
List of remainders: 0, 1, 1\. 
Inverse is 1, 1, 0, which is also the binary representation of 6: 110

让我们编写一些代码来计算数字 39:1001112的二进制表示:

# binary.py
n = 39
remainders = []
while n > 0:
    remainder = n % 2  # remainder of division by 2
    remainders.insert(0, remainder)  # we keep track of remainders
    n //= 2  # we divide n by 2

print(remainders)

在前面的代码中,我突出显示了n > 0,这是保持循环的条件。通过使用divmod函数,我们可以使代码稍微短一点(并且更加 Pythonic),该函数用一个数字和一个除数调用,并返回一个元组,其中包含整数除法的结果及其余数。例如,divmod(13, 5)将返回(2, 3),实际上52+3=13*:

# binary.2.py
n = 39
remainders = []
while n > 0:
    n, remainder = divmod(n, 2)
    remainders.insert(0, remainder)

print(remainders)

在前面的代码中,我们在一行中将n重新分配给除以2的结果和余数。

请注意,while循环中的条件是继续循环的条件。如果计算为True,则执行主体,然后执行另一个计算,依此类推,直到条件计算为False。发生这种情况时,循环将立即退出,而不执行其主体。

If the condition never evaluates to False, the loop becomes a so-called infinite loop. Infinite loops are used, for example, when polling from network devices: you ask the socket whether there is any data, you do something with it if there is any, then you sleep for a small amount of time, and then you ask the socket again, over and over again, without ever stopping.

能够在一个条件上循环,或者无限期地循环,这就是为什么仅仅使用for循环是不够的,因此 Python 提供了while循环。

By the way, if you need the binary representation of a number, check out the bin function.

为了好玩,让我们使用 while 逻辑改编其中一个示例(multiple.sequences.py):

# multiple.sequences.while.py
people = ['Conrad', 'Deepak', 'Heinrich', 'Tom']
ages = [29, 30, 34, 36]
position = 0
while position < len(people):
    person = people[position]
    age = ages[position]
    print(person, age)
    position += 1

在前面的代码中,我强调了position变量的初始化条件更新,这使得可以通过手动处理迭代变量来模拟等价的for循环代码。使用for循环可以完成的所有事情也可以使用while循环完成,尽管您可以看到为了达到相同的结果,您必须通过一些样板文件。反之亦然,但除非你有理由这么做,否则你应该为工作使用正确的工具,99.9%的时间你会没事的。

因此,概括地说,当您需要迭代 iterable 时,使用for循环,当您需要根据是否满足的条件进行循环时,使用while循环。如果您牢记这两个目的之间的差异,就永远不会选择错误的循环构造。

现在让我们看看如何改变循环的正常流。

break 和 continue 语句

根据手头的任务,有时需要更改循环的规则流。您可以跳过单个迭代(任意次数),也可以完全打破循环。跳过迭代的一个常见用例是,例如,当您在一个项目列表上进行迭代时,只有在某些条件得到验证时,您才需要处理其中的每一个项目。另一方面,如果您在迭代一组项目,并且发现其中一个项目满足了您的某些需求,那么您可能会决定不完全继续循环,从而中断循环。有无数可能的场景,所以最好看几个例子。

假设您希望对篮子列表中到期日为今天的所有产品应用 20%的折扣。实现这一点的方法是使用continue语句,它告诉循环构造(forwhile)立即停止主体的执行,并转到下一个迭代(如果有)。这个例子将把我们带到兔子洞更深的地方,所以准备好跳跃:

# discount.py
from datetime import date, timedelta

today = date.today()
tomorrow = today + timedelta(days=1)  # today + 1 day is tomorrow
products = [
    {'sku': '1', 'expiration_date': today, 'price': 100.0},
    {'sku': '2', 'expiration_date': tomorrow, 'price': 50},
    {'sku': '3', 'expiration_date': today, 'price': 20},
]

for product in products:
    if product['expiration_date'] != today:
        continue
    product['price'] *= 0.8  # equivalent to applying 20% discount
    print(
        'Price for sku', product['sku'],
        'is now', product['price'])

我们首先导入datetimedelta对象,然后设置产品。将sku作为13的产品的有效期为today,这意味着我们希望对其提供 20%的折扣。我们在每个product上循环并检查有效期。如果不是(不等式运算符,!=``today,我们不想执行 body suite 的其余部分,所以我们continue

请注意,在 body 套件中的什么位置放置continue语句并不重要(您甚至可以多次使用它)。当您到达它时,执行停止并返回到下一个迭代。如果我们运行discount.py模块,这是输出:

$ python discount.py
Price for sku 1 is now 80.0
Price for sku 3 is now 16.0

这表明主体的最后两行尚未针对sku编号2执行。

现在我们来看一个打破循环的示例。假设我们想知道当馈送到bool函数时,列表中是否至少有一个元素的计算结果为True。考虑到我们需要知道是否至少有一个,当我们找到它时,我们不需要继续扫描列表。在 Python 代码中,这转化为使用break语句。让我们把它写进代码中:

# any.py
items = [0, None, 0.0, True, 0, 7]  # True and 7 evaluate to True

found = False  # this is called "flag"
for item in items:
    print('scanning item', item)
    if item:
        found = True  # we update the flag
        break

if found:  # we inspect the flag
    print('At least one item evaluates to True')
else:
    print('All items evaluate to False')

前面的代码是编程中常见的模式,您将看到很多。当您以这种方式检查项目时,基本上您要做的是设置一个flag变量,然后开始检查。如果您发现一个元素与您的条件相匹配(在本例中,该元素的计算结果为True),那么您将更新该标志并停止迭代。迭代之后,检查标志并采取相应的操作。执行结果:

$ python any.py
scanning item 0
scanning item None
scanning item 0.0
scanning item True
At least one item evaluates to True

查看True被发现后执行是如何停止的?break语句的行为与continue语句完全相同,因为它立即停止执行循环体,但也阻止任何其他迭代运行,从而有效地中断循环。在forwhile循环结构中,continuebreak语句可以一起使用,但数量不受限制。

By the way, there is no need to write code to detect whether there is at least one element in a sequence that evaluates to True. Just check out the built-in any function.

特别条款

我只在 Python 语言中见过的一个特性是在whilefor循环之后有else子句。它很少使用,但拥有它确实很好。简言之,您可以在forwhile循环后拥有else套房。如果循环由于迭代器耗尽(for循环)或最终不满足条件(while循环)而正常结束,则执行else套件(如果存在)。如果执行被break语句中断,则不执行else子句。让我们以一个for循环为例,循环遍历一组项目,寻找一个符合某个条件的项目。如果我们没有找到至少一个满足条件的,我们想提出一个异常。这意味着我们希望阻止程序的正常执行,并发出信号,表明存在无法处理的错误或异常。异常将成为第 8 章测试、分析和处理异常的主题,因此如果您现在还没有完全理解它们,请不要担心。请记住,它们将改变代码的常规流程。

现在让我向您展示两个做完全相同事情的示例,但其中一个是使用特殊的for...else语法。假设我们想在一群人中找到一个会开车的人:

# for.no.else.py
class DriverException(Exception):
    pass

people = [('James', 17), ('Kirk', 9), ('Lars', 13), ('Robert', 8)]
driver = None
for person, age in people:
    if age >= 18:
        driver = (person, age)
        break

if driver is None:
    raise DriverException('Driver not found.')

再次注意flag模式。我们将驱动程序设置为None,然后如果我们找到一个,我们更新driver标志,然后在循环结束时,我们检查它是否找到一个。我有一种感觉,这些孩子会驾驶一辆非常金属的汽车,但无论如何,请注意,如果找不到司机,DriverException就会升起,向程序发出执行无法继续的信号(我们缺少司机)。

使用以下代码可以更优雅地重写相同的功能:

# for.else.py
class DriverException(Exception):
    pass

people = [('James', 17), ('Kirk', 9), ('Lars', 13), ('Robert', 8)]
for person, age in people:
    if age >= 18:
        driver = (person, age)
        break
else:
    raise DriverException('Driver not found.')

请注意,我们不再被迫使用flag模式。异常是作为for循环逻辑的一部分提出的,这很有意义,因为for循环正在检查某些条件。我们只需要设置一个driver对象,以防我们找到一个,因为代码的其余部分将在某个地方使用该信息。请注意,代码更短、更优雅,因为逻辑现在正确地分组在它所属的位置。

In the Transforming Code into Beautiful, Idiomatic Python video, Raymond Hettinger suggests a much better name for the else statement associated with a for loop: nobreak. If you struggle remembering how the else works for a for loop, simply remembering this fact should help you.

把这些放在一起

现在,您已经了解了有关条件和循环的所有内容,是时候让事情变得更加有趣了,看看我在本章开头所预期的两个示例。我们将在这里混合搭配,这样您就可以看到如何将所有这些概念结合使用。让我们从编写一些代码开始,生成一个素数列表,并达到一定的限制。请记住,我将编写一个非常低效和基本的算法来检测素数。对您来说,重要的是集中精力在代码中属于本章主题的那些位上。

主发电机

根据维基百科:

A prime number (or a prime) is a natural number greater than 1 that has no positive divisors other than 1 and itself. A natural number greater than 1 that is not a prime number is called a composite number.

基于这个定义,如果我们考虑前 10 个自然数,我们可以看到 2, 3, 5 和 7 是素数,而 1, 4, 6、8, 9 和 10 不是。为了让计算机告诉你一个数字N是否为素数,你可以将该数字除以[2,N范围内的所有自然数)。如果这些除法中有一个除法的余数为零,那么这个数就不是素数。说得够多了,我们开始吧。我将写两个版本,第二个版本将利用for...else语法:

# primes.py
primes = []  # this will contain the primes in the end
upto = 100  # the limit, inclusive
for n in range(2, upto + 1):
    is_prime = True  # flag, new at each iteration of outer for
    for divisor in range(2, n):
        if n % divisor == 0:
            is_prime = False
            break
    if is_prime:  # check on flag
        primes.append(n)
print(primes)

在前面的代码中有很多东西需要注意。首先,我们建立了一个空的primes列表,它将包含末尾的素数。极限是100,你可以看到它包含在外循环中,我们称之为range()。如果我们写了range(2, upto),那就是*[2,高达*,对吧?因此range(2, upto + 1)给我们*[2,高达+1】==[2,高达】*。

因此,有两个for循环。在外层,我们循环遍历候选素数,即从2upto的所有自然数。在这个外循环的每个迭代中,我们设置一个标志(在每个迭代中设置为True,然后开始将当前n除以2n - 1之间的所有数字。如果我们为n找到一个合适的除数,这意味着n是复合的,因此我们将标志设置为False并中断循环。请注意,当我们打破内部的一个,外部的一个继续正常运行。我们在找到n的适当除数后中断的原因是,我们不需要任何进一步的信息来判断n不是素数。

当我们检查is_prime标志时,如果它仍然是True,这意味着我们在【2】n中找不到任何数字是n的适当除数,因此n是一个素数。我们将n附加到primes列表中,然后跳!继续另一次迭代,直到n等于100

运行此代码将产生:

$ python primes.py
[2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71, 73, 79, 83, 89, 97] 

在我们继续之前,有一个问题:在外循环的所有迭代中,其中一个不同于其他所有迭代。你能告诉我是哪一个吗?为什么?想一想,回到代码,试着自己弄明白,然后继续阅读。

你明白了吗?如果没有,不要难过,这是完全正常的。我让你把它作为一个小练习,因为这是程序员一直在做的事情。通过简单地查看代码来理解代码所做的事情的技能是随着时间的推移而构建的。这是非常重要的,所以尽可能地锻炼它。我现在就告诉你答案:行为不同于所有其他迭代的迭代是第一个。原因是在第一次迭代中,n2。因此,最里面的for循环甚至不会运行,因为它是一个迭代range(2, 2)for循环,如果不是[2,2],那又是什么呢?你自己试试,用这个 iterable 编写一个简单的for循环,在 body suite 中放一个print,看看是否发生了什么(不会……)。

现在,从算法的角度来看,这段代码效率低下,所以让我们至少让它变得更漂亮:

# primes.else.py
primes = []
upto = 100
for n in range(2, upto + 1):
    for divisor in range(2, n):
        if n % divisor == 0:
            break
    else:
        primes.append(n)
print(primes)

好多了,对吧?is_prime标志消失,当我们知道内部for循环没有遇到任何break语句时,我们将n追加到primes列表中。看看代码看起来如何更干净,读起来更好?

打折

在本例中,我想向您展示一种我非常喜欢的技术。在许多编程语言中,除了if/elif/else构造之外,无论它们以何种形式或语法出现,您都可以找到 Python 中缺少的另一条语句,通常称为switch/case。它相当于级联的if/elif/../elif/else子句,语法与此类似(警告!JavaScript 代码!):

/* switch.js */
switch (day_number) {
    case 1:
    case 2:
    case 3:
    case 4:
    case 5:
        day = "Weekday";
        break;
    case 6:
        day = "Saturday";
        break;
    case 0:
        day = "Sunday";
        break;
    default:
        day = "";
        alert(day_number + ' is not a valid day number.')
}

在前面的代码中,我们使用了一个名为day_number的变量switch。这意味着我们得到它的值,然后决定它适合什么情况(如果有的话)。从15有一个级联,这意味着无论数量多少,15都会下降到将day设置为"Weekday"的逻辑位。然后我们有06的单个案例,还有一个default案例以防止错误,它会提醒系统day_number不是有效的日数,即不在06中。Python 完全能够使用if/elif/else语句实现这样的逻辑:

# switch.py
if 1 <= day_number <= 5:
    day = 'Weekday'
elif day_number == 6:
    day = 'Saturday'
elif day_number == 0:
    day = 'Sunday'
else:
    day = ''
    raise ValueError(
        str(day_number) + ' is not a valid day number.')

在前面的代码中,我们使用if/elif/else语句在 Python 中重现 JavaScript 片段的相同逻辑。最后我举了一个例子,如果day_number不在06中,我就提出了ValueError异常。这是翻译switch/case逻辑的一种可能方式,但也有另一种方式,有时称为分派,我将在下一个示例的最后一个版本中向您展示。

By the way, did you notice the first line of the previous snippet? Have you noticed that Python can make double (actually, even multiple) comparisons? It's just wonderful!

让我们从新的示例开始,简单地编写一些代码,根据客户的优惠券价值为他们分配折扣。这里我将把逻辑降到最低,记住我们真正关心的是理解条件和循环:

# coupons.py
customers = [
    dict(id=1, total=200, coupon_code='F20'),  # F20: fixed, £20
    dict(id=2, total=150, coupon_code='P30'),  # P30: percent, 30%
    dict(id=3, total=100, coupon_code='P50'),  # P50: percent, 50%
    dict(id=4, total=110, coupon_code='F15'),  # F15: fixed, £15
]
for customer in customers:
    code = customer['coupon_code']
    if code == 'F20':
        customer['discount'] = 20.0
    elif code == 'F15':
        customer['discount'] = 15.0
    elif code == 'P30':
        customer['discount'] = customer['total'] * 0.3
    elif code == 'P50':
        customer['discount'] = customer['total'] * 0.5
    else:
        customer['discount'] = 0.0

for customer in customers:
    print(customer['id'], customer['total'], customer['discount'])

我们从建立一些客户开始。他们有一个订单总数、一个优惠券代码和一个 ID。我制作了四种不同类型的优惠券,两种是固定的,两种是基于百分比的。您可以看到,在if/elif/else级联中,我相应地应用了折扣,并将其设置为customer字典中的'discount'键。

最后,我只是打印出部分数据,看看我的代码是否正常工作:

$ python coupons.py
1 200 20.0
2 150 45.0
3 100 50.0
4 110 15.0

这段代码很容易理解,但所有这些子句都有点混乱逻辑。一眼就看出发生了什么并不容易,我也不喜欢。在这种情况下,您可以利用字典发挥优势,如下所示:

# coupons.dict.py
customers = [
    dict(id=1, total=200, coupon_code='F20'),  # F20: fixed, £20
    dict(id=2, total=150, coupon_code='P30'),  # P30: percent, 30%
    dict(id=3, total=100, coupon_code='P50'),  # P50: percent, 50%
    dict(id=4, total=110, coupon_code='F15'),  # F15: fixed, £15
]
discounts = {
    'F20': (0.0, 20.0),  # each value is (percent, fixed)
    'P30': (0.3, 0.0),
    'P50': (0.5, 0.0),
    'F15': (0.0, 15.0),
}
for customer in customers:
    code = customer['coupon_code']
    percent, fixed = discounts.get(code, (0.0, 0.0))
    customer['discount'] = percent * customer['total'] + fixed

for customer in customers:
    print(customer['id'], customer['total'], customer['discount'])

运行前面的代码会产生与前面代码片段完全相同的结果。我们保留了两行,但更重要的是,我们在可读性方面获得了很多,因为for循环的主体现在只有三行长,并且非常容易理解。这里的概念是将字典用作调度器。换句话说,我们尝试根据代码(我们的coupon_code)从字典中获取一些东西,并且通过使用dict.get(key, default),我们确保我们也能满足code不在字典中并且我们需要一个默认值的情况。

注意,为了正确计算折扣,我必须应用一些非常简单的线性代数。每个折扣在字典中都有一个百分比和固定部分,由两个元组表示。通过应用percent * total + fixed,我们可以获得正确的折扣。当percent0时,公式只给出固定金额,固定为0时给出percent * total

这项技术很重要,因为它也用于其他上下文中,包括函数,在这些上下文中,它实际上比我们在前面的代码片段中看到的功能强大得多。使用它的另一个优点是,您可以以动态获取discounts字典的键和值(例如,从数据库)的方式对其进行编码。这将允许代码适应任何折扣和条件,而无需修改任何内容。

如果你不完全清楚它是如何工作的,我建议你慢慢来,尝试一下。更改值并添加打印语句,以查看程序运行时发生的情况。

快速浏览 itertools 模块

如果没有几句关于itertools模块的话,一章关于可重用性、迭代器、条件逻辑和循环的内容是不完整的。如果你喜欢迭代,这是一种天堂。

根据 Python 官方文档(https://docs.python.org/2/library/itertools.htmlitertools模块为:

This module which implements a number of iterator building blocks inspired by constructs from APL, Haskell, and SML. Each has been recast in a form suitable for Python. The module standardizes a core set of fast, memory efficient tools that are useful by themselves or in combination. Together, they form an “iterator algebra” making it possible to construct specialized tools succinctly and efficiently in pure Python.

我在这里绝对没有空间向您展示您在本模块中可以找到的所有优点,因此我鼓励您亲自查看,我保证您会喜欢它。简而言之,它为您提供了三大类迭代器。我将给你们一个非常小的例子,从每一个迭代器中选取一个迭代器,只是为了让你们口水直流。

无限迭代器

无限迭代器允许您以不同的方式使用for循环,例如,如果它是while循环:

# infinite.py
from itertools import count

for n in count(5, 3):
    if n > 20:
        break
    print(n, end=', ') # instead of newline, comma and space

运行该代码可以得到以下结果:

$ python infinite.py
5, 8, 11, 14, 17, 20,

count工厂类生成了一个迭代器,该迭代器一直在计算。它从5开始,并不断添加3。如果我们不想陷入无限循环,我们需要手动打破它。

终止于最短输入序列的迭代器

这个类别很有趣。它允许您基于多个迭代器创建迭代器,并根据某些逻辑组合它们的值。这里的关键点是,在这些迭代器中,如果其中任何一个都比其他迭代器短,那么生成的迭代器不会中断,只要最短的迭代器用完,它就会立即停止。这是非常理论化的,我知道,所以让我用compress给你举个例子。此迭代器根据选择器中的相应项返回数据,该选择器为TrueFalse

compress('ABC', (1, 0, 1))将返回'A''C',因为它们对应于1。让我们看一个简单的例子:

# compress.py
from itertools import compress
data = range(10)
even_selector = [1, 0] * 10
odd_selector = [0, 1] * 10

even_numbers = list(compress(data, even_selector))
odd_numbers = list(compress(data, odd_selector))

print(odd_selector)
print(list(data))
print(even_numbers)
print(odd_numbers)

注意,odd_selectoreven_selector有 20 个元素长,而data只有 10 个元素长。compress将在data产生其最后一个元素后立即停止。运行此代码会产生以下结果:

$ python compress.py
[0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1]
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
[0, 2, 4, 6, 8]
[1, 3, 5, 7, 9]

这是从 iterable 中选择元素的一种非常快速和好的方法。代码非常简单,请注意,我们没有使用for循环来迭代压缩调用返回的每个值,而是使用list(),它也会执行相同的操作,但没有执行指令体,而是将所有值放入一个列表并返回。

组合生成器

最后但并非最不重要的是,组合生成器。如果你对这类事情感兴趣的话,这些真的很有趣。让我们看一个关于排列的简单例子。

根据 Wolfram Mathworld 的说法:

A permutation, also called an "arrangement number" or "order", is a rearrangement of the elements of an ordered list S into a one-to-one correspondence with S itself.

例如,ABC 有六种排列:ABC、ACB、BAC、BCA、CAB 和 CBA。

如果一个集合有N个元素,那么它们的排列数就是N!N阶乘)。对于 ABC 字符串,排列为3!=321=6。让我们用 Python 来做:

# permutations.py
from itertools import permutations 
print(list(permutations('ABC'))) 

这段非常短的代码片段产生以下结果:

$ python permutations.py
[('A', 'B', 'C'), ('A', 'C', 'B'), ('B', 'A', 'C'), ('B', 'C', 'A'), ('C', 'A', 'B'), ('C', 'B', 'A')]

玩排列时要非常小心。它们的数量增长的速度与你要排列的元素数量的阶乘成正比,这个数字可以变得非常大,非常快。

总结

在本章中,我们向扩展编码词汇表迈出了另一步。我们已经看到了如何通过计算条件来驱动代码的执行,以及如何在序列和对象集合上循环和迭代。这使我们能够控制代码运行时发生的事情,这意味着我们正在了解如何塑造代码,使其满足我们的需要,并对动态变化的数据做出反应。

我们还通过几个简单的示例了解了如何将所有内容组合在一起,最后,我们简要介绍了itertools模块,该模块中充满了有趣的迭代器,可以进一步丰富我们使用 Python 的能力。

现在是时候换个角度,向前迈出一步,谈谈功能了。下一章是关于它们的,因为它们非常重要。确保你对到目前为止被掩盖的内容感到满意。我想给大家提供一些有趣的例子,所以我得快一点。准备好的翻开这一页。