开始前需要什么 你以前运行过一个小的 Python 程序。如果没有,请先读 Python 入门指南。本篇从“身边没有计算机帮你检查”这一步开始。 一支笔或铅笔、一块橡皮,以及方格纸或横线纸。在方格纸上画表格更方便。 每课大约 20 分钟。检查题要写在纸上,不要只在脑子里想。 本篇目录 像计算机一样读代码 画出名字和对象 要背熟的语法 手写时常见的错误 给自己打分 01
像计算机一样读代码 学完你能 你能一行一行地手动运行一小段 Python 程序,并写下它用到的每个名字和值。
在纸上写代码时,没有任何东西会告诉你某一行写错了。你唯一的检查办法,是自己把程序运行一遍:一半在脑子里,一半写在纸上。这叫手动跟踪(英文叫 dry run):你扮演计算机,拿着笔,一行一行地运行程序。
计算机不知道你的程序是做什么用的。它严格按每一行写的内容,按顺序执行,并且只记得存在各个名字之下的东西。在 Python 里,名字就是一个值的标签,例如 count 或 items。你在跟踪时也要这样做:按这一行实际写的内容去做,而不是按你原本想写的内容去做。
用的工具叫跟踪表。跟踪表里每个名字占一列,每执行一行代码占一行。分四步来做。
1. 在页边给程序的每一行编号,从 1 开始。 2. 画表格。第一列是“行号”,接着每个名字一列,最后一列叫“备注”,用来写发生了什么,例如“条件为真”,或者程序输出了什么。 3. 开始运行。Python 从第 1 行开始,往下执行。每执行一行,就加一行记录:先写行号,再写这一行执行完之后每个名字的值。没有变化的值要从上一行抄下来,这样每一行单独看也能读懂。 4. 对还不存在的名字写短横线(–)。在 Python 里,名字是在第一次有东西存到它下面时才出现的。在此之前使用它,程序会因 NameError 而停止。
以 def 开头的那一行只是创建函数。函数里面的代码要等到有人调用这个函数时才会运行。所以 def 那一行只占一行记录,函数体的记录要到调用的时候才出现。
Python 用缩进(行首的空格)表示哪些代码属于一起。在 for、if 或 def 下面缩进的行,属于它们。回到左边,就离开了那个代码块。
循环语句会执行很多次,所以会有很多行记录。对于 for x in items 这种形式,每当 x 取到下一个元素,就加一行记录,写下 x 的新值。元素取完之后,再加最后一行记录,表示循环结束。和 C、Java 不同,循环结束之后名字 x 仍然存在,保留着最后一个元素。
if 语句只占一行记录。如果条件为假,下一行记录就是缩进代码块之后的第一行代码。
程序调用函数时,函数的参数会成为新的名字,初始值就是你传进去的值。把它们放在同一张表里,并在备注里说明现在在函数内部。函数返回时,把返回值写在备注里。
新词
手动跟踪(dry run) 不用计算机,拿笔手动运行一个程序。
跟踪表 一张表格:每个名字一列,每执行一行代码一行。
名字(变量) 一个值的标签,例如 count。名字在第一次有东西存到它下面时出现。
缩进 行首的空格,表示哪些行属于某个 for、if 或 def。
None 表示“这里什么都没有”的值。没有 return 的函数会返回它。 完整例子 Python 3 复制代码
def count_big ( items , limit ):
count = 0
for x in items :
if x > limit :
count = count + 1
return count
print ( count_big ([ 3 , 8 , 5 ], 4 ))
先预测输出,再看结果 逐步讲解 先做准备。从 1 开始给有代码的行编号:def count_big 是 1,count = 0 是 2,for x in items 是 3,if x > limit 是 4,count = count + 1 是 5,return count 是 6,print 是 7。空行这里不编号。画六列:行号 items limit count x 备注。 第 1 行 items = – limit = – count = – x = – def 创建函数 count_big。函数体现在不运行。Python 接着执行下一行代码,也就是第 7 行。 第 7 行 items = [3, 8, 5] limit = 4 count = – x = – print 需要 count_big([3, 8, 5], 4) 的值,所以先进行调用。参数 items 和 limit 是传入值的新名字。备注:进入 count_big。 第 2 行 count = 0 创建 count 并存入 0。 第 3 行 x = 3 for 这一行取 items 的第一个元素。备注:第一个元素。 第 4 行 x = 3 count = 0 3 > 4 为假。缩进的第 5 行被跳过。 第 3 行 x = 8 count = 0 for 这一行取下一个元素。 第 4 行 x = 8 count = 0 8 > 4 为真,进入缩进的代码块。 第 5 行 x = 8 count = 1 count = 0 + 1,存入 1。 第 3 行 x = 5 count = 1 for 这一行取最后一个元素。 第 4 行 x = 5 count = 1 5 > 4 为真。 第 5 行 x = 5 count = 2 count = 1 + 1,存入 2。 第 3 行 x = 5 count = 2 没有元素了,所以循环结束。x 仍然存在,保留着最后一个元素 5。 第 6 行 x = 5 count = 2 return count 把 2 送回调用处。备注:返回 2。 第 7 行 回到主程序,print 输出 2。程序输出 2。
常见错误 错误写法 for i in range(1, 4):
print(i) range(1, 4) 在 4 之前停止,得到 1、2、3。如果你想要 1、2、3、4,第二个数字应该比最后一个想要的值大 1:range(1, 5)。把 i 的取值写成一排,最后一个值一眼就能看出来。
正确写法 for i in range(1, 5):
print(i) 错误写法 for i in range(7 / 2):
print(i) 在 Python 里,/ 总是得到浮点数,所以 7 / 2 是 3.5,而 range 不能用浮点数来数数,程序会因 TypeError 而停止。// 做除法后向下取整:7 // 2 是 3。
正确写法 for i in range(7 // 2):
print(i) 错误写法 for i in range(3):
print(i)
print("done") 两条 print 都缩进了,所以都属于循环。done 会输出三次,每轮一次。如果你想让它只在最后输出一次,就把它移回左边,与 for 对齐。
正确写法 for i in range(3):
print(i)
print("done") 错误写法 def add(a, b):
result = a + b
print(add(1, 2)) 没有 return 的函数会返回 None。和被算出来,函数一结束就被丢掉了。你的跟踪表里这次调用那一行的备注会是:返回 None。要写 return result。
正确写法 def add(a, b):
result = a + b
return result
print(add(1, 2)) 检查自己 为下面四行代码(编号 1 到 4)做一张跟踪表:a = 4,然后 b = a,然后 a = a + 1,然后 b = b * 2。最后 a 和 b 各是多少?
先自己做,再看答案 第 1 行:a = 4,b = –。第 2 行:a = 4,b = 4。第 3 行:a = 5,b = 4。第 4 行:a = 5,b = 8。最后 a 是 5,b 是 8。第 3 行只改变 a,第 4 行用的是 b 自己的值。
不运行,直接预测:for n in range(10, 0, -3): print(n) 会输出什么?写出 n 的每个取值。
先自己做,再看答案 依次输出 10、7、4、1,各占一行。步长是 -3,下一个值 -2 没有大于终点 0,所以循环结束。
在上面的完整例子里,第 6 行那一行记录中,x 这一列应该写什么?为什么?
先自己做,再看答案 写 5。Python 的循环变量在循环结束后仍然存在,保留着最后一个元素。[3, 8, 5] 的最后一个元素是 5。
02
画出名字和对象 学完你能 你能画出 Python 程序在任意一行时的名字和对象,说清 = 做了什么,并分辨可变对象和不可变对象。
在 Python 里,名字不是装着值的盒子。名字是贴在对象上的标签,就像贴在罐子上的标签。两张标签可以贴在同一个罐子上。这一点从代码里看不出来,必须画出来。每次都用同一种画法。
画两列:左边是名字,右边是对象。
对象。每个值都是对象:数字 5、文字 "hi"、列表 [1, 2]。把每个对象画成一个方框,写上它的类型和值,例如 int 10 或 str "hi"。给每个对象一个标签 O1、O2、O3,按创建的顺序编号。列表是一个方框,每个元素占一个单元。单元里如果是数字或文字,可以直接写出值;如果是列表或别的可以改变的对象,就画一支箭头指向它。
名字。名字是左边的一个标签,恰好有一支箭头指向一个对象。名字自己没有值。函数里的名字画在栈帧里,也就是围住它们的一个方框,只在函数运行期间存在。
= 做了什么。语句 b = a 从不复制任何东西。它只是在 a 已经指向的对象上,再贴一张标签 b。语句 a = 5 找到或创建对象 5,并把 a 的箭头移过去。赋值移动的是箭头,不是复制对象。
不可变与可变。不可变对象在创建之后永远不会改变。数字、字符串、元组和 None 是不可变的。可变对象可以原地改变。列表、字典和集合是可变的,你自己定义的类的对象也是。
对不可变对象,s = s + "!" 不会碰旧的字符串,而是创建一个新字符串,并把 s 的箭头移过去。旧字符串上的其他标签看到的仍然是旧文字。对可变对象,b.append(3) 改变的是列表本身,贴在这个列表上的每张标签看到的都是改变后的内容。同一个对象有两个名字,叫做别名。
复制。b = a.copy() 创建一个新列表,单元与原来相同。这是浅拷贝:单元里存的是指向原来那些元素的箭头。如果某个元素本身是列表,两个外层列表就共用它。把箭头画出来,你就会看到这一点。
函数调用。调用会产生新的栈帧。它的参数名是贴在调用者传入的那些对象上的新标签。所以函数可以就地修改可变的实参。但是像 param = something 这样的语句,只会移动栈帧里的标签。
相等与同一个。a == b 问的是两个对象看上去是否相等。a is b 问的是两个名字是否贴在同一个对象上。
垃圾。既没有标签、也没有箭头指向的对象,再也不可能被访问到。Python 会把它清除。在图里把它划掉。
新词
对象 内存里的一个值,例如 5、"hi" 或 [1, 2]。Python 里的每个值都是对象。
名字 贴在对象上的标签。它指向对象,自己没有值。
不可变 创建之后不能改变:数字、字符串、元组、None。
可变 可以原地改变:列表、字典、集合、你自己定义的类的对象。
别名 同一个对象的第二个名字。
浅拷贝 新建的外层对象,它的单元通向与旧对象相同的那些元素。
栈帧 围住一次函数调用中所有名字的方框,只在这次调用运行期间存在。 完整例子 Python 3 复制代码
a = [ 1 , 2 ]
b = a
b . append ( 3 )
c = a . copy ()
c . append ( 4 )
x = 10
y = x
y = y + 1
nested = [ a , [ 5 ]]
dup = nested . copy ()
dup [ 1 ] . append ( 6 )
dup [ 0 ] = [ 0 ]
print ( a , b , c )
print ( x , y )
print ( nested )
print ( dup )
先预测输出,再看结果 预期输出 [1, 2, 3] [1, 2, 3] [1, 2, 3, 4]
10 11
[[1, 2, 3], [5, 6]]
[[0], [5, 6]] 逐步讲解 a = [1, 2]。画两列:名字和对象。创建列表 O1,单元是 1 和 2,再画一个标签 a,箭头指向 O1。
b = a。没有创建任何新东西。在 O1 上再贴一张标签 b。一个列表,两个名字。
b.append(3)。这个方法改变的是 O1 本身:它现在有 1、2、3 三个单元。a 和 b 都通向 O1,所以两者看到的都是 [1, 2, 3]。
c = a.copy()。创建新列表 O2,单元是 1、2、3,c 通向 O2。接着 c.append(4) 只改变 O2,它变成 [1, 2, 3, 4]。O1 不受影响。
x = 10。创建 int 对象 O3,值为 10,再画标签 x,箭头指向它。接着 y = x 把标签 y 也贴在同一个 O3 上。
y = y + 1。先算出 y + 1,得到 int 值 11。把它画成一个新对象 O4。(Python 也许会重用已经存在的 11,但数字永远不会改变,所以你看不出区别。)然后 y 的箭头移到 O4。O3 里仍是 10,x 仍然通向它。数字从不会被原地改变。
nested = [a, [5]]。创建列表 O5,有两个单元。单元 0 画一支箭头指向 O1(也就是 a 所指的那个列表)。单元 1 画一支箭头指向新列表 O6,里面是 5。标签 nested 通向 O5。
dup = nested.copy()。创建新列表 O7,它的两个单元里是同样的两支箭头:单元 0 指向 O1,单元 1 指向 O6。这是浅拷贝:元素是共用的。标签 dup 通向 O7。
dup[1].append(6)。dup[1] 顺着 O7 单元 1 的箭头走到 O6。append 把 O6 改成 [5, 6]。O5 的单元 1 也通向 O6,所以 nested 里看到的也是 [5, 6]。
dup[0] = [0]。创建新列表 O8,里面是 0,O7 单元 0 的箭头移到 O8。这只改变了 dup 的那个单元:O5 的单元 0 仍然通向 O1。
输出。a、b、c 分别是 [1, 2, 3]、[1, 2, 3] 和 [1, 2, 3, 4]。x 和 y 是 10 和 11。nested 是 [[1, 2, 3], [5, 6]],dup 是 [[0], [5, 6]]。
常见错误 错误写法 scores = [70, 80]
backup = scores
scores.append(90) 本意是保留旧列表的备份,但 backup = scores 只是在同一个列表上贴了第二张标签。scores 变长时,backup 显示的也是 [70, 80, 90]。要用 scores.copy() 创建一个独立的列表。
正确写法 scores = [70, 80]
backup = scores.copy()
scores.append(90) 错误写法 def add_item(item, items=[]):
items.append(item)
return items 默认值 [] 只在 def 那一行运行时创建一次,每一次省略 items 的调用,拿到的都是贴在同一个列表上的标签。所以 add_item(1) 得到 [1],接着 add_item(2) 得到 [1, 2],而不是 [2]。应该把默认值写成 None,并在函数内部创建新列表。
正确写法 def add_item(item, items=None):
if items is None:
items = []
items.append(item)
return items 错误写法 grid = [[0] * 3] * 3
grid[0][0] = 1 [[0] * 3] * 3 重复的是箭头,不是那一行。外层列表的三个单元全都通向同一个内层列表。设置 grid[0][0] 改的就是这一行,三行看起来都变了。要用推导式分别创建每一行。
正确写法 grid = [[0] * 3 for _ in range(3)]
grid[0][0] = 1 字符串是不可变的,不能原地修改其中某一个字母,Python 会因 TypeError 而停止。要创建一个新字符串并移动标签:s = "H" + s[1:]。
正确写法 s = "hi"
s = "H" + s[1:] 检查自己 画出来:s = "hi",然后 t = s,然后 s = s + "!"。最后 s 和 t 各通向什么?
先自己做,再看答案 执行 t = s 之后,两张标签都贴在字符串 O1 "hi" 上。然后 s + "!" 创建新字符串 O2 "hi!",s 的箭头移到 O2。t 仍然通向 O1。所以 s 是 "hi!",t 是 "hi"。字符串不可变,t 永远不会变。
画出来:x = [1, 2],然后 y = [1, 2],然后 z = x。x == y、x is y 和 x is z 哪些为 True?
先自己做,再看答案 一共有两个列表:O1,由 x 和 z 通向;O2,由 y 通向。x == y 为 True,因为两个列表看上去相等。x is y 为 False,因为它们是不同的对象。x is z 为 True,因为两张标签贴在同一个对象上。
有一个函数 def f(v):,下面有两行:v.append(0),然后是 v = [9]。执行 a = [1] 和 f(a) 之后,print(a) 显示什么?
先自己做,再看答案 显示 [1, 0]。在调用里,v 是贴在 a 所通向的那个列表上的第二张标签,所以 v.append(0) 改变的是这个共享的列表。接着 v = [9] 只是把栈帧里的标签 v 移到一个新列表上,不会影响 a 通向的那个列表。
03
要背熟的语法 学完你能 你能凭记忆写出下面八个 Python 骨架,从最短的开始,每个冒号和缩进都不缺。
有些代码没法当场推出来,你必须不看资料就知道它的形状。这一课的八个骨架,是 Python 里反复要用的。它们都很短,可以背下来。按下面的顺序学,从最短的开始,上一个背牢了,再加下一个。
1. def,以及 if、elif、else。2. for。3. f 字符串。4. 列表的方法。5. 字典的方法。6. 推导式。7. try 和 except。8. 带 __init__ 的类。
背诵的办法:盖住页面,在纸上默写这个骨架,再和页面逐个字符对照。只有完全对、中途没有偷看,才打勾。明天再来一遍。连续三天都能写对的骨架,才算学会。
下面的程序包含全部八个骨架,每个骨架旁边有一条形如 (2) 的注释。先读一遍,再盖住它,在你自己的纸上把每个骨架单独默写出来,每个只写几行。
有两件事永远不能忘。每一个开启代码块的行都以冒号结尾:def、if、elif、else、for、while、class、try、except。每个代码块的缩进量都要一致,通常是四个空格。
列表的方法。words.append(x) 在末尾添加。words.pop() 删除最后一个元素并把它交还给你。words.insert(i, x) 把 x 放在位置 i。words.remove(x) 删除第一个 x。words.sort() 原地排序,返回 None。sorted(words) 返回一个新的排好序的列表。len(words) 是长度。words[0] 是第一个元素,words[-1] 是最后一个,words[1:3] 是切片:位置 1 和 2,因为不包括末尾位置。
字典的方法。d[key] = value 存入。d[key] 读取,键不存在时会以 KeyError 停止。d.get(key, default) 读取,键不存在时给出默认值。key in d 问键是否存在。d.keys()、d.values() 和 d.items() 分别给出键、值和(键,值)对。del d[key] 删除。
新词
骨架 一段代码固定不变的形状,细节留待填写。
推导式 用一行从另一个集合创建列表或字典的写法,例如 [n * n for n in range(4)]。
f 字符串 引号前有字母 f 的字符串。{ } 里的名字和表达式会被放进文字里。
异常 中断正常流程的错误,例如 ValueError。except 块可以处理它。
self 方法的第一个参数,代表调用这个方法的那个对象。 完整例子 Python 3 复制代码
# (1) def, with if, elif and else
def average ( nums ):
return sum ( nums ) / len ( nums )
def label ( score ):
if score >= 8 :
return "high"
elif score >= 4 :
return "mid"
else :
return "low"
# (4) list methods
words = [ "pear" , "fig" , "apple" ]
words . append ( "kiwi" )
words . sort ()
last = words . pop ()
print ( words , last , len ( words ))
# (5) dict methods, and (2) for
lengths = {}
for w in words :
lengths [ w ] = len ( w )
for key , value in lengths . items ():
# (3) f-string
print ( f " { key } has { value } letters" )
print ( lengths . get ( "plum" , 0 ), "fig" in lengths )
# (6) comprehensions
squares = [ n * n for n in range ( 4 )]
long_words = [ w for w in words if len ( w ) > 3 ]
sizes = { w : len ( w ) for w in words }
print ( squares , long_words , sizes )
# (8) class and __init__
class Counter :
def __init__ ( self , name ):
self . name = name
self . total = 0
def add ( self , amount ):
self . total += amount
c = Counter ( "steps" )
c . add ( 5 )
c . add ( 7 )
print ( f " { c . name } : { c . total } , average { average ([ c . total , 8 ]) : .1f } , { label ( c . total ) } " )
# (7) try and except
try :
n = int ( "12a" )
except ValueError :
n = - 1
print ( n )
先预测输出,再看结果 预期输出 ['apple', 'fig', 'kiwi'] pear 3
apple has 5 letters
fig has 3 letters
kiwi has 4 letters
0 True
[0, 1, 4, 9] ['apple', 'kiwi'] {'apple': 5, 'fig': 3, 'kiwi': 4}
steps: 12, average 10.0, high
-1 逐步讲解 (1) def,以及 if、elif、else。def 名字(参数): 然后是缩进的函数体。return 把一个值交还回去。if 条件: ... elif 条件: ... else: ... 这些行都以冒号结尾,elif 和 else 与 if 左对齐。average(nums) 用总和除以长度。label(score) 在 8 及以上给出 high,4 到 7 给出 mid,其余给出 low。
(2) for。for 元素 in 集合: 然后是缩进的循环体。for key, value in lengths.items(): 每次取一对,并给每一部分起各自的名字。要重复 n 次,用 for i in range(n):,i 取 0 到 n - 1。
(3) f 字符串。f"文字 {name} 更多文字 {value}"。字母 f 紧贴在左引号前面。花括号里的东西会被算出来并放进文字里。冒号后面可以接格式:{x:.1f} 写出保留一位小数的 x。这一行输出 apple has 5 letters,依此类推。
(4) 列表的方法。words 一开始是 pear、fig、apple。append("kiwi") 在末尾加入 kiwi。sort() 按字母顺序排好:apple、fig、kiwi、pear。pop() 删除 pear 并把它交还回来,所以 last 是 pear,words 是 apple、fig、kiwi。这一行输出 ['apple', 'fig', 'kiwi'] pear 3。
(5) 字典的方法。lengths = {} 创建空字典。lengths[w] = len(w) 存入一对。lengths.get("plum", 0) 得到 0,因为 plum 不是键。"fig" in lengths 为 True。这一行输出 0 True。
(6) 推导式。[表达式 for 元素 in 集合] 创建列表。在末尾加 if 条件,只保留一部分元素。{键: 值 for 元素 in 集合} 创建字典。表达式写在最前面。squares 是 [0, 1, 4, 9],long_words 是 ['apple', 'kiwi'],sizes 是长度的字典。
(7) try 和 except。try: 下面缩进写有风险的代码,然后 except 错误名: 下面缩进写备用方案。int("12a") 无法把这段文字读成数字,于是引发 ValueError,try 块里剩下的部分被跳过,except 块把 n 设为 -1。程序输出 -1。
(8) 带 __init__ 的类。class 名字: 然后 def __init__(self, 参数): 在里面设置 self.属性 = 值。每个方法的第一个参数都是 self。调用时不写 self:c.add(5) 调用 add,并把 self 设为 c。程序输出 steps: 12, average 10.0, high。
常见错误 错误写法 words = ["pear", "fig"]
ordered = words.sort() sort() 原地改变列表并返回 None,所以 ordered 是 None。要么原地排序然后继续用 words,要么用 sorted(words) 得到一个新的排好序的列表。
正确写法 words = ["pear", "fig"]
ordered = sorted(words) 错误写法 ages = {"Leon": 20}
age = ages["Emma"] 字典里没有键 Emma,所以 d[key] 会让程序因 KeyError 而停止。如果键可能不存在,就用 d.get(key, default),或者先用 key in d 判断。
正确写法 ages = {"Leon": 20}
age = ages.get("Emma", 0) 错误写法 words = ["pear", "fig"]
n = words.length
m = words.size() Python 询问“有多少个”只有一种办法:函数 len,把对象放在括号里。列表没有 length 字段,也没有 size 方法。要写 len(words)。
正确写法 words = ["pear", "fig"]
n = len(words) 错误写法 name = "Mia"
print("Hello {name}") 引号前面没有字母 f,它就是一个普通字符串,花括号会原样输出:Hello {name}。不会报任何错误。要写 f"Hello {name}"。
正确写法 name = "Mia"
print(f"Hello {name}") 检查自己 在纸上默写一个函数 longest(words):用 for 循环,返回一个非空列表中最长的字符串。
先自己做,再看答案 def longest(words): 接着缩进写 best = words[0],然后 for w in words:,再缩进写 if len(w) > len(best):,再缩进写 best = w,最后回到第一层缩进写 return best。对照检查:def、for、if 后面有冒号;每个代码块里缩进一致;return 在循环外面。
写一行代码:用推导式创建 0 到 8 中偶数的平方组成的列表。
先自己做,再看答案 [n * n for n in range(9) if n % 2 == 0] 结果是 [0, 4, 16, 36, 64]。表达式 n * n 写在最前面,然后是 for,最后是 if。
写一个类 Dog:__init__ 接收名字并保存它;再写一个方法 speak,用 f 字符串返回“名字 says woof”这段文字。
先自己做,再看答案 class Dog: 接着缩进写 def __init__(self, name):,再缩进写 self.name = name,退回一层写 def speak(self):,再缩进写 return f"{self.name} says woof"。对照检查:两个方法的第一个参数都是 self,读取属性时用 self.name。
04
手写时常见的错误 学完你能 你能把自己写的 Python 答案像解释器那样再读一遍,抓住本来会丢分的小错。
在计算机上,Python 会在第一个语法错误处拦住你,并告诉你是哪一行。在纸上,没有人拦你,而这些小错每一个都会丢分,尽管它们和你是否理解题目毫无关系。所以你必须自己当自己的解释器。
答案写完之后,再读第二遍。这一遍不要问逻辑对不对,只问形式对不对。照下面的清单,用笔逐项打勾。
1. 每一个开启代码块的行都以冒号结尾:def、if、elif、else、for、while、class、try、except。 2. 同一个代码块的各行缩进量一致。代码块之后的行要退回左边。 3. 每个 ( [ { 都有配对的另一半,每个左引号都有同一种的右引号。 4. = 是存入一个值,== 是提出一个问题。Python 没有 ++:要写 += 1。 5. 用 and、or、not。用 elif,不要写 else if。True、False、None 的首字母要大写。 6. 名字区分大小写:count 和 Count 是不同的名字。 7. 类的每个方法的第一个参数都是 self,读写属性时要用 self。 8. 需要交还一个值的函数要有 return。没有 return,它交还的是 None。 9. 文字和数字不能混着算:"5" + 1 是错误。要用 int() 或 str() 转换。 10. 拼错的名字,Python 要等那一行真正运行时才会指出来。把每个名字都和它第一次被存入的地方核对一遍。
下面是最容易丢分的四组错误。每组里第一块是常见的错误写法,第二块是改正后的写法。
常见错误 错误写法 def double(x)
return x * 2
for i in range(3):
print(i) 两处错误。def 那一行末尾没有冒号。print 那一行没有缩进,所以 for 没有循环体。Python 对这两处都会拒绝。每个开启代码块的行后面都要加冒号,然后把它下面的行缩进四个空格。
正确写法 def double(x):
return x * 2
for i in range(3):
print(i) 错误写法 if x > 0 && y > 0:
count++
else if x == 0:
print("zero") 三处来自 C 和 Java 的习惯。Python 里 && 要写成 and。它没有 ++,要写 count += 1。else if 要写成 elif。Python 会在其中第一处报语法错误。
正确写法 if x > 0 and y > 0:
count += 1
elif x == 0:
print("zero") 错误写法 if x = 3:
print("three")
if x == 3 or 4:
print("three or four") 两处错误。单个 = 是存入,条件里不允许这样写,Python 会报语法错误。而且 x == 3 or 4 的意思是 (x == 3) or 4。4 永远为真,所以第二个 print 总会执行。要把每个比较写完整,或者用 x in (3, 4)。
正确写法 if x == 3:
print("three")
if x == 3 or x == 4:
print("three or four") 错误写法 class Dog:
def __init__(name):
self.name = name
def rename(self, new):
name = new 两处错误。__init__ 的第一个参数必须是 self,否则 Python 无法把新对象传进来,Dog("Rex") 会失败。另外,在 rename 里,name = new 只创建了一个局部名字,方法一结束就消失了。属性应当写成 self.name。
正确写法 class Dog:
def __init__(self, name):
self.name = name
def rename(self, new):
self.name = new 检查自己 改正两处错误:else if x == 3 print("three")(两处:一个关键字,和一个漏掉的符号)。
先自己做,再看答案 应该写成 elif x == 3:,然后 print 放到下一行并缩进:print("three")。也就是用 elif 代替 else if,并在条件后面加冒号。
改正这个循环的开头:for i in range(len(items): total += items[i]
先自己做,再看答案 括号没有配对:range( 和 len( 都打开了,却只关闭了一个。应写成 for i in range(len(items)):,并把 total += items[i] 放到下一行并缩进。把括号成对地数一遍就能发现。
在类 Dog 里,方法 __init__(self, name) 中有一行 name = name。哪里错了?应该怎么写?
先自己做,再看答案 它把参数又存回同一个局部名字,方法结束后什么也没有留下。属性需要 self:self.name = name。
05
给自己打分 学完你能 你能对照评分标准诚实地给自己的手写答案打分,找出分数丢在哪里,安排明天的练习,并在限时下写代码。
在纸上,一页写满的答案看起来像是完成了,其实可能是错的。给自己打分能解决这个问题。你拿答案去对照评分标准。评分标准是一组条目,每条值若干分,每条描述一件你能在答案里看到的东西。要严格。真正算数的分数,是一个陌生人会给你的分数。
怎样逐条读评分标准。
1. 读一条。问自己:一个正确的答案里,我到底会看到什么? 2. 在你的答案里找到那样东西,用笔尖点住它。指得出来,就打勾并写下这条的分数;指不出来,这一条就是零分。 3. “差不多”不给半分。只有评分标准写明怎么拆分时,才给一条的部分分数。 4. 检查每一条路径。像“处理空字符串”这样的条目,如果只有正常情况能工作,就是零分。 5. 把各条加起来,把总分写成“得分/满分”的形式放在页面上。
怎样找出分数丢在哪里。把每一个丢掉的分放进四个格子之一。思路:我不知道怎么解。记忆:我知道,但记不起语法或方法名。小错:在我本来会的东西上犯的小错误,例如漏了冒号或漏了 return。时间:我没写完。数一数每个格子。最大的格子告诉你该练什么。思路:重读对应的课。记忆:多做“盖住再默写”。小错:用上一课的解释器检查清单,再加一个很小的测试。时间:练习先做计划。
第二天要做什么。第一,在空白页上不看资料,把同一道题的答案重写一遍。第二,做一道同类型的不同题目。第三,把两份答案再各打一次分。三天后和一周后再重复。当最大的格子换了,说明你在进步。
怎样在限时下写。
1. 先做计划。用掉最初的十分之一时间。写出 def 那一行,再写三四行说明步骤的注释。不要跳过这一步。 2. 代码块之间空一行。你会需要空间补写漏掉的一行。Python 对缩进很敏感,所以缩进要写得尽量清楚,并且每次都用同样的四个空格。 3. 名字要起得清楚:用 count,不要用 c;用 longest,不要用 l。名字清楚,就不会把两个名字混在一起。 4. 用一个很小的输入手算测试。代码写完后,用最小的情况走一遍:只有一项、空列表、只有一个字母的单词。再走一个有重复值的情况。这只要一分钟,却能找出大部分边界错误。 5. 大约每分花一分钟。某一部分卡住了,就写一行注释说明你打算怎么做,然后往下写,回头再补。 6. 留出两分钟做解释器检查。
在“练习”页面里,题型为“手写代码”的题目有手写模式:有计时器、参考答案,以及可以打勾的评分标准。完全按这一课的办法用它:先在纸上写,再打分。
新词
评分标准 一组条目,每条值若干分,每条描述一件你能在答案里指出来的东西。
边界情况 落在边缘上的测试输入,例如只有一项、空字符串或有重复值。
解释器检查 把答案再读一遍,只检查形式:冒号、缩进、括号、self。 完整例子 Python 3 复制代码
def count_letters ( word ):
counts = {}
for ch in word :
counts [ ch ] = counts . get ( ch , 0 ) + 1
print ( count_letters ( "aba" ), "(expected {'a': 2, 'b': 1})" )
先预测输出,再看结果 预期输出 None (expected {'a': 2, 'b': 1}) 逐步讲解 题目:写 count_letters(word),返回一个字典,说明 word 中每个字母出现了多少次。评分标准有四条,每条一分。(1) def 那一行正确:函数名、一个参数,以及冒号。(2) 从一个空字典开始。(3) 访问每个字母,给该字母的计数加 1,包括第一次遇到这个字母的情况。(4) 返回这个字典。
给第 1 条打分。def count_letters(word): 是对的,用笔点住它。+1。
给第 2 条打分。counts = {} 在那里。+1。
给第 3 条打分。for ch in word 访问每个字母,counts[ch] = counts.get(ch, 0) + 1 加 1,字母是新的时候从 0 开始。+1。
给第 4 条打分。笔下什么也指不出来:没有 return 那一行。0 分。
得分:4 分里得 3 分。现在手算一个很小的测试:单词 "aba"。字典在函数内部建对了,但函数结束时没有 return,所以调用交还的是 None。程序输出 None (expected {'a': 2, 'b': 1})。丢掉的那一分放进“小错”格:思路是对的,只是漏了一行。改法是加上 return counts,缩进一层,与 for 对齐。
常见错误 错误写法 counts = {}
for ch in word:
counts[ch] += 1 第一次遇到某个字母时,counts[ch] 还不存在,所以 counts[ch] += 1 会以 KeyError 停止。用很小的测试 "a" 就能立刻发现。要从默认值开始:counts[ch] = counts.get(ch, 0) + 1。
正确写法 counts = {}
for ch in word:
counts[ch] = counts.get(ch, 0) + 1 错误写法 total = 0
for i in range(1, len(items)):
total += items[i] range(1, len(items)) 从位置 1 开始,所以 items[0] 从来没有被加进去。用只有一项的小测试就能看到:总和一直是 0。要从 0 开始,或者干脆不用位置,直接遍历元素本身。
正确写法 total = 0
for x in items:
total += x 错误写法 def total(items):
s = 0
for x in items:
s += x
print(s) 题目说的是函数“返回”总和,这个函数却把它打印出来,返回的是 None,调用者拿不到可用的值。这是读题的疏漏:写之前先在题目里找出“返回”“打印”“读取”这些词,并划上线。
正确写法 def total(items):
s = 0
for x in items:
s += x
return s 检查自己 评分标准里有一条“对空字符串给出正确结果”。你的函数对 "aba" 能工作,但对 "" 会崩溃。这一条得多少分?为什么?
先自己做,再看答案 0 分。这一条说的是一条路径,而这条路径出错了。把笔点在处理空字符串的代码上,没有任何能工作的代码。要用很小的输入测试每一条路径,不能只测常见的那一条。
你有 12 分钟,完成一道 12 分的题:写一个带 __init__ 和一个方法的类,再写几行测试它。写出带时间的计划。
先自己做,再看答案 1 分钟:把 class 那一行和两个 def 行先写成注释。4 分钟:写类。2 分钟:为一个很小的情况写测试行。3 分钟:用跟踪表手算这个情况。2 分钟:做解释器检查,核对冒号、缩进和 self。
写出你会用来手算测试“返回单词中出现最多的字母的函数”的几个很小的输入。
先自己做,再看答案 空字符串(先决定返回什么,再检查代码是否照做)、只有一个字母的单词、两个字母并列最多的单词(谁胜出?),以及像 "aab" 这样有明确胜者的单词。把每个都用跟踪表走一遍。
独立整理的学习笔记,不是官方课程材料。