5. 条件和递归
本章主要话题是if表达式 , 它根据程序的状态执行不同的代码 .
但首先介绍两个操作符号 : 向下取整除法操作符和求模操作符 .
5.1 向下取整除法操作符和求模操作符
向下取整除法操作符 ( / / ) 对两个数除法运算 , 并向下取整得到一个整数 .
假设 , 一个电影的播放时长为 105 分钟 , 你肯能会想知道按小时算这是多长 .
传统的除法会得到一个浮点数 : ( 但是 , 在写小时数时通常并不用小数点 . )
>> > minutes = 105
>> > minutes / 60
1.75
向下取整除法 , 则丢弃小数部分 , 得到整数的小时数 :
>> > minutes = 105
>> > hours = minutes // 60
>> > hours
1
要求得余数 , 可以从分钟数中减去 1 小数 :
>> > remainder = minutes - hours * 60
>> > remainder
45
另一种办法是使用求模操作符 ( % ) 将两个数相除 , 得到余数 :
>> > remainder = minutes % 60
>> > remainder
45
求模操作符其实有很多实际用途 .
例如 , 可以用它来检测一个数是不是另一个的倍数-如果x % y是 0 , 则x可以被y整除 ( x除以y的意思 ) .
另外 , 也可以用它来获取一个数后一位或后几位数字 ( 这个数字是余数 ) .
例如 , x % y可以得到x的个位数 ( 10 进制 ) .
类似地 , x % 100 可以获得最后两位数 . 如果使用的是Pythond2 , 除法机制会有所不同 ,
除法操作符 ( / ) 在两个操作数都是整数的情况下 , 实际进行的是向下取整数除法操作 ,
而当两个操作数中有一个是浮点数时 , 则进行的浮点数除法 .
>> > 5 / 2
2
>> > 5.0 / 2
2.5
>> > 5 / 2.0
2.5
5.2 布尔表达式
布尔表达式是值为真或假的表达式 .
下面的例子中使用了 = = 操作符 , 来比较两个操作对象是否相等 , 如果相等 , 则得到True , 否则为False .
True和False是类型bool的两个特殊值 , 它们不是字符串 .
>> > 5 == 5
True
>> > 5 == 4
False >> > type ( True )
< class 'bool' > >> > type ( False )
< class 'bool' >
= = 操作符是一个关系操作符 , 其它的关系操作符有 :
x ! = y x不等于x
x > y x大于y
x < y x小于y
x > = y x大于或等于y
x < = y x小于或等于y
* 千万要注意不要写成 = > , = < . Python的操作符和数学的操作符还是有区别的 , 最常见的错误是使用单等号 ( = ) 而不是双等号 ( = = ) .
请注意 = 是一个赋值操作符 , 而 = = 是一个关系操作符 .
5.3 逻辑操作符
逻辑操作符有三个 : and , or , not . 这些操作符的语义和他们在英语中的意思差不多 .
例如 , x > 0 and x < 10 , 只有当x大于 0 且比 10 要小 , 结果才为真 , 否则为假 .
n % 2 = = 0 or n % 3 = = 0 , 当其中任意一个条件为真 , 结果才为真 , 否则为假 .
最后 , not操作符可以否定一个布尔表达式 , 所以not ( x > y ) 在x大于y时结果为假 , x小于y时结果为真 .
严格地说 , 逻辑操作符的操作对象应该是布尔值表达式 ,
但是Python并不那么严格 , 任何非 0 , 非空 ( None , 空字符串 , 空列表 . . . ) 的数都被解释为True .
>> > 42 and True
True
这种灵活性可能会很有用 , 但有时候也会带一些小困惑 ,
除非你很确切地知道自己在做什么 , 否则因该避免使用它 .
5.4 条件执行
为了编写有用的程序 , 我们几乎总是需要检查条件并据此改变程序的行为 . 条件语句给我们带来这种能力 .
简单的形式是if表达式 :
if x > 0 : print ( 'x is positive' )
if之后的布尔表达式被称为条件 ( condition ) . 如果它为真 , 则会执行后面缩进的语句 , 否则什么都不做 .
if表达式的结构和函数定义一样 , 一个语句头 , 接着是缩进的语句体 . 这种类型的语句称为复合语句 .
语句体中出现的语句数量并没有限制 , 但是最少需要一行 .
偶尔可能会遇到一个语句体什么都不做 ( 通常是标记一个你还没有来得及写的代码的位置 ) .
这个时候可以使用pass语句 , pass语句什么都不做 . ( 使用pass占位 , 避免语法错误 . )
if x < 0 : pass
5.5 选择执行
if语句的第二种形式是选择执行 , 这种形式下 , 有两种可能 , 而if的条件决定哪一种运行 .
语句看起来是这样的 :
if x % 2 == 0 : print ( 'x is even' ) else : print ( 'x is odd' )
如果x除以 2 的余数是 0 , 则我们知道x是偶数 ( even ) , 并且程序会显示合适的消息 'x is even' .
如果条件为假 , 则第二段语句会运行 , 因为条件必定是真假之一 , 所以必然只会有一段语句运行 .
这两段不同的语句成为分支 ( branch ) , 因为它们是程序执行流程中的两个支流 .
5.6 条件链
有时候有超过两种可能 , 所有我们需要更多的分支 .
表达这种计算的一种方式是条件链 ( chained conditional ) :
if x < y: print ( 'x is less than y.' ) elif x > y: print ( 'x is greater than y.' )
else : print ( 'x and y are equal.' )
elif是 'else if' 的缩写 , 和之前一样 , 只有一个分支会运行 . elif语句的数量没有限制 .
如果有一个else语句 , 则它必须放在最后 , 但也可以没有else语句 .
if choice == 'a' : draw_a( )
elif choice == 'b' : draw_b( )
elif choice == 'c' : draw_c( )
每个条件都按顺序检查 , 如果第一个条件是False , 则检查下一个条件 , 依次类推 .
如果有一个条件为真 , 则运行相应的分支 , 而整个语句结束 .
即使有很多个条件为真 , 也只有第一个为真的分支会运行 .
5.7 嵌套条件
条件判断可以再嵌套条件判断 , 我们可以修改前一节中的示例 , 如下 :
if x == y: print ( 'x and y are equal.' )
else : if x < y: print ( 'x is less than y.' ) else : print ( x is greater than y. )
外侧的条件语句包含两个分支 ( 顶格书写的语句 ) , 第一个分支包含一行简单的语句 .
第二个分支则包含了另一个if语句 , 它本身也有两个分支 ,
这两个分支也都是简单语句 , 虽然它们其实也可以是条件语句 ( 意思是说 , 这些简单的语句可以是条件语句 ) . 虽然语句的缩进让结构非常清晰 , 但嵌套条件语句会很快随着嵌套层数增多而变得非常难以阅读 ,
应该尽量避免它 .
逻辑操作符常常能够用来简化嵌套条件语句 . 例如 , 我们可以将下面的语句替换为单独的一个条件 :
if 0 < x: if x < 10 : print ( 'x is a positive single-digit number.' )
对于这种类型的条件 , Python还提供了一个更简洁地语法 :
if 0 < x < 10 : print ( 'x is a positive single-digit number.' )
5.8 递归
函数调用另外一个函数时合法的 ; 函数调用自己也是合法的 .
这样做有什么好处可能还不明显 , 但它其实是程序能做的最神奇的事情之一 . 例如 , 考虑下面的函数 :
def countdown ( n) : if n <= 0 : print ( 'Blastoff!' ) else : print ( n) countdown( n - 1 ) countdown( 3 )
如果n是 0 或负数 , 它会输出单词 'Blastoff!' ,
其它情况下 , 它会输出n , 并调用一个名为countdown的函数 ( 它自己 ) , 并传入实参n- 1.
我们调用这个函数时会发生什么? countdown的执行从n = 3 开始 , 因为n比 0 大 , 所有会输出 3 , 并接着调用自己 . . . 这时 , countdown的执行又从n = 2 开始 , 因为n比 0 大 , 所以会输出 2 , 并接着调用自己 . . . 这时 , countdown的执行又从n = 1 开始 , 因为n比 0 大 , 所以会输出 1 , 并接着调用自己 . . . 这时 , countdown的执行又从n = 0 开始 , 因为n不比 0 大 , 所以会输出单词 'Blastoff!' , 并返回 . 接收n = 1 的函数countdown返回 . 接收n = 2 的函数countdown返回 .
接收n = 3 的函数countdown返回 . 然后就会到了__main__函数 , 所以 , 全部的输出如下 :
3
2
1
Blastoff!
调用自己的函数称为递归的 ( recursive ) 函数 , 这个执行的过程叫作递归 ( recursion ) .
另外举一个例子 , 我们可以写一个函数打印某个字符串n次 .
def print_n ( s, n) : if n <= 0 : return print ( s) print_n( s, n - 1 ) print_n( 's' , 4 )
如果n < = 0 , return语句会直接退出当前函数 . 执行流程会立即返回到调用者 , 之后的语句不会运行 .
函数另外的部分和countdown类似 , 如果n大于 0 , 它会打印s并调用自己 , 再进行n- 1 次显示s的操作 .
所有输出行数是 1 + ( n - 1 ) , 也就是n .
s
s
s
s
对于这样简单的例子来说 , 可以使用for循环会更容易 .
当我们会在后面见到一些示例 , 使用for循环很难写 , 但使用递归则会很简单 , 所以早早开始了解递归是件好事 .
5.9 递归函数和栈图
在 3.10 节中 , 使用一个栈图来表示程序在进行函数调用时的状态 , 同样的栈图 , 可以用来帮助我们解释递归函数 .
一个函数每次被调用时 , Python会创建一个帧 ( function frame ) , 来包含函数的局部变量和参数 .
对于递归函数 , 栈上可能同时存在多个函数帧 .
下图展示了countdown函数在n = 3 调用时的栈图 .
和往常一样 , 栈的顶端是__mian__的函数栈 .
因为我们没有在__main__函数例新建任何变量或传入任何参数 , 所有它是空的 . 4 个countdown函数帧有不同的参数n , 最低端的栈 , 其n = 0 , 被称为基准情形 ( base case ) .
因为它不再镜像递归调用 , 所有后面没有其他函数帧了 .
作为练习 , 为函数print_n画一个栈图 , 其调用的实参s = 'Hello' 和n = 2.
然后写一个函数do_n , 接收一个函数对象和一个数字n作为形参 , 他会调用给定的函数n次 .
def print_hello ( part) : print ( part) def do_n ( func, n) : if n <= 0 : return do_n( func, n - 1 ) do_n( print_hello, 3 )
5.10 无限递归
如果一个递归永远达不到基准情形 , 则它会永远继续递归调用 , 而程序也永远不停止 .
这个现象被成为无限递归 , 而它并不是个好主意 , 下面是一个会引起无限递归的最简单函数 :
def recurse ( ) : recurse( ) recurse( )
大多数程序环境中 , 无限递归的函数并不会真的永远执行 , Python会在递归深度达到上限时报告一个出错消息 :
Traceback ( most recent call last ) : File "C:\Users\13600\PycharmProjects\test\test.py" , line 5 , in < module > recurse ( ) File "C:\Users\13600\PycharmProjects\test\test.py" , line 2 , in recurse recurse ( ) File "C:\Users\13600\PycharmProjects\test\test.py" , line 2 , in recurse recurse ( ) File "C:\Users\13600\PycharmProjects\test\test.py" , line 2 , in recurse recurse ( ) [ Previous line repeated 996 more times ]
RecursionError : maximum recursion depth exceeded .
[ 上一行重复 996 次 ] 递归错误:超过最大递归深度 .
当这个错误发生时 , 栈上已经有 1000 个recurse帧了 ( 默认值为 1000 , 某些电脑可能只能达到 99 x层 . ) !
如果你不小心写出了一个无线循环 , 请复查自己的函数 , 确认里面至少有一个基准情况不进行递归调用 .
如果已经有了一个基准情形 , 检查是否已经确保在运行时能达到它 .
5.11 键盘输入
目前为止我们写过的程序都不能接收用户的输入 , 它们只能每次做相同的事情 .
Python提供了一个内置函数input来从键盘获取输入并等待用户输入一些东西 .
当用户按下回车 , 程序会恢复运行 , 而且input则通过字符串的形式返回用户输入的内容 .
在Python 2 里 , 这个函数叫作raw_input .
>> > text = input ( )
Whate are you waiting for ?
>> > text
'Whate are you waiting for?'
在从用户那里获得输入之前 , 最好打印一个提示信息 , 告诉用户希望他们输入什么 .
input函数可以接受一个参数作为提示 :
>> > name = input ( 'What ... is your name?\n' )
What . . . is your name?
kid
>> > name
'kid'
提示信息最后的 \ n表示一个换行符 , 它是会引起输出显示换行的特殊字符 .
这也是为何用户的输入显示在提示信息的下一行的原因 .
如果希望有户输入一个整数 , 可以尝试将输入值转换为int :
>> > pormpt = 'What ... is the airspeed velocity of an unladen swallow?\n'
>> > speed = input ( pormpt)
42
>> > int ( speed)
42
但如果用户输入不是数字的话 , 会得到错误 :
>> > speed = input ( pormpt)
What . . . is the airspeed velocity of an unladen swallow?
What do you mean, an African or a European swallow?
>> > int ( speed)
Traceback ( most recent call last) : File "<stdin>" , line 1 , in < module>
ValueError: invalid literal for int ( ) with base 10 :
'What do you mean, an African or a European swallow?'
后面我们会看到如何处理这种错误 .
5.12 调试
当发生语法错误和运行时错误时 , 出错信息包含了大量的信息 , 但由时候反而会信息过量 , 最有用的信息是 :
* 错误的类型 ;
* 发生错误的地方 ;
语法错误通常很容易定位 , 但也有辣手之处 . 空格问题引起的错误很难难处理 ,
因为空格和制表符都是不可以见的 , 我们已经习惯与忽略它们 .
>> > x = 5
>> > y = 6 File "<stdin>" , line 1 y = 6
IndentationError: unexpected indent
这个例子中 , 问题的原因是第二行多缩进了一个空格 .
但出错消息指向的是y , 容易误导 .
总的来说 , 出错消息会告诉我们发生错误的地址 ,
但真正发生的地方可能在更前面的代码中 , 有时候甚至在前一行 .
运行时错误也是如此 , 假设你想要按照分贝来计算信噪比 , 公式为 : SBRdb = 10 lg ( Psignal / Pnoise ) .
在python中会这么写 :
import mathsignal_power = 9
noise_power = 10
ratio = signal_power // noise_power
decibels = 10 * math. log10( ratio)
print ( decibels)
在运行这个程序时 , 会得到一个异常 :
Traceback ( most recent call last ) : File "C:\Users\13600\PycharmProjects\test\test.py" , line 6 , in < module > decibels = 10 * math . log10 ( ratio )
ValueError : math domain error
出错的信息指向第五行 , 但那一行其实没有什么错误 , 要找到真正的错误 , 可能要打印出ratio的值 ,
结果你会发现是 0. 问题出在第四行 , 这个是使用向下取整除法而不是浮点数除法 .
你因该花一点时间认真阅读出错信息 , 但不要认为出现消息上说的每一样都对 .
5.13 术语表
向下取整除法 ( floor division ) : 用 ( / / ) 表示的操作符 , 用于将两个数相除 , 并对结果进行向下取整- ( 靠近 0 取整 ) , 得到整数结果 . 求模操作符 ( modulus operator ) : 用 ( % ) 表示的操作符 , 用于两个整数求模 , 返回两数相除的余数 . 布尔表达式 ( booleam expression ) : 一种表达式 , 其值是True或False . 关系操作符 ( relationnal operator ) : 用来表示两个操作对象的比较关系的操作符 , 如下之一 : = = , ! = , > , < , > = , < = . 条件语句 ( conditional statement ) : 依照某些条件控制程序执行流程的语句 . 条件 ( condition ) : 条件语句中的布尔表达式 , 由它决定执行哪一个分支 . 复合语句 ( compound statement ) : 一个包含语句头和函数体的语句 . 语句头以冒号 ( : ) 结尾 , 语句体相对语句头缩进一层 . 分支 ( branch ) : 条件语句中的一个可能性分支语句段 . 条件链语句 ( chained conditional ) : 一种包含多个分支的条件语句 . 嵌套条件语句 ( nested conditional ) : 在其它条件语句的分支中出现的条件语句 . 返回语句 ( return statement ) : 导致一个函数立即结束并返回到调用者的语句 . 递归 ( recursion ) : 在当前函数中调用自己的过程 . 基准情形 ( base case ) : 递归函数中的一个条件分支 , 里面不会再继续递归调用 . 无限递归 ( infinite recursion ) : 没有基准情形的递归 , 或者永远无法达到基准情形的分支的递归调用 . 最终 , 这种无限递归会导致运行时错误 .
5.14 练习
1. 练习1
time模块提供了一个函数 , 名字也叫time , 它能返回从 '纪元' 起到当前的格林尼治时间 .
'纪元' 其实在认为选作基准点的时间 , 在UNIX系统中 , 纪元时间点是 1970 年 1 月 1 日 .
>> > import time
>> > time. time( )
1678371019.427301
编写一个脚本 , 读写当前时间 , 并转换为一天中的小时数 , 分钟数 , 秒数 , 以及从纪元到现在的天数 .
import timenow_time = time. time( )
print ( now_time)
seconds_per_day = 86400
day = now_time // seconds_per_day
print ( day)
today_seconds = now_time % seconds_per_day
hours_seconds = 3600
hours = today_seconds // hours_seconds
print ( hours + 8 )
minute = ( today_seconds - hours * hours_seconds) // 60
print ( minute)
now_seconds = ( today_seconds - hours * hours_seconds) % 60
print ( now_seconds)
2. 练习2
费马大定理是说对于任何大于 2 的n , 不存在任何正整数a , b和c能够满足 : a * * n + b * * n = c * * n .
1. 编写一个函数check_fermat , 接收 4 个形参 ( 即a , b , c和n ) 并检查费马定理是否成力 , 如果n比 2 大并且满足a * * n + b * * n = c * * n , 则程序应当打印 '天哪, 费马弄错了' , 否则程序应当打印 '不, 那么样不行' .
def check_fermat ( a, b, c, n) : num1 = a ** n + b * nnum2 = c ** nprint ( num1, num2) if n > 2 and num1 == num2: print ( '天哪, 费马弄错了' ) else : print ( '不, 那样不行' ) check_fermat( a= 1 , b= 2 , c= 5 , n= 3 )
2. 编写一个函数 , 提示用户输入a , b , c和n的值 , 将它们转换为整数 , 并使用check_fermat来验证它们是否违背了费马定理 .
def check_fermat ( a, b, c, n) : num1 = a ** n + b * nnum2 = c ** nprint ( num1, num2) if n > 2 and num1 == num2: print ( '天哪, 费马弄错了' ) else : print ( '不, 那样不行' ) def input_num ( ) : a_str = input ( '提供参a的值>>>:' ) b_str = input ( '提供参b的值>>>:' ) c_str = input ( '提供参c的值>>>:' ) n_str = input ( '提供参n的值>>>:' ) a = int ( a_str) b = int ( b_str) c = int ( c_str) n = int ( n_str) check_fermat( a, b, c, n) input_num( )
3. 练习3
如果给你 3 更木棍 , 你可以将它们摆成一个三角形 , 也可能不可以 .
例如 , 如果一根木棍的长度是 12 英寸 , 而其它两根都只有 1 英寸 , 那么你无法让短的木棍在中间相接 .
对于任意三个长度 , 有一个简单的测试可以让它们是否可以组成一个三角形 :
如果其中任意一个长度的值大于其它两个长度的和 , 则你不能组成三角形 , 否则可以 .
( 如果任意一个长度等于第三个 , 在这儿它们组成一个 '退化' 的三角形 . ) 1. 编写一个函数is_triangle , 接收是三个参数 , 并根据这组长度的木棍是否能组成三角形来打印 'Yes' 或 'No' .
def is_triangle ( x, y, z) : if x + y >= z and x + z >= y and y + z >= x: print ( 'Yes' ) else : print ( 'No' ) is_triangle( 3 , 4 , 1 )
2. 编写一个函数提示用户输入 3 根木棍的长度 , 将其转换为整型 , 并使用is_triangle检查这些长度的木棍是否可以组成三角形 .
def is_triangle ( x, y, z) : if x + y >= z and x + z >= y and y + z >= x: print ( 'Yes' ) else : print ( 'No' ) def stick_length ( ) : a_str = input ( '输入第一条木棍的长度>>>:' ) b_str = input ( '输入第一条木棍的长度>>>:' ) c_str = input ( '输入第一条木棍的长度>>>:' ) a = int ( a_str) b = int ( b_str) c = int ( c_str) is_triangle( a, b, c) stick_length( )
4. 练习4
下面的程序的输出是什么? 泛化一个栈图来显示程序打印结果时的状态 .
def recurse ( n, s) : if n == 0 : print ( s) else : recurse( n - 1 , n + s) recurse( 3 , 0 )
1. 如果像recurse ( - 1 , 0 ) 这样调用这个函数 , 会发生什么?
recurse ( - 1 , 0 ) 调用 , 永远无法达到基准情形 , 程序会无限递归调用 , 最终导致运行时错误 :
递归错误 : 比较中超过了最大递归深度 .
2. 编写一段文档字符串 , 向人解释清楚要使用这个函数需要知道的东西 ( 并且不多写其它内容 ) .
def recurse ( n, s) : """函数递归调用n次, 当n=o时结束递归, 递归时, 计算加法运算 s = n + s.:param n: int, 值必须大于0.:param s: int:return:""" if n == 0 : print ( s) else : recurse( n - 1 , n + s) recurse( 4 , 0 )
5. 练习5
接下来的练习需要使用第 4 章描述的turtle模块 .
阅读下面的函数 , 并看看你能否能清楚它在做什么 ( 参考第 4 章中的实例 ) , 接着运行它 , 看你的理解是否真确 .
import turtlebob = turtle. Turtle( ) def draw ( t, length, n) : if n == 0 : return angle = 50 t. fd( length * n) t. lt( angle) draw( t, length, n - 1 ) t. rt( 2 * angle) draw( t, length, n - 1 ) t. lt( angle) t. bk( length * n)
draw( bob, length= 5 , n= 5 ) turtle. mainloop( )
6. 练习6
科赫曲线 ( Koch curve ) 是一个分形 , 它看起来像下图 ( 代码后面 ) 所示 .
要绘制一个长度为x的科赫曲线不只需要做 :
* 1. 绘制为x / 3 的科赫曲线 .
* 2. 向左转 60 度 . * 3. 绘制为x / 3 的科赫曲线 .
* 4. 向右转 120 度 . * 5. 绘制为x / 3 的科赫曲线 .
* 6. 向左转 60 度 . * 7. 绘制为x / 3 的科赫曲线 .
当x比 3 小的时候例外 : 在那种清理下 , 你可u直接绘制一个长度为x的直线 .
1. 编写一个函数koch , 接收一个Turtle对象以及长度x作为形参 , 并使用Turtle对象绘制指定长度的科赫曲线 .
import turtledef koch ( t, x) : """绘制科赫曲线的函数.参数:t -- Turtle对象x -- 曲线的长度返回值:None""" if x < 10 : t. fd( x) return koch( t, x / 3 ) t. left( 60 ) koch( t, x / 3 ) t. right( 120 ) koch( t, x / 3 ) t. left( 60 ) koch( t, x / 3 )
bob = turtle. Turtle( )
koch( bob, 100 )
turtle. mainloop( )
2. 编写一个函数snowflake , 绘制三条科赫曲线 , 组成一个雪花形状 .
解答 : https : / / github . com / AllenDowney / ThinkPython2 / blob / master / code / koch . py
import turtledef koch ( t, n) : if n < 10 : t. fd( n) return m = n / 3 koch( t, m) t. lt( 60 ) koch( t, m) t. rt( 120 ) koch( t, m) t. lt( 60 ) koch( t, m) def snowflake ( t, n) : for i in range ( 3 ) : koch( t, n) t. rt( 120 ) bob = turtle. Turtle( ) bob. pu( )
bob. goto( - 150 , 90 )
bob. pd( )
snowflake( bob, 300 ) turtle. mainloop( )
3. 科赫曲线而已用几种方法泛化 . ( 看一眼就得了 , 国外的网站 , 访问不了的 . )
查看 : http : / / en . wikipedia . org / wiki / Koch_snowflake