10月5日讲稿封面

10月5日讲稿

Python 基础语法课堂笔记,一次课连讲七讲中的条件判断与运算,并穿插六节复习。复习转义字符(换行符、制表符、回车符与原始字符串 r)、布尔类型与为假的值、None 占位、ASCII/Unicode/UTF-8 编码与 encode/decode、比较运算符与身份运算符;正课讲条件判断三种结构(if、if-else、if-elif-el…

Python 基础语法课堂笔记

(共七讲,按老师课堂讲授顺序整理)

第一讲条件判断与运算

这节课开始进入Python 的第一个语法结构:条件判断(分支语句)。老师说这节课比前面难,因为从这节课起不再只是认识数据类型,而是开始学结构。整节课分三块:先复习上节课内容,再讲作业,最后讲新内容(条件判断、运算与优先级、and 与or)。

复习一:转义字符

字符串与字符串相加是拼接,拼接时不会自己产生空格,拼接顺序取决于相加的顺序,加号前面的在左边,加号后面的在右边。

字符串与整数相乘是把字符串首尾连接重复若干次,它不会自动换行,因为代码里没有给它换行的命令。

换行要靠特定的命令,就是字符串里的反斜杠n,写作\n。它虽然叫换行符,但本质上仍然是字符串,必须写在引号里面,左右两边不能不写引号。

print('hello\nworld')

像这种有着特定功能、特定含义的字符串,就叫转义字符。常用的转义字符有:

\n 换行符,用得最多,打印时自动换一行。

\t 水平制表符,相当于按一个Tab。

\b 退格符(回退一个字符)。

\r 回车符,它把光标挪回行首,\r 后面的字符会把前面的字符覆盖掉。它来自打字机的回车,只把指针归到最左边。

在Windows 系统里敲回车,实际是两个字符:换行符加回车符;在Linux 系统里只有一个换行符。

单引号和双引号配合使用时,只能一种引号包裹另一种引号:单引号里可以直接写双引号,双引号里可以直接写单引号。

如果要在同一种引号里显示引号本身,就在引号前面加反斜杠,让它只表示一个普通引号,不再和最外层引号配对:

第 1 页
print('I\'m a student')

中文标点在Python 里就是普通字符,在中文引号前面加反斜杠没有任何转义意义,会原样打印出来。所以不要用中文引号写代码。

两个反斜杠打印出来是一个反斜杠。想在所有反斜杠前面都再加一个反斜杠,先下手为强,让它转义成一个反斜杠,后面的反斜杠就失去转义作用了。

更简单的办法是原始字符串:在字符串前面加一个小写或大写的r,它罩住的这一整块字符串都不承认任何转义字符,反斜杠就是普通反斜杠。

print(r'C:\Users\Desktop\test.txt')

原始字符串的缺点是:你真的想在里面用转义字符也用不了,宁可错杀三千,不放过一个。

三种可以直接用原始字符串的场景:第一,网址链接地址;第二,正则表达式;第三,文件路径地址。这三种地方都不会出现需要转义的字符。

补充:两个相邻字符串之间即使不写加号,也会自动首尾拼接,但这种写法可读性差,不推荐。

复习二:布尔类型

布尔类型总共只有两个值:True 和False,严格区分大小写,注意拼写(True 首字母大写,不是ture)。

True 代表真,False 代表假。它们一般出现在条件判断的条件里,比如比较两个数是否相等,对就是True,错就是False。

布尔类型有自己的强制转换函数bool(),它能把其他类型的值转成布尔值。不用记所有值,只需要记住为假的那些值,其余全部为真。

为假的值:False 本身、None、数字0(含0.0 等)、空字符串”、空列表、空元组、空字典等空容器。

数字里只有0 为假,非零数字都为真;字符串里只有空字符串为假。

老师建议不要把bool() 理解成“转换”,而要理解成每个数据身上都自带一个布尔属性,bool() 只是把这个属性取出来。这样后面看条件判断和循环才好理解。

第 2 页

复习三:None 类型

None 的类型名是NoneType,和int、float、str、bool 一样是一种类型名。None 类型只有一个值,就是None。

None 的意思是“没有意思”,意义就是“没有意义”,用来表示空无一物、占位。数据库里不能空着不填时,可以填一个类似null 的值表达“这里没有东西”。

如果暂时想不出变量该赋什么有意义的值,可以先写a = None 占位,它在语法上是合法的。

None 的布尔属性是False。于是常见的假值有四个:False、0、空字符串、None。

注意这四个值互相之间不相等,类型也不同,属性相等不等于值相等。0 还能做加减乘除,空字符串还能拼接,它们都带着自己所属类型的功能;只有None 真正表示空无一物。

None 不能和字符串相加,类型不同,会报TypeError。

复习四:编码格式

计算机里真正存储数据的是二进制,只有0 和1。

最小的存储单位叫位(bit),一个位只能放一个0 或1。

比位大一级的单位是字节(byte),1 个字节等于8 个位,所以一个字节能表示2的8 次方,即256 种不同的二进制组合。

让每一种组合对应一个字符,最多能对应256 个字符。最原始的ASCII 码标准只规定了128 个字符(英文字母大小写、数字、标点、控制命令),特点是一个字节对应一个字符。

汉字有上万个,一个字节甚至两个字节(2 的16 次方)都不够,需要三个字节(2的24 次方)才放得下。

于是先做一个字符集,把全世界每种语言的每个字符都编好序号,这就是万国码Unicode。字符集只到单个字符为止,不包含词语、成语、单词,因为字符组合数量太大,无法一一收录。

建立在Unicode 之上的可变长度存储方式最常用的是UTF-8:英文字符仍是一个字节,汉字是三个字节。

UTF-8 的前128 个字符和ASCII 完全一样,所以说UTF-8 是ASCII 的扩展是对的;ASCII 一定能在UTF-8 里正常解析,反过来则可能乱码。

encode 是编码,decode 是解码。用什么方式编码,就必须用什么方式解码,否则乱码。就像中文翻成英文要用中英字典,中文翻成法文就不能再用中英字典。

第 3 页

复习五:比较运算符与身份运算符

比较运算符用来比较大小关系,结果一定是布尔类型:==、!=、>、<、>=、<=。

Python 支持链式比较,例如1 < a < 5,这是上节课作业的要求。

字符串之间也能比较:按Unicode 编码序号逐字符比较,先比第一个字符,第一个相同再比第二个,依次类推。所以input 得到的字符串即使不转int 也能比较大小,但比的是字符串,不是数字。

身份运算符is 和is not 用来判断两个数据的内存地址(id)是否相同,即是不是同一个数据。

两个数据地址相同,a is b 为真,那么a == b 也一定为真;但反过来,两个值相等不一定是同一个数据(比如两个长得一模一样的列表,地址并不相同)。

两个值不相等,就一定不是同一个,因为一个地址上不可能同时放两个不同的值。

判断类型时建议写type(a) is int,它比type(a) == int 更顺口,也不会有黄色波浪线提醒。

常用的小整数在内存里有固定的地址,其他数据每次运行的地址可能变化。老师用公共课班长固定座位、其他同学随便坐的比喻帮助记忆。

作业讲评

第一题:用两种方式输出一句含有引号和反斜杠的话。

第一种,用转义处理转义:同种引号前加反斜杠,所有反斜杠前也统一加反斜杠。

第二种,用原始字符串r:里面所有转义都失效,但引号仍要成双成对、就近匹配;为了避免引号错误配,可以单双引号混合使用。

第二题:先输入范围的最小值和最大值,再输入一个整数,判断这个整数是否落在范围之内。

input 输入的是字符串,必须用int() 转成整数后再比较,然后用链式比较判断是否在区间内。

代码大致是:

第 4 页
a = int(input('请输入最小值:'))
b = int(input('请输入最大值:'))
num = int(input('请输入一个整数:'))
if a <= num <= b:
    print('在范围内')
else:
    print('不在范围内')

新内容一:条件判断if

生活中处处是条件判断:人脸识别打卡,是本人则打卡成功,不是本人则失败;手机人脸解锁,是本人则解锁,不是本人则打不开。

第一个关键字是if,意思是“如果”,它是黄色的关键字,要和后面的内容用空格隔开。

if 后面跟一个条件,条件的结果只有True 和False 两种。只有条件为真时,才执行里面的代码。

if 这一行的末尾必须写冒号。冒号表示“我要开始说话了”,接下来要写代码。

冒号下面的代码要缩进,缩进一般是四个空格,或者按一个Tab 键(Q 左边那个键)。

缩进表示从属关系:缩进里面的代码属于缩进上面的判断,只有条件成立才运行。同缩进就是同层级。

最基础的例子:

a = 2
b = 3
if a < b:
    print('python 是世界上最好的语言')

代码从上往下读:定义a 为2,b 为3;如果a < b 成立为真,就执行缩进里的打印。把2 改成6 后条件不成立,就不打印。

缩进里面不限制代码行数,只要属于这个缩进,条件成立时都会运行。

第二个关键字是else,意思是“其他的”。除了上面if 所声明的情况以外,其他所有情况都走else。

用else 的前提是必须先有if,else 不能单独出现。else 后面有冒号有缩进,但不能再跟条件,否则就像“除了张同学以外的其他同学,张同学、李同学……”一样啰嗦。

第 5 页

if 满足就执行if,if 不满足就执行else,二者构成二选一的互斥结构,有你没我。

a = 6
b = 3
if a < b:
    print('a 小于b')
else:
    print('a 不小于b')

如果一行print 没有缩进,顶格写,它就不属于任何一个结构,无论条件成立与否都会运行。读代码既要从上往下看,也要看缩进。

not 表示反义词:not False 等于True,not True 等于False。if not a > b 表示当a > b不成立时才执行。

if 后面可以直接放一个变量,比如if a,这就是在考验这个变量的布尔属性;变量为真就通过,为假就不通过。if not a 则相反。

注意input 的坑:input 输入的16 是字符串’16’,它和整数16 不相等,比较前要用int() 转换。

判断类型:type(a) == int 表示判断a 的类型是否是整数;推荐写type(a) is int,字符串同理type(b) is str。黄色波浪线只是格式提醒,不影响运行。

新内容二:if 的嵌套

如果写三个并列的独立if,它们之间没有任何联系,各自独立判断,你通过你的、我通过我的。

如果希望“通过第一关才能判断第二关,通过第二关才能判断第三关”,就要用嵌套写法,把第二个if 写进第一个if 的缩进里。

看代码从上往下看,也要从外往里看。缩进相同的是同辈,缩进多一层就晚一辈,是层层递进的关系。

第 6 页
a = 5
if a > 0:
    print('第一句')
    if a < 6:
        print('第二句')
        if a > 4:
            print('第三句')

如果第二层判断进不来,第三层判断根本不会被考虑,哪怕里面的条件满足也没关系。打游戏过关是这个道理,中考没考就谈不上保送大学。

else 里面也能嵌套,年龄分段的例子:

age = int(input('请输入你的年龄:'))
if age <= 11:
    print('儿童')
else:
    if age <= 14:
        print('少年')
    else:
        if age <= 17:
            print('青少年')
        else:
            print('青年')

走到else 就说明上面的条件不成立,年龄大于11;再往下判断14、17。每个嵌套里至少要有一个if,else 必须跟着if 出现。

新内容三:取整与取余

取整用两个斜杠//,取余用百分号%。

10 除以3 除不尽,三三得九余一,所以10 // 3 等于3,10 % 3 等于1。

10 能被2 整除,所以10 // 2 等于5,10 % 2 等于0。一个数能被另一个数整除,余数就是0。

2 除以3 时,没达到除数的要求,取整为0,余数为2。

第 7 页

新内容四:判断闰年(嵌套写法)

闰年规则有两条:

第一条,四年一闰,百年不闰:能被4 整除,但不能被100 整除,比如2004 年。

第二条,四百年再闰:能直接被400 整除,比如2000 年、2400 年。

1900 年、2100 年能被4 整除,也能被100 整除,但不能被400 整除,所以不是闰年。

判断流程:先判能否被4 整除,不能则直接不是闰年;能,再判能否被100 整除;不能被100 整除就是闰年(第一种情况);能,再判能否被400 整除,能则是闰年,不能则不是闰年。

用嵌套if 写成代码:

year = int(input('请输入年份:'))
if year % 4 == 0:
    if year % 100 == 0:
        if year % 400 == 0:
            print('此年份是闰年')
        else:
            print('此年份不是闰年')
    else:
        print('此年份是闰年')
else:
    print('此年份不是闰年')

判断能否整除就看取余是否为0:year % 4 == 0 表示能被4 整除。

老师强调写代码是一层一层写的,叫“摸着石头写代码”:先把当前层的骨架写好,再去填里面的内容,写完一句才有下一句。就像画画先画轮廓,再一点点细描,不是从纸头一路画到纸尾。

新内容五:elif 多选一

如果写几个独立的if,最后再配一个else,else 是就近原则,只和离它最近的那个if 构成二选一,并不能形成多选一,可能打印多个结果。

要想四选一,就要用elif(else if 的连写),意思是“其他如果”。

elif 不能用作开头,只能跟在if 后面;同一层级里可以有无数个elif;if、elif、else之间是互斥

第 8 页

的。

代码从上往下运行,只要前面有一个条件满足,下面的就全都不再运行,给出唯一的结果。

a = 19
if a >= 18:
    print(1)
elif a >= 12:
    print(2)
elif a >= 9:
    print(3)
else:
    print(4)

a 为19 时三个条件都满足,但只打印第一个1;a 为8 时前面都不满足,靠最后的else 兜底打印4。

如果没有最后的else,所有条件都不满足时就没有结果,所以else 是默默负重前行的兜底。

案例:BMI 体质指数

体质指数算法:体重用千克,身高用米,bmi 等于体重除以身高的平方。

分级标准:小于18.5 偏瘦;18.5 到25 正常;25 到28 超重;28 到32 肥胖;大于32 非常肥胖。

用多个独立if 会把偏瘦、正常、超重、肥胖都打印出来,结构错误。

用if / elif / else 能保证只打印一个结果,但不一定正确:范围必须从小到大。

如果先判断bmi < 32,那么无论是16 还是24 都小于32,会先命中“肥胖”,下面的判断全被跳过,结果唯一但错误。

正确写法是按范围从小到大:

第 9 页
bmi = 24
if bmi < 18.5:
    print('偏瘦')
elif bmi < 25:
    print('正常')
elif bmi < 28:
    print('超重')
elif bmi < 32:
    print('肥胖')
else:
    print('非常肥胖')

新内容六:算术运算与优先级

运算符优先级从高到低,老师按“江湖排名”讲:

第一名,小括号()。Python 里只有小括号能改变运算顺序,中括号、大括号都用来定义数据类型。多层小括号先算最里面的,再算外面的。

第二名,幂运算(指数运算),两个星号连在一起写**。2 ** 3 表示2 的三次方,等于8。

第三名,正负号。正号不改变值,负号取相反数。

第四名,乘、除、取整、取余:*、/、//、%,它们是同一级,同一级出现多个时从左往右算。

第五名,加法和减法+、-,同级也是从左往右。

除法/ 得到的结果是浮点数。

内置函数pow(a, b) 也能算幂,a 是底数,b 是次方数:pow(2, 3) 等于8。

内置函数divmod(a, b) 一次返回取整和取余两个结果,放在一个小括号里(以后要学的元组),第一个是取整,第二个是取余。

print(divmod(10, 3))
print(divmod(5, 2))

上面两行分别打印(3, 1) 和(2, 1)。

第 10 页

第六名,比较运算符。数学运算和比较同时出现时,先算两边的数学运算,再比较,比较的结果是布尔值。

第七名,逻辑运算符not、and、or,三者内部也有顺序:not 第一,and 第二,or 第三。

新内容七:and 与or

and 是“和”,把左右两个条件合并成一个条件,两边都为真才为真,只要有一边为假就为假。要求严格,像找对象既要求长得帅又要求长得高。

or 是“或”,一边为真就为真,两边都为假才为假;两边都为真当然也为真。要求宽松,长得帅或长得高满足一个即可。

例子:a = 6 时既能被2 整除也能被3 整除,and 条件成立;改成9 后不能被2 整除,and 条件不成立。

a = 6
if a % 2 == 0 and a % 3 == 0:
    print('能同时被2 和3 整除')

复合条件要按顺序算:先算数学运算,再做比较,然后加not,最后用and、or 连接。

本课最难点:短路原则(定性原则、惰性运算)

当and、or 连接的不是True、False,而是数字、字符串等具体值时,判断建立在每个数据的布尔属性上。

Python 做这种运算时有惰性运算的特点:代码从左往右读,读到哪个值能确定整个式子的结果,就立刻停下来(短路),并把这个值作为结果输出,后面的不再看。

判断分两步走:第一步,先给整个式子定性,看它整体为真还是为假;第二步,从左往右找,看谁能立刻决定这个结果,就输出谁。

or 的规律:一边为真即为真,所以从左往右找第一个为真的值,找到就返回它;如果全都为假,必须读到最后一个才能死心,返回最后一个值。

and 的规律:一边为假即为假,所以从左往右找第一个为假的值,找到就返回它;如果全都为真,必须读到最后一个才能确认,返回最后一个值。

老师的刮奖比喻:刮奖刮出“一台”就知道是一等奖,刮出“一包”就知道是洗衣粉,刮出“谢谢”就知道是谢谢惠顾,后面的字不用再刮了。

第 11 页

or 的例子:

print(1 or 2)
print(0 or 1)
print(0 or None)

1 or 2:两边都为真,读到1 就能定性为真,输出1。

0 or 1:0 为假不能定性,继续读到1 才确定为真,输出1。

0 or None:0 为假,右边可能为真,必须读完,最后读到None,输出None。

and 的例子:

print(0 and 10000)
print(1 and 0)
print(1 and 2)

0 and 10000:读到0 就知道一边为假,结果为假,输出0,右边一万、十万都不用看。

1 and 0:1 为真不能定性,继续读到0,0 决定它为假,输出0。

1 and 2:两边都为真才为真,1 不能单独决定,读到2 才能确认,输出2。

混合时and 优先级高于or,可以先手动加括号帮助阅读:

print(1 or 2 and 3 or 4 and 5 or 6)

先算2 and 3 得3,4 and 5 得5,式子变成1 or 3 or 5 or 6,最左边1 为真,一路短路,结果输出1。

回到闰年:简化写法

学了and、or 后,闰年不用再嵌套那么多层,两个条件用or 连接:第一种能被4 整除且不能被100 整除;第二种能被400 整除。满足其一就是闰年,否则是平年。

第 12 页
year = int(input('请输入年份:'))
if (year % 4 == 0 and year % 100 != 0) or (year % 400 == 0):
    print('闰年')
else:
    print('平年')

这里的小括号不影响运算结果(数学运算仍先于比较,and 仍先于or),但能让代码层级更清楚、可读性更强,是一举两得。

代码都是从复杂写到简单,再写到极简,但不要看不起复杂的嵌套写法,那是必经的来时路。复盘:这一讲的主线是先有条件(布尔值),再用if / else / elif 把条件组织成二选一、多选一的结构;判断条件靠比较运算符和逻辑运算符,运算符之间有严格的优先级。最容易绕的是and、or 连接具体值时的短路:先定性,再从左往右找能拍板的那个值。

作业:用if 和else 实现BMI 多分支判断,不准用elif。可以只用if,也可以用if 加else嵌套,但要达到和if/elif/else 一样的效果。

第二讲循环结构与异常捕获

这节课讲三个内容:循环结构(重点)、异常捕获(重点)、变量交换(较简单)。

课前复习提要

条件判断能让代码根据不同情况做不同操作。if 后面跟条件,条件是布尔值,为真就进入判断;冒号下面是缩进,缩进可以用一个Tab 或四个空格,全文件要统一,不要一会儿四个空格一会儿八个。

缩进表示从属关系,缩进里的代码可以有若干句,不限行数。else 必须和if 搭配,后面不能跟条件。顶格写的代码不属于任何结构,无论条件是否成立都运行。

not 表示对立面:not False 等于True,not True 等于False。if 后面直接写变量,是在看它的布尔属性;判断类型推荐type(a) is int 这种写法。

嵌套if 是层层递进,通过第一关才能进第二关;else 里也能嵌套,每层都必须有if。

取整//,取余%,能整除时余数为0。

闰年判断、if/elif/else 多选一、BMI 范围必须从小到大。

运算优先级:小括号、幂、正负号、乘除取整取余、加减、比较、逻辑(not、and、or)。

第 13 页

短路两步法:先给整个式子定性,再从左往右找能立刻决定结果的值。

pow(a, b) 算a 的b 次方,第三个参数c 很少用,加上表示a 的b 次方对c 取余。

divmod(a, b) 一次返回取整和取余两个结果。

作业讲评:不用elif 实现BMI 判断(三种写法)

题目要求:用if 和else 的组合,或者干脆只用if,实现BMI 的五个区间判断,不准用elif。老师给出三种常见写法。

写法一,从大范围到小范围逐层嵌套,这是正确写法:

bmi = 22
if bmi <= 32:
    if bmi <= 28:
        if bmi <= 25:
            if bmi < 18.5:
                print('偏瘦')
            else:
                print('正常')
        else:
            print('超重')
    else:
        print('肥胖')
else:
    print('非常肥胖')

读法:先看是不是小于等于32,不是则非常肥胖;是,再看28、25;能进到最内层说明18.5 <= bmi <= 25,再判18.5 分出偏瘦和正常。

错误演示一:把最严格的小范围(<18.5)放在最外层。没有数字会满足小于18.5却不满足小于25,等于一开始就把敌人拒之门外,里面排兵布阵全白费。嵌套的条件必须从大到小,层层诱敌深入。

错误演示二:每层都写成“大于等于下界、小于等于上界”的独立判断,看似结构一样,实际bmi 为29 时会连续打印正常、超重、肥胖三个结果。每个else 里再开一个新的独立if,就会出现多重人格式的多答案。

写法二,每层都手动把上下限写全(如else 里再判18.5 < bmi < 25)。结

第 14 页

果正确,但啰嗦:能走到else 就说明上面的条件不成立,它的对立面已经确定,没必要再把上一层的条件复制一遍。

写法三,全部用独立if,简单粗暴,是用得最多的:

bmi = 22
if bmi < 18.5:
    print('偏瘦')
if 18.5 <= bmi < 25:
    print('正常')
if 25 <= bmi < 28:
    print('超重')
if 28 <= bmi <= 32:
    print('肥胖')
if bmi > 32:
    print('非常肥胖')

虽然if 语句之间不互斥,但这里每个区间条件是互相独立、互不从属的,所以结果正确。代价是五个if 每次都会运行判断一遍,即使第一个就已经命中,后面的照样要跑,只是没通过。

注意:写法三里不能把最后一个if 改成else。因为else 会就近和最近的if 配对,结果可能打印出“正常”和“非常肥胖”两个答案。

另外强调一个变量的坑:变量不要定义在if 判断里面。

下面的代码里如果a 不等于1,b 就从未被定义,最后print(b) 会报NameError。变量是否存在不应取决于某个条件是否成立。

a = 1
if a == 1:
    b = 100
print(b)

新内容一:for 循环

循环就是一件事重复去做。

先看需求:有一个字符串a = ‘abcdefg’,共七个字符,想从左往右把每个字符一个一个拎出来,第

第 15 页

一次拿a,第二次拿b,一直到g。这要用for 循环。

for 是关键字,in 也是关键字,in 的意思是“在什么什么里面”。for 和in 中间的i 是变量名,使用前没有单独定义,它在循环里被动态赋值。

a = 'abcdefg'
for i in a:
    print(i)

运行过程:第一次i 等于a,第二次i 等于b 覆盖掉a,第三次c 覆盖b,依次到g,每拿到一个元素就执行一次缩进里的print,所以一共打印七次。

元素个数决定循环次数:数据有几个元素,缩进里的代码就重复执行几次。

循环结束后在循环外再print(i),i 仍然能用,值是最后一次循环赋的g。

如果缩进里打印的不是i,而是“我爱你”,照样打印七次。i 这个元素可以不用,只借用获取元素所产生的循环次数。

老师的苹果比喻:一箱苹果,拿一个不吃,每拿一个就拍你一下;吃不吃苹果不影响拍你,只是借拿苹果的次数决定拍你的次数。

缩进里有两句,就每次循环都执行两句,得到a 我爱你、b 我爱你…… 共七组。

每个print 默认打印完另起一行,因为print 有个参数end,默认值是换行符\n。

把end 设成横杠,相邻两个print 之间就用横杠连接,不再换行:

for i in range(10):
    print('python 是世界上最好的语言', end='-')

end 只管当前这一句和下一句print 之间用什么连接,而且只对相邻的下一句有效。循环里最后一次打印会和循环外紧跟的print 连在一起;再下一句是否连,取决于中间那句自己的end。

range 函数

整数不能被for 一个个取元素(123456789 作为整数无法区分元素),需要range 函数。

range 生成给定开头、给定结尾的连续整数序列,是左闭右开区间,包头不含尾。

range(0, 10000) 包含0 到9999,共一万个数字,不包含10000。

第 16 页

range 可以只填一个参数,range(10000) 默认从0 开始,效果和range(0, 10000) 一样。

所以想循环一万次,直接for i in range(10000) 即可,和i 最终最大是多少无关,只看产生了多少个数字。

专业名词:迭代遍历、迭代

把一个数据里的元素一个一个依次取出来,这个动作叫遍历。

每取出一个元素,就在循环里重复执行同一个操作(打印、运算等),叫迭代。

合起来叫迭代遍历:for 循环逐个取出元素,然后统一处理。

for 里的变量名不固定,i、j、k 都可以,但循环里用的名字要和前面写的变量名一致。

循环几次取决于数据的长度,也就是元素个数。

能被for 循环一个一个取出元素的数据类型,叫可迭代数据类型;不能的叫不可迭代数据类型。

目前学过的类型里,字符串是可迭代的;整数不可迭代,直接for 会报错(数字6无法确定按什么单位拆,不能也拆成0.5、1、1.5……)。

练习一:1 到100 累加

先设一个起始值sum_ 为0,变量名加下划线是为了避开内置函数sum。

range(1, 101) 包头不含尾,正好拿到1 到100。

sum_ = 0
for i in range(1, 101):
    sum_ += i
print('总和', sum_)

sum_ += i 是自增操作,等价于sum_ = sum_ + i:先算右边,0+1 得1,赋给左边,再1+2 得3,依此类推,最后结果5050。

print 可以同时打印多个值,用逗号隔开,显示在一排、中间用空格分开。

练习二:累乘

累加的起始值是0,因为加0 不改变值;累乘的起始值是1,因为乘1 不改变值。

第 17 页
product = 1
for i in range(1, 101):
    product *= i
print(product)

product *= i 等价于product = product * i,1 到100 连乘的结果非常大。

练习三:字符串反转

原字符串是abc,想用空字符串b 拼出反的cba。

a = 'abc'
b = ''
for i in a:
    b = i + b
print(b)

关键是每次把新元素拼到b 的前面:第一次b 为a,第二次b 变成ba,第三次变成cba。像排队改成后来者站前面,队伍就反了。

如果写成b = b + i,元素往后拼,得到的是原样abc。

练习四:去掉字符串中的空格

遍历原字符串,每个字符都会被取到,包括空格;只有当字符不等于空格时才拼进新字符串。

a = 'hello world 哈哈哈'
b = ''
for i in a:
    if i != ' ':
        b += i
print(b)

满足条件才拼接,不满足就跳过进入下一次循环,最后得到去掉空格的字符串。这里没有倒转,是因为元素仍然按顺序往后拼。

第 18 页

练习五:3 和5 的倍数

一个循环里可以放多个判断。要同时找出3 的倍数和5 的倍数,这里的两个if 必须是独立的,不能用elif。

for i in range(1, 101):
    if i % 3 == 0:
        print(i, '是3 的倍数')
    if i % 5 == 0:
        print(i, '是5 的倍数')

因为15 这样的公倍数既该打印“3 的倍数”又该打印“5 的倍数”,如果用elif 变成互斥,就只会打印一句。

如果一定要用elif,就先把公倍数单独拎出来,排除掉公倍数之后再互斥判断:

for i in range(1, 101):
    if i % 3 == 0 and i % 5 == 0:
        print(i, '是3 和5 的公倍数')
    elif i % 3 == 0:
        print(i, '是3 的倍数')
    elif i % 5 == 0:
        print(i, '是5 的倍数')

这是结构套结构:循环里套判断,每次循环都要完整走一遍判断。

break:结束整个循环

正常情况下range(10) 会打印0 到9。希望i 等于6 时提前结束,就用关键字break。

for i in range(10):
    if i == 6:
        break
    print(i)
print('哈哈哈哈')
第 19 页

打印0 到5 后,i 为6 时执行break,结束整个循环。它不是报错结束,而是正常结束:循环结束后,循环下面的print 照常运行。

综合游戏:猜数字

规则:计算机随机生成一个1 到100 的整数,玩家自己决定猜几次;猜小了提示小了,猜大了提示大了,猜中就结束;次数用完还没猜中就游戏结束。

生成随机数要先导入内置模块random,固定写法import random,先记住会用即可。

random.randint(1, 100) 生成1 到100 的随机整数,这是为数不多包头又包尾的函数,包含1 也包含100。

import random
a = random.randint(1, 100)
n = int(input('请输入你要猜的次数:'))
for i in range(n):
    guess = int(input('请输入你猜的数字:'))
    if guess == a:
        print('恭喜你猜对了')
        break
    elif guess < a:
        print('你猜的小了')
    else:
        print('你猜的大了')
print('游戏结束,不玩了')

最后一个分支可以用else:两个数字比较,除了相等、小于,剩下的只有大于,排除前两个就剩它。

三个必须想清楚的位置问题:

第一,random.randint 不能放进循环里,否则每次猜的都是一个新随机数,每次都是百分之一的概率,大小提示也失去意义。

第二,猜数字的input 不能放到循环外,否则总共只能猜一次,循环变成拿两个相同的数字重复比较十次。

第三,循环结束后的print 不管是猜中break 还是次数用完都会执行,所以放循环外。

第 20 页

新内容二:变量交换

需求:a = 1,b = 2,想让a 变成2、b 变成1。

错误写法a = b 再b = a:第一句执行后a 已经是2,第二句b = a 拿到的就是2,结果两个都变成2,原值被覆盖。

其他语言的通用做法是引入第三方变量暂存:

a = 1
b = 2
c = a
a = b
b = c

c 先把a 的1 存起来,a 再放心去拿b 的2,最后b 从c 拿回1。

Python 的做法更简单,一行搞定:

a = 1
b = 2
a, b = b, a

原理:等号赋值先看右边再看左边,右边按位置先把b 的2 和a 的1 准备好,再按位置依次赋给左边的a 和b,于是2 给a、1 给b。

新内容三:异常捕获

Python 是解释型语言,代码哪一行报错,就停在哪一行,后面的代码全部不运行。

报错很多时候不是代码的问题,比如爬虫运行时突然断网,代码本身没问题却无法运行。所以需要一套备用方案:不报错就正常走,报错了就走B 方案,不让整个程序崩掉。

第一个关键字是try,意思是努力尝试:写在try 冒号缩进里的代码,会尝试运行,一旦报错就被捕获。

try 不能单独出现,捕获了错误还必须说明报错后怎么办,这就是B 方案。

第二个关键字是except,意思是否则、不然:try 里正常时它不运行,一旦报错,立刻跳到except 执行里面的代码。

第 21 页
try:
    print(1 + '2')
except:
    print('hello world')

整数和字符串不能相加,try 里第一句报错,马上切换到except 打印hello world,整个程序不再出现红色报错。

把’2’ 改成数字2,不报错,就正常运行try,except 不运行。

注意try 里报错位置之后的语句不再执行,一旦报错立刻跳到except,不会把try 里剩下的代码跑完。

报错分类型,想知道具体错在哪,用Exception 表示捕获所有类型的错误,再用as给这次错误取个别名(通常叫e),打印e 就能看到报错原因。

try:
    a = int(input('请输入数字:'))
    print(a)
except Exception as e:
    print(e)

as 就是取别名,给这次捕获到的错误信息起个名字。

完整结构还有else 和finally:

else:try 里没有报错时,跟着执行;报错了就不执行。

finally:无论报不报错,最后都执行。

老师的调侃:else 人品不咋地,跟谁都不对付——在if 里和if 互斥,在except 后和except 互斥。

第 22 页
try:
    a = int(input('请输入数字:'))
    print(a)
except Exception as e:
    print(e)
else:
    print(a + 1)
finally:
    print('结束')

输入100 能正常转整数:打印100,再走else 打印101,最后走finally 打印结束。输入abc 无法转整数:走except 打印错误信息,else 不执行,finally 仍执行。

finally 为什么不直接顶格写?因为finally 表示跳出结构之前在结构内部完成的最后一步,涉及内存管理、文件操作时有区别,暂时先记住它属于这个结构。

综合案例:求每个字符的倒数

有一个字符串,里面字母和数字混杂。数字字符能转成整数,可以求倒数(1 除以它);字母转不了整数,就提示不能得到倒数。

for 循环里套异常捕获,每次循环都拿不同的i 走一遍完整的异常结构。

s = 'abcdef1234567890'
for i in s:
    try:
        num = int(i)
        print(1 / num)
    except Exception:
        print(i, '不能得到倒数')

a 到f 转不了整数,提示不能得到倒数;1 到9 分别得到1.0、0.5、0.333……

这里有个bug:0 能转成整数,转换不报错,但0 没有倒数,除数不能为0。所以要把1 / num 也放进try 里,让它一并捕获除零错误。报错有两种可能:转不了整数、0 没有倒数。

第 23 页

常见错误类型(先有概念,后面会反复遇到)

报错信息最下面一行的开头会直接写出错误类型,不用死记。

TypeError:类型错误,把操作或函数用在不适当类型的对象上,比如整数5 加字符串’10’。

ValueError:值错误,参数类型正确但值不合法,比如int(‘abc’),字符串类型对但内容不能转整数。

NameError:名称错误,变量没有定义就使用。

IndexError:索引错误(下标越界),列表等序列的索引从0 开始,访问不存在的编号就报这个错。

KeyError:键错误,字典里拿着不存在的键去取值。

ZeroDivisionError:除数为零错误。

IOError:输入输出错误,读取不存在、路径错误或无法访问的文件时引发。

ImportError:导入错误,import 了一个不存在的库。

AttributeError:属性或方法错误,调用了某种数据类型不存在的属性或方法。

KeyboardInterrupt:键盘中断,在input 等待时手动终止程序。

除了用Exception 捕获所有错误,也可以把Exception 换成具体类型,精准定向捕获,比如只except TypeError,则其他类型错误仍会报出。复盘:这一讲的主线是“重复”和“兜底”。for 循环的本质是迭代遍历可迭代数据,元素个数决定循环次数,range 用来制造指定次数的整数序列,break 提前终止循环。异常捕获的本质是给可能出错的代码准备B 方案:try 尝试、except 处理报错、else 处理不报错、finally 无论如何都收尾。

作业一:用for 循环计算0 到100 所有偶数的和。作业二:用for 循环计算0 到100 之间所有被3 除余1 的数字之和,包含1 和100。作业三:用try / except / else / finally 完整结构写一段异常捕获代码,自己决定每一句该放在哪个结构里。

第三讲列表

列表是Python 里最灵活、用得最多的数据类型,可以说没有之一。这节课只讲列表一种类型,要记的方法很多,老师几乎把常用方法都讲了一遍。

列表、集合、元组、字典这四种自带结构的数据类型合称数据结构,也叫数据容器:它们能把五花八门的不同数据装进一个容器里。

第 24 页

课前复习与作业讲评提要

for 循环的两个作用:一是迭代遍历,把数据里的元素一个个取出来用;二是只借用迭代产生的循环次数,元素本身可以不用。

循环次数由所遍历数据的长度决定;range 生成包头不含尾的连续整数,只填一个参数默认从0 开始。

print 的end 参数决定本句和下一句之间用什么连接,默认是换行符。

累加起始值是0,用+=;累乘起始值是1,用*=。

字符串反转靠把新元素拼到前面:b = i + b;去空格靠遍历时只拼接不是空格的字符。

两个独立if 不能乱改elif,否则公倍数只会命中一次;要互斥就先把公倍数单独拎出来。

break 只能用在循环里,结束整个循环但不影响循环后面的代码。

random.randint 是包头又包尾的特例,必须放循环外;猜数字的input 必须放循环内。

异常捕获:try 里可以写很多句,但一次只能捕获第一个报错的位置,第一句报错就直接跳到except;求倒数案例要把1 / num 也放进try,因为0 不能作除数。

偶数和作业的要点:range(0, 101) 才包含100;判断相等是两个等号==,一个等号是赋值。

print 的缩进位置决定打印次数:放循环外只打印最终结果;放判断里只在偶数命中时打印。0 到100 共101 个数,其中偶数(含0)51 个。

range(0, 101, 2) 也能取偶数,第三个参数是步长,每两个取第一个;但老师更推荐用if 判断,因为可读性、普适性更强,不用为每道题重新找规律。

不能写i += i 代替累加变量,每次循环i 都被重新赋值,结果变成0+0、2+2、4+4……

一次循环也可以同时完成两类累加:准备两个起始值,各归各的队伍,一次遍历分头判断。

异常作业标准结构:input 与int 转换放try;except 打印“不能转换为整数”;else 打印转换成功并继续处理;finally 打印“代码运行结束”。

列表的定义

列表用一对方括号(中括号)声明,里面可以放多个元素,元素之间用逗号分隔。

列表对元素的数据类型没有任何要求:整数、浮点数、字符串都可以,甚至列表本身也能作为一个元素。

第 25 页
a = [1, 2, 3.3, 'abcd', 5, '上山打老虎']

定义时是什么顺序,输出时就是什么顺序,所以列表是一种有序的数据结构。

列表的类型名是list,列表是可迭代数据类型,可以直接用for 遍历。

数据放进列表很安全:既不会改变元素的值,也不会改变元素的类型。

长度len

len() 函数可以获取数据的长度,长度就是元素的个数。

能被迭代遍历、能拆出一个个元素的数据类型才有长度。

注意:子列表自己即使有3 个元素,在外层列表里也只占一个位置,就像不管体重100 斤还是200 斤,在教室里都只占一个座位。

print(len(a))

索引(下标)

有序意味着每个元素都有位置。按照顺序给每个元素排的号叫索引,老版本也叫下标,相当于学生的学号。

索引不是从1 开始,而是从0 开始,相当于整体向左偏了一位。

列表有六个元素时,最大索引是5。上面的列表a 中:

索引0 是1,索引1 是2,索引2 是3.3,索引3 是’abcd’,索引4 是5,索引5 是’上山打老虎’。

取值语法:列表名+ 中括号+ 索引,用的是中括号不是小括号(小括号一般用于调用函数)。

print(a[0])
print(a[3])

这种方式只是按“座位号”查看元素,元素并不会离开列表,列表原样不变。

索引超过范围会越界,报IndexError。

空列表是一对方括号里什么都没有:[],它仍然是列表,布尔属性为False;空列表取a[0] 同样报IndexError,因为一个元素都没有。

第 26 页

列表里即使放了三个假值(0、False、None),列表本身的布尔属性仍为真,因为它是一个包含三个元素的非空列表,元素为假是元素自己的事。

遍历空列表既不报错也不打印:不报错是因为可迭代性由列表类型决定,空列表也是列表;不打印是因为没有元素驱动循环。

index 方法:通过元素找索引

这是第一次接触“列表名.方法名()”的调用方式,方法建立在列表自身身上。

index 方法返回指定元素在列表中的索引:

print(a.index('abcd'))

上面的结果是3。

不是所有变量都有index 方法:当a 指向整数时还去调a.index,会报AttributeError,因为这个方法不属于整数类型。

for i in a 拿到的仍然是元素本身,不是索引,学了索引后这点别搞混。

想同时打印元素和它的索引,可以在循环里再调用index:

for i in a:
    print(i, a.index(i))

列表里有重复元素时,index 从左往右只返回第一个匹配的位置。

index 还有两个可选参数:起始位置和结束位置,结束位置不包含,写法a.index(元素, 开始索引, 结束索引),在这个范围内找第一个匹配。

嵌套列表取值

当外层列表的索引6 放的是子列表[1, 2, 3] 时:

a = [1, 2, 3.3, 'abcd', 5, '上山打老虎', [1, 2, 3]]
print(a[6][1])

先a[6] 锁定子列表[1, 2, 3],再在它基础上取索引1,得到2,一层一层往里找。

第 27 页

切片(本节课最难点)

索引取值是一个一个取,切片是一段一段取。黄瓜既可以切成片,也可以切成段。

切片语法:中括号里用冒号分隔,形如a[开始:结束:步长]。

a[1:3] 从索引1 开始切,到索引3 为止,不包含3,包头不含尾,结果是[2, 3.3]。

a[3] 取到的是元素本身’abcd’;a[3:4] 虽然只有一个元素,但取到的是包含一个元素的列表[‘abcd’]。这是索引取值和切片的核心区别。

a[0:3] 可以简写成a[:3],冒号前面不填,默认从0(头)开始。

a[1:5] 不包含索引5;想切到最后,结束位置可以超过最大索引(比如a[1:100]),黄瓜总共就那么长,第二刀隔空切,取到的还是整段。

a[1:] 冒号后面不填,默认切到最后,包含最后一个元素。

a[:] 前后都不填,从头切到尾。

写成a[1:len(a)] 也包含最后一个元素:因为列表长度永远比最大索引大1(长度从1数,索引从0 数)。

第三个位置是步长,和range 的步长同理:每几个取第一个,默认步长是1。

print(a[1:5:2])

a[1:5:2] 从索引1 到5(不含5)每两个取第一个,得到[2, ‘abcd’]。

段内元素个数是奇数也没关系,Python 是惰性运算,只在自己要取的位置拿得到元素就取,不会验证后面有没有元素配对。

负索引:正号表示从左往右取,负号表示从右往左取。

从右往左第一个是-1,没有-0,因为-0 还是0,取到的仍是从左往右第一个。

a[-1] 取到最后一个元素’上山打老虎’。

步长也可以是负数:步长为2 是从左往右跳着取,步长为-2 是从右往左跳着取。

必须背下来的规则:第一个冒号前面不管正负、大小,永远是开始位置;第一个冒号后面永远是结束位置;第二个冒号后面永远是步长。

不要自作主张把大的数当结束,a[5:3:-1] 就是从索引5 开始、到索引3 结束、从右往左取。

第 28 页

只有步长方向和切片方向一致,才能切到东西;一个向左一个向右,配合不了,得到空列表。

a[-2:-6:-1] 从-2 开始、到-6 结束(不包含-6)、步长-1,得到[5, ‘abcd’, 3.3, 2]。

a[4::-1] 从索引4 一直取到开头,得到[5, ‘abcd’, 3.3, 2, 1]。

a[1:-1] 从索引1 到-1(不包含最后一个),得到[2, 3.3, ‘abcd’, 5]。

省略的位置到底是头还是尾,取决于步长方向:

a[:3] 步长为1,省略头,从头切到3;a[:3:-1] 步长为-1,省略的就是尾,从尾切到3,得到[‘上山打老虎’, 5]。

a[3::-1] 省略的是头,因为只有从头(0)到3 才是从右往左,与-1 一致。

关键细节:写出来的边界遵循包头不含尾,不包含;而省略的边界是包含的。a[3:0:-1] 不包含索引0,a[3::-1] 则包含索引0。

a[::] 步长为1,从头到尾;a[::-1] 步长为-1,从尾到头,正好实现列表反转。

老师的总结:省略的开头或结尾是墙头草,哪边能和步长方向保持一致、能取到东西,它就往哪边倒。

添加元素一:append

append 在列表末尾添加一个元素,把括号里的内容当做一个整体追加。

a = [1, 2, 3]
a.append(4)
a.append([6, 7])

上面append([6, 7]) 会把整个子列表[6, 7] 作为一个元素放进去,不会拆开。

添加元素二:extend

extend 把一个可迭代类型的数据拆开,把元素一个个放到末尾。

a.extend([6, 7])
a.extend('啦啦啦')

对比:append 把[6, 7] 当整体,列表增加1 个元素;extend 把[6, 7] 拆开,列表增加2 个元素

第 29 页

。

添加元素三:insert

insert 在指定位置插入元素,两个参数:第一个是位置,第二个是元素,先安排座位再让新同学坐进去。

a.insert(4, '哈哈哈哈')
a.insert(2, [2, 3])

insert 也是把内容作为整体插入,不会像extend 那样拆开。

循环中一般不使用insert:一个元素插队后,后面所有元素的位置都被推后一位,列表会变得不可控,像超市排队插队。实际用得最多的是append,先来的在前、后来的在后。

修改元素

按索引修改,等号赋值:

a[3] = '蝎子精'

也可以按切片整段替换,不讲究长度对等,3 个换4 个、4 个换3 个都可以。

a[3:6] = ['a', 'b', 'c', 'd']

切片替换的右侧不要求是列表,只要是可迭代类型即可,字符串会被拆成一个个字符替换进去。

删除元素

pop 方法默认删除最后一个元素,并把被删的元素返回出来,可以用变量接收。

c = a.pop()
print(c)
a.pop(0)

pop(索引) 删除指定索引的元素,不填参数就删最后一个。

clear 一键清空,不需要参数,清空后列表变成空列表,但列表本身还在。

第 30 页
a.clear()

del 是关键字,直接删除整个列表本身,删完再打印会报NameError。

del a

比喻:clear 是把房子里的人全部赶走、房子还在;del 是连房子一起强拆。

remove 方法按元素值删除,直接告诉它删掉哪个元素:

a.remove('abcd')

pop 是按索引删,remove 是按元素删。

排序:sort

有序不等于排序。有序是保持定义时的顺序;排序是按数学规律从小到大或从大到小排列。

sort 方法默认从小到大,在原列表上原地排序,排序前后内存地址相同。

a.sort()

不要写print(a.sort()):排序是一个动作,没有有意义的返回值,会打印None;应该先a.sort(),再直接看a。

比喻:老师给小朋友排队,家长的眼睛应该盯着排好队的小朋友,而不是盯着排队的老师。

列表里类型不同不能排序(数字和字符串无法比),数字之间整数和浮点数可以一起排。

全是字符串可以排序,按每个字符的Unicode 编码序号逐字符比较:先比第一个字符,相同再比后面的。

ord() 函数可以查单个字符的Unicode 编码序号;ord 只接受单个字符,传多个字符会报TypeError。

sort 有参数reverse,默认False 表示从小到大,设为True 则从大到小。

a.sort(reverse=True)
第 31 页

反转:reverse

reverse() 方法把列表原地掉个头,不涉及大小排列,只是从左往右变成从右往左。

a.reverse()

它和切片a[::-1] 效果相同但有区别:reverse 在原列表上改,地址不变;反向切片得到一个方向相反的新列表,地址变了。

计数:count

count 统计某个元素在列表中出现的次数,没有出现过就返回0。

print(a.count(1))
print(a.count(5))

字符串相关:join、replace、split

join 用指定符号把列表里的字符串拼成一个字符串:

l1 = ['hello', 'world', '哈哈哈']
print(''.join(l1))
print(' '.join(l1))
print('-'.join(l1))

replace 替换字符串里的字符,生成新字符串,不修改原字符串:

s = 'hello world 哈哈哈'
print(s.replace(' ', '-'))
print(s.replace(' ', '-', 1))

三个参数分别是:被替换的字符、替换成什么、从左往右替换几个;次数不填默认全部替换。

split 是join 的对立面,按指定符号把字符串切成片段,结果保存为列表,切割符号会消失:

第 32 页
b = s.split(' ')
b = s.split(' ', 2)

第二个参数maxsplit 是从左往右切几次,切两次得到三段。

自引用的坑

不要写出a.append(a) 这种把自己加进自己的代码:人不能把自己举起来,Python会显示三个省略号[…] 表示“对你很无语”。

更麻烦的是,之后再给a 追加元素时,里面的那个a 也会同步变化,因为放进去的是它自己的地址(引用)。

正确做法是新建一个列表再放进去:

a = [1, 2, 3]
b = [1, 2, 3]
a.append(b)
a.append(1)

技术好不是指能驾驭危险写法,而是一开始就把代码写得没有缺陷、没有歧义、可读性高、健壮。复盘:列表是有序容器,核心是“位置”和“方法”。索引从0 开始,切片记住[开始:结束:步长]、包头不含尾、步长方向决定省略的是头还是尾。增删改查各司其职:append/extend/insert 增,索引赋值与切片替换改,pop/remove/clear/del 删,索引、index、count 查。作业一:把列表里的’abcd’ 和’上山打老虎’ 两个元素的位置互换。

作业二:对一个含多种类型元素的列表进行分类,可以用索引做,也可以用类型判断做。

第四讲集合、元组与可变不可变类型

上节课作业讲评一:交换两个元素的位置

最简洁的写法就是之前学过的变量交换写法,按位置同时赋值:

a[3], a[5] = a[5], a[3]

等号赋值先看右边,右边是把a[5](‘上山打老虎’)和a[3](‘abcd’)打包,再按位置分别赋给a[

第 33 页

3]、a[5],一次完成交换。

有同学写得更“高级”:a[a.index(‘abcd’)], a[a.index(‘上山打老虎’)] = a[a.index(‘上山打老虎’)], a[a.index(‘abcd’)],语法上看似行,实际交换失败。

原因:第一句赋值完成后,列表里已经出现两个’上山打老虎’,再执行第二句时,index 从左往右找第一个,找到的是刚被替换到索引3 的那个,于是又改了回去,玩了个寂寞。

代码是按顺序执行的,把代码写复杂不会显得成熟,反而增加报错概率;好代码应该通俗易懂、严谨、不出错。

切片带步长也能赋值

切片不仅能整段替换,还能结合步长,只替换切出来的那些元素:

a = [1, 2, 3, 4, 5, 6]
a[1:5:2] = ['a', 'b']

上面把索引1 和3(2 和4)替换成a、b。

a[::3] = ['a', 'b']

从左往右每三个取第一个,切到1 和4 并替换。

a[::-1] = ['a', 'b', 'c', 'd', 'e', 'f']

从右往左每个都替换,整段全部换掉。

a[::-2] = ['a', 'b', 'c']

从右往左每两个取第一个,替换的是6、4、2。

a[1:5:1] 就是不带步长简写的整段替换,同样不要求长度对等。

上节课作业讲评二:元素分类

正确写法:先建两个空列表,一个收字符串,一个收数字;再遍历原列表,逐个判断类型后放入对应列表。

第 34 页
a = [1, 2, 3.3, 'abcd', 5, '上山打老虎']
list_str = []
list_number = []
for i in a:
    if type(i) == str:
        list_str.append(i)
    elif type(i) == int or type(i) == float:
        list_number.append(i)

凡是需要从容器里取元素,第一反应就应该是迭代遍历,遍历拿到元素后再判断类型,不要绕远路用range 配索引。

错误写法一:if i == int or float,这是拿元素本身去和类型比较,元素属于某类型,不等于元素“就是”类型,正如“朋友属猪”不等于“朋友是猪”。

错误写法二:if type(i) == int or float,or 右边的float 是类型本身,而int、float、str这些类型本身的布尔值永远为真。

一个or 里有了True 兜底,整个条件永远为真,所有元素都会进数字列表。

比喻:酒店100 间房住了99 间,问“酒店住人了吗”,答案当然是住了;类型本身存在,就为真。

错误写法三:type(i) == (int or float),int 和float 都为真,短路运算取第一个int,条件退化成只判断int,浮点数3.3 就丢了。

结论:老老实实地把两个条件写完整,就不会出错;老师讲这些不是教偷懒,是教你不要偷懒。

重要警告:不要一边遍历一边修改列表

边遍历边append 会造成无限循环:每迭代一次就加一个元素,元素永远取不完,就像“再来一瓶”,也像热情敬酒的碗一直被续满。

边遍历边pop()(不填参数,每次删最后一个),对[1, 2, 3, 4, 5, 6] 模拟:第一次取1 删6,第二次取2 删5,第三次取3 删4,循环结束,剩下[1, 2, 3]。

边遍历边remove(i) 更要注意,遍历记住的是索引位置而不是元素本身:

对[1, 2, 3, 4, 5, 6],第一次位置0 取1 删1,列表变成2 开头;第二次直接找现在的位置1(数字3)删掉;第三次找现在的位置2(数字5)删掉;三轮结束,剩下[2, 4, 6]。

第 35 页

班主任记性不好,赶走一个学生连座位一起搬走,再按“第二个位置”找人,三条漏网之鱼就是这么来的。

添加、删除、修改都不要在遍历同一个列表时进行。

集合的定义

集合用一对大括号定义,元素之间用逗号分隔,类型名是set。

s = {1, 2, 3, 2, 1, 3, 4}
print(s)
print(type(s))

集合的两大特点:

第一,无序,不保证输出顺序等于定义顺序;

第二,自动去重,重复元素只保留一个。

集合在被定义的那一瞬间就完成去重和无序排列,无论是字面量还是变量都一样。

底层基于哈希算法:数字的哈希值与数字本身正相关,所以纯数字集合看起来像从小到大排好了,但这只是数字的特殊现象,不是集合具备排序功能,绝不能拿集合排序。

换成字符串后,每次运行顺序都会变;数字多到一定程度后也不再严格有序。

一次遍历集合的过程中,它的顺序保持不变,否则无法保证每个元素恰好出现一次;但每次重新运行,遍历顺序都会不同。

集合的增删

集合无序,因此没有索引,也没有任何针对索引的操作。

pop 删除集合里的第一个元素。因为无序,谁在第一个位置不确定,所以一般理解为随机删除;纯数字集合中1 总在最前面,表现为每次都删掉1。

remove(元素) 直接指定删除哪个元素,不涉及索引:

s.remove(200)

clear 清空集合。

第 36 页

空集合必须写成set(),不能写成{}:一对空大括号{} 被留给了后面要学的字典(dict),打印类型可以验证。

add 把元素作为一个整体添加进集合,位置不确定,不存在“加到最后面”的说法。

s.add('df')

update 是集合的拓展方法,接受可迭代类型,把里面的元素拆开逐个添加:

s.update([1, 2, 3])
s.update('def')

update 对参数要求很宽松:可以传列表等可迭代数据,也可以传多个参数,字符串’def’ 也会被拆成d、e、f 三个元素。

extend 是列表专用的拓展方法,update 是集合专用的,这是二者本质区别。

集合的运算

差集用减号:你有的我都有,我有的你不一定有,去掉重叠部分。

a = {1, 2, 3, 4, 5, 6}
b = {4, 5, 6, 7, 8, 9}
print(a - b)

a - b 得到{1, 2, 3}。

交集用&(and 符号):你有我也有的部分,得到{4, 5, 6}。

print(a & b)

并集用竖杠|:把两个集合所有元素合在一起,得到{1, 2, 3, 4, 5, 6, 7, 8, 9}。

print(a | b)

这些运算规则和数学里的集合完全一致。

第 37 页

集合的比较

列表不适合比较,因为列表有序,相同元素顺序不同就不能算相等(田忌赛马,同样的马换个出场顺序结果就不同);集合无序,只比较元素本身。

你有的我都有、我有的你不一定有,则前者大于等于后者(包含关系),用>=、<= 判断子集与超集。

两个集合元素完全相同,互为子集,== 为True。

{1, 2, 3} 是{1, 2, 3, 4} 的真子集,真子集必须严格小于。

如果两个集合互不包含(你有的我没有、我有的你也没有),比较大小都为False,只有!= 为True。

元组

元组用小括号声明,元素之间用逗号分隔,类型名是tuple。

t = (1, 2, 3, 'abcd')

元组可以看作“被束缚的列表”:

它有序、可迭代、能索引取值、能切片,定义什么顺序输出就是什么顺序;但元组切片得到的是元组。

最大区别是不可变:元组一旦定义,里面的元素不能修改,t[0] = ‘哈哈’ 直接报错。

单元素元组必须在唯一的元素后面加一个逗号:

t = (1,)

只写(1) 会被当成改变运算优先级的括号,结果是整数1;加了逗号才是元组。

元组的另一种定义方式叫打包:一个变量接收多个逗号隔开的值,默认打包成元组。

a = 1, 2, 3

反过来,多个变量按位置接收一个元组叫解包(拆包)。

之前学的a, b = b, a 原理就在这里:先在右边打包成两个元素的元组,再按位置解包给a 和b。

第 38 页

数据结构的简单相加

列表与列表相加,是首尾连接成新列表:

a = [1, 2, 3]
b = [4, 5, 6]
a = a + b

元组与元组相加同理,得到连接后的新元组。

番外篇一:可变类型与不可变类型

先看现象:a = a + b 对列表做相加时,a 的内存地址(id)一定会变,因为每一次赋值都会重新分配内存。

而改用a += b 扩展列表时,是在原列表上、保持地址不变的前提下往里加元素,地址不变。

同样的+= 用在整数上,地址仍然会变;用在元组上,地址也会变,元组学列表原地扩展属于东施效颦。

差别由数据类型本身决定。

可变类型:能保持内存地址不变、对内部元素进行修改的类型,Python 自带的有列表、字典、集合。

不可变类型:只要修改就必须更换内存地址的类型,有整数、浮点数、字符串、元组。

这是语言作者在设计类型时就定好的,就像同学既可以按性别分,也可以按南北户口分,是对同一批数据按不同特性分类。

不能用a = a + b 导致地址变化来论证列表不可变:那是用错误的方式(重新赋值覆盖)去论证,等于拿铅球让人家投篮;正确方式是+=,列表能做到原地改,它就是可变的。

不可变类型无论用什么写法,都无法保持地址不变。

回头解释前面的坑

a = 1,b = 1,a == b 且a is b 成立;而a = [1, 2, 3],b = [1, 2, 3],a == b 但a is b 不成立。

对不可变类型,可以假设相等的值共用一个地址:能开一个内存空间绝不开两个,就像一块钱能买的水没必要花两块。

第 39 页

x 和y 都指向同一个字符串,x 做+= 修改时,因为字符串不可变,只能断开原地址、指向新地址,y 不受影响,像宿舍单身汉搬去和女朋友住、自己搬新地址,不伤害室友。

所以不可变类型相等可以是同一个,一个改不影响另一个。

可变类型则不能共用:如果x、y 指向同一个列表,x[0] = 6 原地修改后,y 会跟着变,必然造成混乱,所以列表即使相等,也要给两个不同地址。

x = [1, 2, 3],y = x 让二者相同,是你主动让y 保存x 的地址,属于写法使然,不能用来证明“可变类型相等也是同一个”。

补充(内存优化):上述“相等就是同一个”只对较小的整数和较短的字符串成立,是Python 自动做的内存优化;长字符串(如’a’ * 1000)和大数字(如2 ** 99)即使相等,地址也不同。日常接触的多是短字符串、小数字,所以结论照常有用。

in 与not in

in 在for 里表示迭代;单独使用时,判断左边的值是否存在于右边。

print('python' in 'python hello world')

字符串里判断包含关系,顺序必须和原字符串一致:‘大数据’ 在原串里为True,‘大据’ 顺序颠倒就为False。

not in 是反面表述:左边不在右边时为True。

print(1 in [1, 2, 3])
print([1] in [1, 2, 3])

1 in 列表为True,因为列表里有1 这个元素;[1] in 列表为False,因为列表按元素判断,里面没有子列表[1],不是按切片匹配。

循环复习与加权求和

for i in [1, 2, 3] 直接遍历字面量,不如先把列表赋给变量a 再遍历:变量可复用,修改后也能保存。

range(0, 100) 包头不含尾共100 次循环,循环体里每次打印两句,总共打印200句。

加权求和:每个分数乘以自己的权重再相加,权重就是各项占的比例。

第 40 页

分数[1, 2, 3, 4, 5],权重[0.1, 0.2, 0.2, 0.3, 0.2],计算10.1 + 20.2 + 30.2 + 40.3 +5*0.2 = 3.3。

values = [1, 2, 3, 4, 5]
weights = [0.1, 0.2, 0.2, 0.3, 0.2]
result = 0
for i in range(len(values)):
    result += values[i] * weights[i]
print(result)

每次循环i 相同,它同时是两个列表的索引,于是能拿到两个列表相同位置上的一对元素相乘再累加。

字符串查找与大小写

字符串和列表、元组一样:有序、有索引、能切片、包头不含尾。

find 方法在字符串中查找子字符串,返回它第一次出现的位置,按子字符串第一个字符的位置计算。

s = 'hello world'
print(s.find('world'))

h、e、l、l、o、空格依次占0 到5,w 在索引6,所以结果是6。

print(s.find('o'))

不指定范围时,从左往右找第一个,hello 里的o 在索引4。

print(s.find('o', 5, 10))

还可以像index 一样指定查找的起始和结束位置,包头不含尾;上面在5 到9 之间找到word 里的o,索引为7。

print(s.find('aaa'))
第 41 页

找不到时不报错,返回-1。-1 表示从右往左数第一个位置,意思是“我已经帮你找到最后一个位置,仍然没找到”;不能返回从左往右最后一个索引,否则元素恰好在末尾时会产生歧义。

字符串严格区分大小写,小写查不到大写。

upper 方法把所有英文字母转成大写,lower 方法把所有英文字母转成小写,都会生成新字符串。

string_upper = s.upper()
string_lower = s.lower()

正确的查询姿势:把匹配方和被匹配方都转成相同的大写或小写,再去比较,避免因为大小写错过结果。复盘:集合记“无序、去重、无索引”,方法对应add、update、remove、pop、clear,运算对应-(差)、&(交)、|(并),比较只看子集包含关系;元组记“不可变的列表”,单元素必须带逗号;可变三兄弟list、dict、set,不可变四兄弟int、float、str、tuple,判据是改内部元素时id 变不变;in 按元素判断、字符串还要按顺序。

作业一:把一个列表中所有的元素去重并保存下来,提示用for 循环配合一个空列表和in 判断。作业二:把列表[1, 2, 3, 1, 2] 以及子列表[3, 4] 里的每个整数按顺序添加到一个空列表中,平铺成[1, 2, 3, 1, 2, 3, 4],可以用索引逐个添加,也可以用循环加判断。

第五讲while 循环、字符串格式化、猜拳游戏与内置函数

复习补充:集合里不能放可变类型

集合里只能放不可变类型数据。如果往集合里添加一个列表,会报TypeError:unhashable type: ‘list’(列表不可被哈希)。

原因:集合的无序与去重建立在哈希算法之上,可变类型数据没有固定的哈希值。

其他容器(列表、元组)没有这个限制,可以放任意元素;只需把集合单独记住。

复习补充:元组里的子列表为什么能改

元组里如果放了一个子列表,修改子列表里面的元素是可以成功的。

记住一句话:一切皆引用,不管变量还是字面量。元组的不可变,指的是里面每个元素的引用(内存地址)不可变。

子列表本身是可变类型,它能保持自己的地址不变、只改内部元素,所以元组层面引用没变,子列表内部却变了,两边都解释得通。

第 42 页

上节课作业讲评一:列表去重

最省事的做法是直接用set 转换去重。

用语法逻辑去写的做法:准备一个空列表b,遍历原列表,元素不在b 里才添加。

a = [4, 1, 2, 1, 3, 4, 5, 5, 6, 2, 3, 6]
b = []
for i in a:
    if i not in b:
        b.append(i)
print(b)

第二个1 出现时,b 里已经有1 了,就不再添加,这样实现去重。

学过in 以后,类型判断也可以写成if type(i) in [int, float],不用再写两个== 条件。

上节课作业讲评二:列表扁平化

目标:把[1, 2, [3, 4]] 拍扁成[1, 2, 3, 4]。

写法一,索引法,大巧不工,一层层append:

a = [1, 2, [3, 4]]
b = []
b.append(a[0])
b.append(a[1])
b.append(a[2][0])
b.append(a[2][1])

写法二,循环加判断,整数直接加,子列表用extend 拆开加:

for i in a:
    if type(i) == int:
        b.append(i)
    elif type(i) == list:
        b.extend(i)
第 43 页

写法三,两层循环:外层判断到子列表后,内层再遍历子列表逐个append。

for i in a:
    if type(i) == int:
        b.append(i)
    elif type(i) == list:
        for j in i:
            b.append(j)

写法四(不推荐),利用报错:整数不能被迭代,try 里尝试for j in i,整数会触发TypeError,except 里直接append;子列表能进入内层循环,就把3、4 加进去。

for i in a:
    try:
        for j in i:
            b.append(j)
    except TypeError:
        b.append(i)

老师不推荐为了用异常而制造问题,但思路可以打开:报错有时会给你新的解题方向。

写代码像爬山,不要想着一步飞上去,写完这一步再看下一步,一步步总能写出来。

while 循环

while 的英文意思是“当……的时候”。for 循环靠遍历数据的元素个数决定次数,适合已知次数;while 循环靠条件驱动,适合不知道具体次数、只知道结束条件的场景。

找对象比喻:有的人5 个就够了,有的人5000 个也找不到,你没法预设次数,只能约定“没找到就继续,找到就结束”。

for 即使配break,也受限于range 给出的范围;while 不带range,只看条件。

基本写法:自己先设一个计数器变量,while 后面跟条件,条件成立就进入循环。

第 44 页
n = 0
while n < 6:
    print(n)
    n += 1

上面打印0 到5。

while 不会自动把n 变成下一个值,忘记n += 1 就会无限打印0。

计数器的加一操作一般放在循环体末尾:保证当前数字该执行的代码执行完,再进入下一次循环。就像读完一年级再升二年级,不能左脚刚进一年级大门就被抬去二年级。

如果把n += 1 写在print 前面,先加后打,就会打印1 到6。

n 等于0 时,0 的布尔值虽然是False,但它照样能进循环:判断条件是n < 6,不是n 的布尔值。

西餐厅比喻:能不能进门只看“男穿西装、女穿礼服”这个条件,跟你是好人坏人无关。

用while 写1 到100 累加,比for 啰嗦,起始值和计数器都要自己管:

sum01 = 0
i = 1
while i <= 100:
    sum01 += i
    i += 1
print(sum01)

没有更好的循环,只有更适合的循环:能方便指定次数用for,按条件重复用while。

无限循环

当条件永远为真时就是无限循环,写1 < 2 这种恒成立条件可以,但不如直接写True 直白。

while True:
    print(n)
    n += 1

无限循环是while 灵活的地方,也可能让内存塞满、风扇狂转,写的时候要自己保证有出口。

第 45 页

break

while True 内部用break 结束循环。下面的例子n 到6 时break,否则打印并加一,循环结束后照常打印循环外的“哈哈哈哈”。

n = 0
while True:
    if n == 6:
        break
    else:
        print(n)
        n += 1
print('哈哈哈哈')

break 只终结当前循环,不终结整个程序;但写在break 后面、与它同缩进的代码永远不会执行,相当于“世界末日就把家产给你”——世界末日了,给你又有什么意义。

break 在for 循环里同样可以使用。

continue

continue 英文是继续,这里理解成“跳过”更准确:立刻回到循环开头,开启下一次循环。

需求是0 到9 除了4 都打印:用break 会在4 处整体结束,5 以后也没了;用continue 则只跳过4,后面的5 到9 照常打印。

在while 里用continue 有陷阱:当n == 4 时直接跳回开头,而while 不会自动加一,于是n 永远是4,陷入死循环。

把n += 1 写在continue 后面也没用,因为continue 之后的代码根本不执行,等于被“截胡”了。

正确做法是在continue 之前先完成加一:

第 46 页
n = 0
while n < 11:
    if n == 4:
        n += 1
        continue
    else:
        print(n)
    n += 1

只要运行到continue,就马上跳到循环开头,同一次循环后面的缩进代码全部绕过。

循环搭配else

else 可以和for、while 搭配,注意它是和循环关键字对齐,不是和if 对齐。

规则:循环正常运行结束(中途没有被break)才执行else;一旦被break 打断,else 就不执行。

for number in numbers:
    if number == 4:
        break
    print(number)
else:
    print('循环正常结束')

哪怕是最后一次循环、最后一句代码break,else 也照样不执行;不管第几次,只要运行了break 就不算正常结束。

while 版本同理:条件不成立而结束,else 执行;被break 打断,else 不执行。

缩进的层级

缩进表示从属关系:缩进后面的代码属于缩进前面的代码。

循环里的if 判断属于循环,每次循环都执行一次(即使只进去一次,其余九次也都判断了);if 里面的print 属于判断,只在条件满足时执行。

看代码要先看缩进、看辈分,就像过年喊人要先问清楚辈分再喊。

出问题时多用print 打印中间值,自己学会debug 调试。

第 47 页

字符串格式化的需求

迎新电子牌的例子:前面姓名要活动变化,后面“欢迎来到某某学院”固定不变,这就是字符串格式化——一部分固定,一部分留占位。

原始写法一:print 用逗号连接多个参数,逗号之间会自动出现空格。

原始写法二:用加号拼接字符串,拼好后是一个参数,没有多余空格。

第一种写法传给print 三个参数,第二种写法拼接后只传一个参数。

格式化1.0:百分号占位

在字符串里用百分号加符号占位,字符串后面再用一个百分号连接填充内容。

a = '123'
print('插入了%s 个变量' % a)

% s 对应字符串,% d 对应整数(digital,数字),% f 对应浮点数,类型不同占位符不同。

a = 1
b = 2
print('插入了%d 个和%d 个变量' % (a, b))

多个值要用元组包起来,严格按顺序对应。

% f 插入浮点数默认保留6 位小数,想保留两位写%.2f。

c = 1.5
print('%f' % c)
print('%.2f' % c)

类型、格式、顺序三重要求,比较麻烦,这是老写法,了解即可。

格式化2.0:format

统一用一对空大括号占位,不再区分类型,字符串调用.format() 方法,按顺序把参数填进大括号。

第 48 页
print('插入了{}个和{}个变量'.format(a, b))

还可以在大括号里填参数的索引,自定义填充顺序:

print('{1}{0}'.format(a, b))

上面把第二个参数填到第一个位置、第一个参数填到第二个位置。

格式化3.0:f-string

Python 3.6 以后,在字符串前面加小写(或大写)f,代表format,大括号里直接写变量名即可。

print(f'插入了{a}个变量')

三种写法都要能看懂,自己写代码优先用最简洁的f-string。

回忆:字符串前面加r 表示原始字符串,加f 表示要格式化。

案例:石头剪刀布游戏

游戏规则:电脑随机出,玩家输入出拳,理论上可以无限玩下去,所以用while True。

思路:用暗号一一对应,1 代表石头,2 代表剪刀,3 代表布,准备两个位置对齐的列表。

import random
number = [1, 2, 3]
gesture = ['石头', '剪刀', '布']
while True:
    player = input('请输入:1 石头,2 剪刀,3 布:')
    computer = random.randint(1, 3)
    if player not in ['1', '2', '3']:
        print('请输入正确的数字')
        continue
    else:
        player = int(player)

先让玩家输入,再生成电脑的随机数,保证公平;输入不合法时提示并continue 重新输入。

第 49 页

数字相同就是平局,打印后continue 再来一局。

显示手势时,先用number.index(数字) 拿到索引,再用这个索引去gesture 列表取对应手势,配合f-string 输出。

if player == computer:
    print(f'你出了{gesture[number.index(player)]},电脑出了
{gesture[number.index(computer)]},平局')
    continue

玩家赢的三种情况:石头砸剪刀、剪刀剪布、布包石头。

elif (player == 1 and computer == 2) or \
    (player == 2 and computer == 3) or \
    (player == 3 and computer == 1):
    print(f'你出了{gesture[number.index(player)]},电脑出了
{gesture[number.index(computer)]},你赢了')
    break

除了平局和赢,剩下的只有电脑赢(大毛、二毛、三儿子只能是小明),else 里判负并break。

else:
    print(f'你出了{gesture[number.index(player)]},电脑出了
{gesture[number.index(computer)]},你输了')
    break

这里的continue 其实可以不写:if 和else 二选一,走完if 这次循环就结束,自然回到循环开头;break 同理,走完分支自然结束本轮。但写上更清晰,按自己的理解来。

数据类型强制转换

学过列表、元组、集合后,转换范围扩大:list()、tuple()、set()、str() 都可以生成新数据。

字符串转列表,会把每个字符拆开装进去,本质等价于遍历字符串往空列表里append。

第 50 页
a = 'python'
print(list(a))

元组转列表就是小括号变中括号;集合转列表会先去重且顺序每次不同,一旦转成列表顺序就固定下来。

print(list((1, 2, 3)))
print(list({1, 2, 3}))

字符串、元组、列表转集合,都得到去重且无序的新集合;字符串、列表转元组,得到拆开后的新元组。

所有强制转换都是生成一个新数据,不修改原值,和int()、float()、str() 一样。

重点的坑:列表转字符串后转不回列表。str() 非常简单粗暴,就是在左右两边各加一个引号;此时空格、中括号、引号、逗号都成了字符串里的字符,再list() 会把这些符号全部拆成元素。

想把字符列表拼回字符串,要用join,不能用str()。

内置函数min、max

min 求可迭代数据里的最小值,列表、元组、集合都可以传;也可以像集合的update 那样把多个数字用逗号直接传进去。

print(min([1, 2, 3, 0]))
print(min(3, 1, 2))

max 求最大值,用法完全相同。

全是字符串时也能比,按字符的Unicode 编码序号取最小或最大。

内置函数sum

sum 接收一个可迭代数据,对里面每个元素求和,元组不去重,集合在定义瞬间就去重,这是一个坑。

第 51 页
print(sum([1, 9, 6]))
print(sum((1, 9, 3, 9, 6)))
print(sum({1, 9, 3, 9, 6}))

列表结果是16;元组结果是28;集合里两个9 只剩一个,结果是19。

第二个参数是累加的起始值:先求元素的和,再加上这个起始值。

print(sum((1, 9, 3, 9, 6), 100))
print(sum({1, 9, 3, 9, 6}, 100))

元组结果是128;集合去重后先得19,再加100,结果是119。第二个参数可以是整数或浮点数,这是重点。

内置函数sorted

列表有sort 方法,集合没有sort(无序,排完又乱,等于洗完澡去泥里打滚),元组也没有sort(不可变,不允许原地改)。

sorted 是不依赖任何具体类型的内置函数,所有可迭代类型都能用,返回一个排好序的新列表,不修改原数据。

s = [3, 1, 2]
a = sorted(s)
print(a)
print(s)

a 和s 不是同一个列表,原列表s 原样不变。

sorted(s, reverse=True) 可以得到从大到小的新列表。

元组、集合传给sorted,结果也都是列表;想要元组结果,再用tuple() 转一次即可。

字符串也能传给sorted,会拆成字符排序;‘1234’ 是字符串而不是数字,按Unicode编码排,不要一厢情愿按数字排。

对比记忆:s.sort() 是列表的方法,原地排序、地址不变;sorted(s) 是内置函数,生成新列表、原数据不动。复盘:while 记“条件+ 计数器”,计数器自加放循环末尾,continue 前要记得手动加一;break 管结束、continue 管跳过,else 只在循环正常结束时执行;格式化三代%、format、f-s

第 52 页

tring,优先f-string;猜拳的核心是数字列表与手势列表按索引一一对应;min、max、sum、sorted 都是直接拿来用的内置函数,转换全部生成新数据,str 后不能靠list 复原。

作业:用1、2、3、4 四个数字组成互不相同且没有重复数字的四位数(如1234、4321),每个数字在每一位上只能用一次。千位4 种选择,百位剩3 种,十位剩2 种,个位剩1 种,共4×3×2×1 = 24 个。要求:用四层循环嵌套,每层循环生成一位数字,再用判断保证各位数字互不相同,把符合要求的四位数append 到列表里,最后打印列表和列表长度,标准答案长度为24。

第六讲迭代器与高阶函数

上节课内容快速回顾

for 适合已知次数、需要逐个取元素的循环;while 适合以结束条件驱动的循环,计数器和加一都要自己管。

while True 写无限循环,靠break 找出口;continue 是跳过本次回到循环开头,在while 里用它之前必须先手动加一。

循环的else 只在循环正常结束时执行,被break 打断就不执行。

格式化三代:% 占位、format、f-string;猜拳用数字列表与手势列表按索引一一对应;min、max、sum、sorted 都是通用内置函数。

上节课作业讲评:四位数(枚举法)

题目:用1、2、3、4 组成互不相同且没有重复数字的四位数,全部保存到列表,并打印列表和个数。

先定量:不筛选重复时,每位都有4 种选择,共4 的4 次方= 256 种组合。

要求无重复时,千位4 种、百位3 种、十位2 种、个位1 种,共4×3×2×1 = 24 种。

方法叫枚举法:像对密码锁,先用数字1 搭配完后面的1 到4,再用2 搭配1 到4,一个不漏地全部列出来。

不要用随机:掷骰子会出现有的组合重复很多次、有的组合一次没出现,时间不可控;能用规律和系统方法解决的,就靠实力不靠运气。

代码结构是四层循环嵌套,循环变量i、j、k、h 依次代表千位、百位、十位、个位。

关键规则:内层循环属于外层循环。外层i 等于1 时,内层所有循环先全部走完(1111 到1114,再1121……),再回到外层换成2。

第 53 页

不要用字符串拼接再转整数,计算机最擅长计算,四位数字直接按位乘权:

number = i * 1000 + j * 100 + k * 10 + h

筛选方式一,逐层淘汰(月考):拿到j 后先判断j 不等于i,才进入第三层;拿到k 后判断k 既不等于i 也不等于j,才进入第四层;拿到h 后再和前三个都比较,每层筛掉一批。

筛选方式二,一次性淘汰(期末考):四层循环照走,在最内层一个if 里把六个不等条件全部写全(i 不等于j、i 不等于k、i 不等于h、j 不等于k、j 不等于h、k 不等于h)。

偷懒写法只写i != j and j != k and k != h 会得到108 种,是错的:它只保证相邻位不同,1212 这种也能通过。

筛选方式三,借集合去重:四个数字放进集合,只有四个元素互不相等时集合长度才为4。

if len({i, j, k, h}) == 4:

最后把符合条件的number append 进列表,打印列表和len(),标准答案24 个。

用while 也能做,但要手动控制索引、每次归零,结构非常难看,练习也要用正确方式练。

Python 的规范是扁平优于嵌套,四层循环实际工作中很少写,这里只是为了理解循环嵌套。

方向大于努力,方式大于细节:先把思路做对,再优化细节。

为什么需要迭代器

字符串、列表、元组、集合都是可迭代数据类型。for 遍历列表时,列表里的所有元素是在循环开始前就一次性加载到内存里的,然后再一个个使用。

元素少时无所谓,当成百上千万个元素时,这会浪费大量内存。

吃饭比喻:你是一道一道吃菜,厨师就应该在你吃完一道后再炒下一道,而不是一次性把所有菜做好摆满桌子;桌子就是内存,摆满了就是浪费。

迭代器同样可以迭代,但它不会一次性生成所有元素,而是迭代到哪个元素才生成哪个元素,内存里任何时刻都只有当前这一个元素。

迭代器的底层原理不用管,Python 自动实现,会用即可。

第 54 页

iter 函数生成迭代器

iter 函数接收一个可迭代数据类型,以它为基础生成一个迭代器。

a = [1, 2, 3, 4, 5, 6]
b = iter(a)
print(b)
print(type(b))

直接打印迭代器得到的是一个地址,不是展开的元素,因为它手里本来就没有所有元素;类型是list_iterator 之类,叫什么不重要,知道它是迭代器就行。

迭代器逐个产出的元素,和原列表逐个取出的元素完全没有区别,同样的水没必要花两份钱,所以优先用省内存的迭代器。

校长办公室比喻:让教导主任把888 个早恋学生一次性全赶到办公室再一个个骂,办公室(内存)挤不下;正确做法是教导主任造一本花名册,校长每骂一个就用广播喊一个进来,办公室里始终只有当前这一个学生。

学生是元素,办公室是内存,花名册就是迭代器。

用内存占用验证迭代器

生成一个含一百万个元素的列表(列表推导式,后面会学):

a = [i for i in range(1000000)]

用标准库sys 的getsizeof 函数查看变量占用的内存大小,单位是字节:

import sys
print(sys.getsizeof(a))
b = iter(a)
print(sys.getsizeof(b))

同样一百万个元素,迭代器占用的内存远远小于列表,优势肉眼可见。

第 55 页

next 函数逐个取值

for 每次都是一次性拿全部,next 函数则可以一个一个主动取:传入迭代器,取当前元素,每调用一次取一个。

print(next(b))
print(next(b))
print(next(b))

列表有六个元素,迭代器就只能取六次,第七次取会报StopIteration 错误。

next 只能传迭代器,不能直接传列表、元组、集合、字符串,否则报TypeError。

for 循环的底层原理

for 循环遍历列表时,并不是直接遍历列表本身,而是先用iter 把列表转成迭代器;最外层是一个while 循环,里面包着异常捕获,在try 里用next 取元素,取不到元素时except 捕获StopIteration 并break。

a = [1, 2, 3, 4, 5, 6]
b = iter(a)
while True:
    try:
        i = next(b)
        print(i)
    except StopIteration:
        break

这就是用iter、while、异常捕获、next 手写的for 循环,while 比for 更贴近底层。

补充:StopIteration 属于BaseException 层面,比Exception 更底层,Python 甚至不把它当成普通错误;所以except Exception 能感知到出了错并切换分支,但捕获不到它的具体报错信息。

第 56 页

next 的默认值参数

next 可以传两个参数:第一个是迭代器,第二个是元素取完之后返回的默认值,这样取完不报错。

print(next(b, '没有元素了'))

前六个元素正常返回元素本身,第七次开始返回默认值;它相当于内部自带了try取元素、except 返回默认值的结构。

两个易错点

易错点一:字符串’python’ 生成迭代器,自始至终用同一个迭代器循环100 次,前6 次依次取出p、y、t、h、o、n,剩下94 次取不到元素,打印94 次“哈哈哈哈”。

易错点二:把iter(s) 写在循环内部,每次循环都生成一个新迭代器,于是每次都取到新迭代器的第一个元素,打印100 个大写P。

记住:iter 每次调用都得到一个全新的迭代器。口香糖比喻:一盒有六片,吃完六片就没了;但每次循环老师都给你换一盒新的,于是你每次都只吃到新一盒的第一片。

in 本质也是迭代:1 in 列表为True,1 in 这个列表生成的迭代器同样为True,元素没有变,只是一个一个数的过程。

reversed 反向迭代器

iter 生成正向迭代器,reversed 生成反向迭代器:元素和原数据一模一样,只是方向相反。

print(reversed('python'))

打印出来仍是一个地址,因为它是迭代器。

用反向迭代器实现字符串反转:拿到的每个字符都是从后往前的,按正常顺序拼接到空字符串上,结果就反了。

a = ''
for i in reversed('python'):
    a += i
print(a)
第 57 页

结果是nohtyp。

把反向迭代器放进list() 就得到反向排列的列表;对这个列表再调一次.reverse() 方法,又转回正向,负负得正。

放进list 是列表,放进tuple 是元组,这就是迭代器的可塑性。

迭代器两大优势与一条铁律

优势一:节约内存资源。

优势二:可塑性极高,得到迭代器后想转成什么容器都行。

铁律:迭代器不可逆,只能从头到尾迭代一次。

列表可以翻来覆去迭代一百次,迭代器迭代完就空了。

for 循环把迭代器遍历完后,再list(迭代器) 得到的是空列表:因为list 转换本质上内置了一次for 循环,逐个迭代再放进新列表,元素已经被前面的for 取光了。

如果在for 里当i 等于4 时break,迭代器只被取到4,剩下的5 和6 还在,再list转换就得到[5, 6]。

把break 换成continue,结果一个元素都没有:4 必须先被取出来才能进入判断,continue 只是跳过它,不代表没取它。

可迭代对象

可迭代对象= 可迭代数据类型(列表、元组、集合、字典、字符串)+ 迭代器。以后看到“可迭代对象”这个词,要把迭代器也算进去。

all 函数

all 用于判断可迭代对象中的元素:所有元素都为真才返回True,有一个为假就返回False。

口诀:全真才为真,有假必为假,all 是奔着找假去的。

注意all(‘10086’) 为True:里面的0 是字符串’0’,字符串只有空字符串为假,字符串0 为真。

重点:all(空列表) 返回True。空列表里连元素都没有,找不到假元素,所以为真。

空碗比喻:老师问“碗里的饭好不好吃”,你不能回答“碗不好吃”;all 问的是载体里有没有假元素,不是问载体本身的布尔值。

第 58 页

对比:bool([0, False, None]) 是True,因为列表非空,列表只有空列表一种为假;all([0, False, None]) 是False,因为这次判断的是里面的元素,全部为假。

any 函数

any 判断可迭代对象中是否存在为真的元素:有真必为真,没有真才为假,它是奔着找真去的。

所以any(空列表) 返回False:连元素都没有,找不到真元素。

enumerate 枚举函数

需求:遍历季节列表时,给每个元素配上它的编号。

seasons = ['春天', '夏天', '秋天', '冬天']

手动写法:遍历时把索引和元素打包成元组。

for i in seasons:
    print((seasons.index(i), i))

用enumerate 后,它自动把每个元素和编号打包成一个二元组,整体返回一个迭代器,遍历得到的就是(0, ‘春天’)、(1, ‘夏天’) 这样的元组。

a = enumerate(seasons)
for k in a:
    print(k)

回忆打包与拆包:j = 1, 2 是打包成元组;i, g = (0, ‘春天’) 是把元组按位置拆给两个变量。

所以可以一边遍历一边拆包,两个变量分别拿到编号和元素:

for i, g in enumerate(seasons):
    print(i, g)

上面第一次循环拆开(0, ‘春天’),第二次拆开(1, ‘夏天’),依此类推。

start 参数指定编号从几开始,默认0:

第 59 页
for i, g in enumerate(seasons, start=1):
    print(i, g)

严格来说,start 为0 时数字恰好和索引吻合,所以平时叫它索引;改成1 或6 后,它只是一段连贯的整数编号,不再是真正的索引。

配合f-string 可以直接做出“第1 个是周周,第2 个是贝贝,第3 个是语嫣”这样的句子。

zip 聚合函数

zip 把多个可迭代对象中对应位置的元素打包成元组,返回一个迭代器。

x = [1, 2, 3]
y = [4, 5, 6]
a = zip(x, y)
print(list(a))

结果是[(1, 4), (2, 5), (3, 6)]。

不止两个参数,三个、四个都可以:

print(list(zip([1, 2, 3], ['a', 'b', 'c'], ['你', '我', '他'])))

得到(1, ‘a’, ‘你’)、(2, ‘b’, ‘我’)、(3, ‘c’, ‘他’),第i 个元组包含每个可迭代对象的第i 个元素。

长度不一致时遵循短板原理:木桶装多少水取决于最短的板,zip 只打包到最短的那个对象用完为止,多出来的元素不打包,也不报错。

x 只有一个元素[1] 时,就只生成一个元组(1, 4, ‘a’)。

案例:名字、年龄、城市三个列表,用zip 打包成三元组,遍历拆包后填入f-string:

names = ['周周', '贝贝', '语嫣']
ages = [33, 22, 18]
cities = ['长沙', '杭州', '深圳']
for name, age, city in zip(names, ages, cities):
    print(f'{name}{age}岁,住在{city}')
第 60 页

猜拳游戏里数字和手势的对应关系,也可以用zip 打包成(1, ‘石头’)、(2, ‘剪刀’)、(3,‘布’)。

map 映射函数(本讲最难)

先分清函数名和函数调用:pow(3, 2) 是3 的平方,结果是9,这是加了括号、传入参数、真正运行了函数;

只打印pow 得到的是函数所在的地址,函数名只是“找到这个函数”的方式,并没有运行它。

手榴弹比喻:队友喊你扔手榴弹,你要把没拔引线的手榴弹完整抛给他,他什么时候用自己拔;函数名就是没拔引线的手榴弹,加括号传参才是拔了引线、引爆并得到结果。

map 根据你提供的函数(运算规则),对指定可迭代对象的每个元素批量运算,结果以迭代器形式返回。

需求:x 里全是底数,y 里全是次方数,求对应位置的1 的5 次方、3 的2 次方、10的2 次方。

不用map 的写法:先zip 配对,再拆包,再调pow。

x = [1, 3, 10]
y = [5, 2, 2]
for i, j in zip(x, y):
    print(pow(i, j))

用map 的写法:第一个参数是运算规则(只写函数名,不加括号),后面按规则所需参数的顺序,把材料分别打包传入。

a = map(pow, x, y)
print(list(a))

它相当于依次执行pow(1, 5)、pow(3, 2)、pow(10, 2),得到1、9、100。

流水线比喻:你有一条流水线,做什么取决于工艺(第一个参数的函数);把所有原材料按参数顺序打包放到流水线上,它就按同一个规则批量生产,结果装进迭代器返回。

后面传几个可迭代对象,取决于第一个函数需要几个参数:pow 需要两个参数,所以传两个;len 只需要一个参数,就只传一个。

map(len, 字符串列表) 就是批量计算列表里每个字符串的长度。

第 61 页
a = map(len, ['hello', 'world', '哈哈哈'])

map 构建的是工程化、流程化、模式化的批量生产,不需要自己写循环一个个算。复盘:iter 造迭代器、next 逐个取值、reversed 反向、迭代器省内存且只能用一次;for的本质是iter 加while 加next 加捕获StopIteration;all 找假(空为真)、any 找真(空为假);enumerate 给元素配编号、zip 按位置配对、map 给每个元素套同一个函数,这三个函数返回的都是迭代器。

作业一:用while 循环加next 函数遍历列表[5, 6, 7, ‘吃鸡’, 8, 9],把整数取出后按“乘以2 加1”打印,遇到字符串跳过,并且循环结束时不能报错(提示用next 的默认值参数,拿到默认值时break)。作业二:用map 和sum 计算[1, 2, 3, 4, 5, 6] 的和加100、[2, 3, 4, 5, 6, 7] 的和加200、[3, 4, 5, 6, 7, 8] 的和加300,重点是构建map 的两个参数,不需要匿名函数;提示sum 接收两个参数(可迭代数据和起始值),所以把三个列表打包给第一个参数、三个加数打包给第二个参数。

第七讲字典、深拷贝与浅拷贝、推导式

上节课内容快速回顾

迭代器用iter 生成、next 逐个取值,省内存、可塑性高、只能迭代一次;for 的本质是iter 加while 加next 加捕获StopIteration。

all 找假(空序列为真),any 找真(空序列为假);enumerate 配编号、zip 按位置配对(短板原理)、map 批量套用同一个函数,返回的都是迭代器。

上节课作业讲评一:while 加next 处理列表

要求:用while 加next 遍历[5, 6, 7, ‘吃鸡’, 8, 9],整数乘以2 加1 打印,字符串跳过,取完不能报错。

正确写法:先用iter 造迭代器,while True 里用一个变量接收next 的结果,给next传默认值,取到默认值就break。

第 62 页
b = [5, 6, 7, '吃鸡', 8, 9]
c = iter(b)
while True:
    a = next(c, '数据已全部取出')
    if type(a) == int:
        print(a * 2 + 1)
    elif a == '数据已全部取出':
        print('数据全部取完')
        break
    else:
        continue

最后的else 可以省略,因为不满足前两种情况时,循环自然回到开头。

错误写法一:默认值也设成字符串,然后用“元素是字符串就continue”判断,结果默认值也被当成字符串,永远continue,形成死循环。

错误写法二(Python 版刻舟求剑):在判断里写next(c),打印时又写一次next(c)。第一次循环判断用的是5,打印时next 已经取到6,一轮用掉两个元素;第二轮判断取到7,打印取到’吃鸡’,‘吃鸡’ 不能乘2 加1,报字符串不能与整数相加的错误。

船在移动,刻在船上的记号找不回剑;next 每调用一次就取下一个元素,所以必须用变量接收,保证同一次循环里判断和处理的是同一个元素。

题目要求用while 就不要用for,用for 就不需要next 了,不能改题目。

上节课作业讲评二:map 加sum 与维度打包

需求:分别计算[1,2,3,4,5,6] 的和加100、[2,3,4,5,6,7] 的和加200、[3,4,5,6,7,8] 的和加300。

先理解维度:一个点是零维,一条线是一维,一个面是二维,一个立体是三维。

map 所谓打包就是升维:一个个数字(点)放进列表变成线(一维),一维列表再打包成二维列表(面)。

pow 需要两个参数,所以两个参数各自打包成一个一维列表。

sum 的第一个参数本身就要求是列表(一维),把三个列表放进一个列表,就从一维升到二维;100、200、300 是散装数字,打包成一维列表。

第 63 页
lists = [[1, 2, 3, 4, 5, 6], [2, 3, 4, 5, 6, 7], [3, 4, 5, 6, 7, 8]]
adds = [100, 200, 300]
a = map(sum, lists, adds)
print(list(a))

map 内部依次执行sum([1,2,3,4,5,6], 100)、sum([2,3,4,5,6,7], 200)、sum([3,4,5,6,7,8], 300)。

错误写法把两个数字直接配对传给sum:sum 的第一个参数必须是可迭代对象,单个整数不可迭代,会报整数不可迭代的错误;用zip 配对后每次只传进一个元组,也不符合sum 的参数要求。

字典是什么

字典(dict)是Python 自带数据结构的最后一种,外层是大括号。

集合里逗号分隔的是一个个元素;字典里逗号分隔的是一对一对的数据,每一对中间用冒号连接,称为键值对。

d = {'name': '周周', 'age': 18, 'gender': '男'}

冒号前面叫键(key),冒号后面叫值(value),通过键来取值:

print(d['name'])

老版Python 字典无序,现代字典已经改良为有序(按定义顺序保存),但字典依然没有索引,键代替了索引的功能。

表格比喻:一个字典就像一张个人信息表,键是字段名(姓名、年龄、性别、居住地、电话),值是这个字段对应的具体数据。

字典只能通过键取值,不能通过值取键。

键的规则

字典里写相同的键,语法上不报错,但后面的值会覆盖前面的值,键只保留一个,所以正常不要写重复键。

键会自动去重,和集合一样建立在哈希算法之上,因此键不能是列表等可变类型,否则报unhashable type: ‘list’;键只能由不可变类型担任。

第 64 页

字典的创建方式

方式一:空大括号创建空字典。注意空大括号是字典不是集合,空字典是字典中唯一布尔值为False 的值。

d = {}

方式二:直接写键值对,最常用。

d = {1: '周周', 2: '周周', 3: '贝贝', 4: '语嫣'}

方式三:借助dict 函数,用关键字参数形式写。等号前面的变量名会自动加上引号变成字符串键,所以这种写法只能生成键全为字符串的字典。

d = dict(name1='周周', name2='周周', name3='贝贝', name4='语嫣')

方式四:把键值对打包成元组,再把多个元组放进列表交给dict。

d = dict([(1, '周周'), (2, '周周'), (3, '贝贝'), (4, '语嫣')])

方式五:dict 配合zip,让zip 自动把对应位置的键和值打包成元组;zip 返回的是迭代器,dict 同样接受,可迭代对象和迭代器都能传。

d = dict(zip([1, 2, 3, 4], ['周周', '周周', '贝贝', '语嫣']))

方式六:dict.fromkeys 批量生成同值字典。它要通过dict 这个类名点出来调用,第一个参数是装所有键的可迭代对象,第二个参数是统一的值。

d = dict.fromkeys([1, 2, 3, 4], '哈哈')

第二个参数是一个统一的值,不会自动拆分:即使你传一个列表,整个列表也会原样作为每个键的值。

第一个参数可以是字符串,字符串可迭代,每个字符成为一个键;不传第二个参数时,所有值默认填充None。

第 65 页

None 的意义:键有了,值的位置语法上必须有东西,但又不能乱填有特殊含义的值,None 就是专门占位、不携带其他含义的值。

字典长度由键值对的个数决定,一个键值对长度就是1。

字典的修改与新增

通过字典名[键] 重新赋值,键存在就是修改这个键的值,键不存在就是新增一个键值对。

d[1] = '啦啦啦'
d[3] = 'abcd'

update 批量扩展

字典的update 类似集合的update,但添加的是一个个键值对;更新过程中字典地址不变,证明字典是可变类型。

出现重复键时,后添加进去的值覆盖原来的值。

update 很灵活,可以直接传一个字典,也可以传能生成字典的元组列表、zip 迭代器,还可以直接写关键字参数。

d01.update(d02)
d01.update([(1, '周周'), (2, '贝贝')])
d01.update(c=4, d=5, e=6)

pop 与popitem 删除

pop(键) 删除的是整个键值对,不是只删值;函数返回的是被删键对应的值,拿变量可以接到。

键不存在时直接报KeyError;写上第二个参数作为默认值后,键存在就删除并返回值,键不存在就返回默认值,不会报错,也不会修改原字典。

a = d.pop(1)
b = d.pop(3, '没有')

popitem 不带参数,删除字典里最后一个键值对,并以(键, 值) 的元组形式返回。

第 66 页
c = d.popitem()

看到item 这个词,就要想到它跟键值对有关。

del 与引用计数、垃圾回收

del 字典名[键] 也是删除整个键值对;del 字典名则删除这个变量本身。

重点:del 删除的是引用,不是内存中的值本身。

a = 1,b = 1 时,内存里只有一个1,它身上有两个引用(引用计数为2)。

del a 只是删掉a 这个引用,b 还在,所以1 还在,b 仍能打印;引用计数减为0时,Python 解释器才会自动把这个值销毁,释放内存。

这就是Python 的内存管理回收机制,也叫垃圾回收机制:当内存中的值没有任何引用时被自动销毁,一切自动。

再记一遍:Python 里一切皆引用。

del 整个字典变量是强拆整栋房子;clear 方法是把房子里的人赶出去,房子还在,得到一个空字典。

get 查值

用字典名[键] 取值,键不存在会报KeyError。

get(键) 方法:键存在返回对应的值,键不存在不报错,返回None。

get(键, 默认值):键不存在时返回你指定的默认值。

print(d.get(1))
print(d.get(5, '讨厌'))

get 不会新建键值对,它只停留在方法层面,返回默认值给你用,不修改原字典。

字典的遍历

直接遍历字典,默认拿到的是键,键是字典与外界打交道的第一道屏障。

in 判断的也是键:1 in d 判断的是1 是否在键当中。

第 67 页

拿到键后,用d[键] 就能取到对应的值。

keys() 一次性获取所有键;values() 一次性获取所有值;items() 一次性获取所有键值对,每一对被打包成(键, 值) 元组。

print(d.keys())
print(d.values())
print(d.items())

这三个结果看起来像列表但不是列表,类型叫dict_keys、dict_values、dict_items,不能按索引取值,但它们可迭代;可迭代就足够了,可以直接遍历,也可以用list()、tuple() 随意转换。

遍历items 时可以直接拆包,同时拿到键和值:

for key, value in d.items():
    print(key, value)

字典嵌套

字典是可变类型,不能作为键,只能作为值,所以字典里嵌套字典是套在值里面。

取子字典里的值,像剥洋葱一样一层一层按键取:

print(d[1]['周周'])

列表嵌套取值是索引加索引,字典嵌套取值是键加键。

把字典直接放进list() 转换,得到的是所有键组成的列表:list 内部自带一次for 循环,而遍历字典本身默认拿到的就是键。

深浅拷贝的引入

拷贝就是复制,复制一定产生新数据:U 盘去同学电脑拷电影,同学电脑里的原片不会动。

引用、浅拷贝、深拷贝分两类:等号赋值的引用不拷贝任何值,只是建立联系;浅拷贝和深拷贝不管深浅都生成新数据。

a = 1 不是把1 复制给a,而是在a 和内存里的1 之间建立引用,a 存的是1 的地址;b = a 是两个变量同时引用同一个1。

第 68 页

拷贝需要import copy 内置模块。

浅拷贝

对于复合数据(比如列表里还有子列表),浅拷贝只拷贝最外面一层。

在你看来[1, 2, [3, 4, 5]] 是三个元素,在Python 看来是三个地址:1 的地址、2 的地址、子列表的地址。

浅拷贝确实生成了一个新的外层列表(外层地址变了),但里面三个位置存的还是原来的地址,两个列表共享内部元素。

import copy
a = [1, 2, [3, 4, 5]]
b = copy.copy(a)

改不可变元素a[0] = 6:不可变类型不能原地改,只能让这个位置断开与1 的联系、指向新建的6,另一个列表不受影响。

改子列表内部a[2][1] = 6:列表是可变类型,子列表原地修改时地址不变,而浅拷贝的另一个列表里那个位置仍指向这个子列表地址,所以会跟着变。

共享文档比喻:浅拷贝就像群里的共享文档,你能改、他能改,所有人的修改对同一份数据生效。

深拷贝

深拷贝只要遇到可变类型数据,就重新开一块内存地址把内容复制下来,一直复制到最后一层,外层和内层子列表都是新的。

b = copy.deepcopy(a)

深拷贝后,改不可变元素依旧互不影响;改子列表内部,另一个列表也不再变化,因为子列表已经彻底分开,不再是同一个地址。

不可变元素没有必要复制,所以深拷贝只对可变类型开新地址。

领导发数据比喻:怕你搞乱原数据,复制一份发给你,你随便改都不影响领导手里的原件。

深浅拷贝完全建立在可变与不可变的基础上:可变不可变是基础,深浅拷贝是它的应用。

浅拷贝并非没用:可以当作照相机,拷贝一份数据去窥探共享数据的变化;要彻底隔离就用深拷贝。

第 69 页

列表推导式

推导式是一种语法糖,用简明扼要的方式创建列表,不是必须,但熟练后能简化代码。

普通写法是空列表加for 循环加append:

a = []
for i in range(10):
    a.append(i)

列表推导式一句代码搞定:for 产生的i 放在最前面,整个结构包在中括号里。

a = [i for i in range(10)]

带条件,只放偶数:循环产生元素,元素通过if 判断才能放进列表。

a = [i for i in range(10) if i % 2 == 0]

放进去的不一定是i,也可以是i 的平方,由前面的表达式决定。

a = [i ** 2 for i in range(10) if i % 2 == 0]

不关心循环变量本身、只利用循环次数时,元素位置可以直接写函数:

a = [random.randint(1, 10) for i in range(10)]

上面生成十个1 到10 的随机整数;再加if,循环十次但只有五次通过,就生成五个随机数。

三元表达式(条件表达式)

普通写法根据条件给一个变量赋不同的值:

第 70 页
x = 10
if x > 0:
    result = '正数'
else:
    result = '非正数'

三元表达式把它压成一行:条件成立取if 前面的值,否则取else 后面的值。

result = '正数' if x > 0 else '非正数'

求两个数的最大值:

a, b = 5, 10
print(a if a > b else b)

三种情况可以在else 里再嵌套一层三元表达式:

result = '正数' if x > 0 else ('0' if x == 0 else '负数')

三元表达式和列表推导式可以组合:元素位置用三元表达式决定放什么。

a = [i if i < 10 else '哈哈' for i in range(100)]

上面生成100 个元素:前十个是0 到9,后面90 个都是“哈哈”。

集合推导式与字典推导式

在大括号里写推导式就是集合推导式,同样可以在for 后面加if,条件可以写多个。

a = {i for i in [1, 2, 3, 4, 5, 6, 7] if i % 2 == 0 and i > 5}

字典推导式用于快速生成字典。键值互换的普通写法:遍历items 拆包,把原值放到新键位置、原键放到新值位置。

第 71 页
d = {'a': 1, 'b': 2}
new_d = {}
for i, j in d.items():
    new_d[j] = i

推导式写法:

new_d = {j: i for i, j in d.items()}

加条件,只翻转值大于100 的键值对:

new_d = {j: i for i, j in d.items() if j > 100}

要求大于100 的翻转、小于等于100 的保持不变,就在键和值两个位置分别写三元表达式:

new_d = {j if j > 100 else i: i if j > 100 else j for i, j in d.items()}

推导式的初衷只是快速生成列表、集合、字典,提高编写速度;觉得绕就用普通写法,它是锦上添花,不是必须。

综合案例:找值最大的键

需求:字典里可能有多个键对应同一个最大值,把这些键找出来放进列表,例如值为10 的键有两个。

Python 没有通过值取键的操作,只能通过键取值。

先用max(d.values()) 找到最大值。

年会家属比喻:保安只认识员工(键),不认识家属(值),但员工和家属一定同时出现;遍历时键和值也是同时出现的,当值等于最大值时,就把配对的键拎出来。

result = [k for k, v in d.items() if v == max(d.values())]
print(result)

代码是从复杂写到简单的,先把普通的循环和判断写对,再慢慢压缩成推导式,不要一开始就逼自己一句写完。复盘:字典靠键值对存数据,键唯一、键必须不可变;增改靠d[键]=值和update,删靠pop

第 72 页

、popitem、del、clear,查靠get;遍历默认拿键,keys、values、items 各取所需;浅拷贝只复制外层、内部共享,深拷贝连可变的内层一起复制;推导式和三元表达式是简化写法,基础是可变与不可变、一切皆引用。

作业一:编写程序让用户输入一个字符串(如apple),统计每个字符出现的次数并存入字典,每个字符作为键、次数作为值,输入apple 应得到{‘a’: 1, ‘p’: 2, ‘l’: 1, ‘e’: 1};提示遍历字符串,字符已在字典里就把值加一,不在就新增键值对。

作业二:生成一个随机数列表(如20 个1 到10 的随机整数),再统计每个随机数出现的次数,结果存入字典;提示用random.randint 配合列表推导式生成列表,再用作业一的思路统计。

第 73 页
返回首页返回「Python」