《像计算机科学家一样思考Python》学习笔记
目录
-
-
前言
-
重要的中英文互译
-
Python小白与大神的区别
-
- 条件表达式
- 列表高端操作
- 收集关键词参数
-
第一章
-
- 何谓程序
- 树立“值才是程序最基本的单位,而不是变量”的概念
- 形式语言和自然语言
-
第二章
-
- 变量及命名
- 表达式和语句的定义
- 字符串可进行的数学操作
-
第三章
-
- 模块及其使用方法
- 函数的定义
- 形参与实参
- 栈图
-
第四章
-
第五章
-
- Python需要注意的运算符
- 条件链(chained conditional)
- 递归(Recursion)
- 无限递归
- 键盘输入
-
第六章
-
- isinstance()检查输入的类型
- 调试的思想
-
第八章
-
- 搜索
- in在字符串中的妙用
-
第十章:列表
-
- sum在列表中的妙用
- 从列表中删除元素的方法
- 列表和字符串
-
第十二章:字典
-
- 创建一个字典
- 访问字典中的元素
- len和in在字典中的应用
- 如何查看一个变量是否是字典的键或值
- 循环在字典中的应用
- 字典使用案例:作为计数器
- 已知“值”,如何查找“键”
-
元组
-
- 创建一个元组
- 访问元组中的元素
- 函数具有多个返回值时元组常常作为接受返回值的数据类型
- 用元组充当可变数量形参
- zip函数在元组中的应用
- 字典和元组
-
如何选择不同的数据结构
-
运算重载在列表、元组、字符串中的运用
-
用os模块操作文件
-
关于类,常见而又不熟悉的操作
-
- __str__方法进行调试
- 多态函数
-
前言
作为计算机科学家,最重要的技能就是问题求解 。所谓问题求解,是发现问题、创造性思维解决方案以及清晰准确地表达解决方案 的能力。而学习编程的过程,正是训练问题求解能力的绝佳机会。
重要的中英文互译
- 可移植性(portability):程序的一种属性:可以在多种类型的计算机上运行。
- 解释器(interpreter):一个读取其他程序并执行其内容的程序。
- 记号(token):即语言的基本元素,比如单词、数字等;3+¥=6中的¥在数学表达式中就不是合法的记号;
- 语法(syntax):用于控制程序结构的规则。
- 语法分析(parse):检查程序并分析其语法结构。
常见的语法错误示例:
SyntaxError:invalid token
意思是:
语法错误:无效的记号
- 交互模式(interactive mode):使用Python解释器的一种方式,在提示符之后键入代码。
- 脚本模式(script mode):使用Python解释器的另一种方式,从脚本中读入代码并运行它。
- 脚本(script):保存在文件中的程序。
- 异常(exception):程序运行中发现的错误。
- 模块(module):包含一组相关的函数的文件。
- 句点表示法(dot notation):调用模块中包含的函数的方法。
Python小白与大神的区别
条件表达式
if x > 0:
y = math.log(x)
else:
y = float('man')
用条件表达式改写后:
y = math.log(x) if x > 0 else float('man')
列表高端操作
def test(t):
res = []
for s in t:
res.append('a')
return res
用列表的高端写法后:
def test(t):
return ['a' for s in t] # 列表可以通过[]赋值直接修改
但下面的写法是错误 的:
def test(t):
res = []
return res.append('a') for s in t # 没有这种写法!!!
会直接报错
收集关键词参数
我们知道*操作符作为形参,可以使函数接受任意个数的实参数量,如下函数:
def printall(*args):
print(args)
但实际上,*操作符不会收集带有关键字(名称)的实参,比如:
printall(1, 2.0, '3') # 这种写法可以调用
printall(1, 2, third='3') # 这种写法会报错,不能接受带有关键字的实参
我们要记住,不带关键字的实参用*操作符收集为一个元组,带有关键字的实参用**操作符收集为一个字典,不能混收。
>>> def printall(*args, **kwargs):
>>> print(args, kwargs)
>>> printall(1, 2, third='3')
(1, 2) {'third': '3'}
第一章
何谓程序
程序是指一组定义如何进行“计算”的指令集合,这里的“计算”具有丰富的内涵 ,可能是解方程根的数学运算;也可能是符号运算,比如搜索和替换文档的文本;或者图形相关的操作,如图像处理。
树立“值才是程序最基本的单位,而不是变量”的概念
值(value)是程序操作的最基本的东西,比如一个字母、数字或字符串 。值有不同的类型。基本上,在任何可以使用值的地方,都可以使用任意表达式 。
所谓变量,是指向一个值的名称;变量所具有的类型,就是其指向的值的类型 。
从状态图 来看可能能更好地理解变量与值的关系:
下面的赋值语句的状态图如图所示:
message = 'And now for....' # 在此只为强调状态图,有些部分省略
n = 17
pi = 3.14....

从状态图 可以明显看出,是“变量 指向 值 ”而不是“值 指向 变量 ”。
形式语言和自然语言
自然语言指的就是人们说的语言,比如普通话。
形式语言是人们为了特殊用途创造的语言,比如编程语言。
第二章
变量及命名
关于变量的定义在上一章已然介绍过,注意其与值的关系。对于变量的命名,一般是以小写字母开头,在多个单词之间用“_”连接。
表达式和语句的定义
表达式 是值、变量和操作符的集合。
语句 则指的是一段会产生效果的代码单元。
字符串可进行的数学操作
仅可以进行两个,一个是“*”,一个是“+”。
第三章
模块及其使用方法
比如说math模块,其中包含了一些数学函数。如果想使用其中的函数,按照如下步骤操作:
1、将模块导入到运行环境中;
import math
2、上述语句将会创建一个名为math的模块对象(module object),该对象中包含了已定义的函数和变量,以一个句点(.)分隔,这个格式称为句点表示法(dot notation)。
height = math.sin(..)
函数的定义
以“def”关键词定义,如:
def print_ly(): #函数头
print(...) #函数体
需要注意的是,函数定义必须在使用它之前。
形参与实参
在函数定义时参数是形参,调用时的参数是实参。在下面这段代码的例子中,bruce是形参,m是实参:
def print_twice(bruce): # 定义
....
print_twice(m) # 调用
另外,函数名本身也可以作为实参输入到函数中进行调用 ,如下例:
def do_twice(f):
f()
f()
def print_spam():
print('spam')
do_twice(print_spam) #将函数名作为实参输入到函数中进行调用a
栈图
用于跟踪哪些变量在哪些地方使用 ,每个函数使用一个帧 表达,帧 实际上在栈图中就是一个带着函数名称的盒子,里面有函数的参数和变量,比如下图中的__main__、cat_twice等。
下面这个栈图的源程序的调用顺序是__main__调用cat_twice,cat_twice又调用print_twice,是由上而下调用的过程。

在实际编写代码的过程中,直接接触到栈图的可能性不多,但是如果函数调用过程中发现了错误,Python会打印出函数名、调用它的函数的名称以及调用这个调用者的函数名,依次类推一直到__main__,如下例:

上图是写程序过程中常见错误,实际上它也是一个**回溯(Traceback)**的过程。它的顺序和栈图的顺序一致,表明了依次调用的关系,当前执行的有错误的函数或代码在最底部 。
第四章
1、封装(encapsulation):指的就是把一段代码用函数包裹起来(有无形参均可) ,好处除了给这段代码一个名称以增加可读性之外,还有就是需要重复使用这段代码时,调用一次函数比辅助粘贴这段代码要快得多。
2、泛化(generalization):听起来很玄,实际上就是重新定义一个已有的函数,给它多加几个形参 。如下所示:
def square(t):
....
对它进行泛化(重新定义、增加参数) :
def square(t,length):
....
3、重构(refactoring):指的是重新组织程序,以改善接口,提高代码复用 ,一个好的函数,其形参应该是不多不少的,既能满足需求,也不需要人输入很多无必要的参数——如果能事先确定的经验参数或是按某种规则确定的参数,可体现在函数体而不是函数形参中。
4、实际上,写程序 的过程也被称为开发计划(development plan) ,这个思路应该是 :
【1】针对需求,直接写一些由上而下的程序,不需要考虑将其封装成函数;
【2】一旦程序成功运行,识别出其中完整的一部分,将它封装到一个函数中,并对函数进行合适的命名;
【3】泛化这个函数,添加合适的形参;
【4】对写好的程序重复以上三个步骤,直到得到一组可行的函数。
【5】完成封装和泛化后,对得到的一组函数思考是否有通过重构来改善程序的机会。例如,如果发现程序中有几处地方有相似的代码,可以考虑将它们抽取出来做一个合适的通用函数。
封装、泛化都比较好理解,下面给出一个对重构的理解例子 :
下面是两个函数,可以不考虑它们的作用,光从字面上来看,很明显第二个函数的第二部分与第一个函数很类似;
这就是重构发挥作用的时候了,可以提高代码复用率;
def polygon(t, length, n): # 画正n边形
for i in range(n):
t.fd(length)
t.lt(360/n)
def arc(t, r, angle):
arc_length = 2 * pi * r * angle / 360
n = int(arc_length / 3) + 1
step_length = arc_length / n # 计算每一步要走的长度
step_angle = angle / n # 计算每一步要走的角度
for i in range(n):
t.fd(step_length)
t.lt(step_angle)
重构后,把相似的部分独立出来单独作为一个基础函数 :
def polyline(t, length, n, angle): # 基础函数
for i in range(n):
t.fd(length)
t.lt(angle)
def polygon(t, length, n): # 画正n边形
polyline(t, length, n, 360/n)
def arc(t, r, angle):
arc_length = 2 * pi * r * angle / 360
n = int(arc_length / 3) + 1
step_length = arc_length / n # 计算每一步要走的长度
step_angle = angle / n # 计算每一步要走的角度
polyline(t, step_length, n, step_angle)
第五章
Python需要注意的运算符
1、乘方运算符:是“**”,而不是C语言等中的“^”。
2、向下取整除法操作符://,注意得到的数据类型不一定就是int形,也可能是25.0这样的float型。
3、求模操作符:%,即求余数。
条件链(chained conditional)
即有多种可能,需要多种判断条件 时,Python对于条件语法的结构如下:
其语法结构如下:
if ...:
...
elif ...:
...
elif ...:
...
else: # 可有可无
...
更便利的是,Python还支持C语法不支持的同时条件语法,比如:
如果变量x在3~10之间,就输出x,那么在C语言的写法一般是用and连接:
if (0<x and x<10)
print(x)
而在Python中,则允许直接用连续小于号:
if 0 < x < 10:
print(x)
递归(Recursion)
所谓的递归函数(Recursive Function),指的是自己调用自己的函数。虽然从直观上看递归很奇怪,但实际上它是程序所能做的最神奇的事情之一。关于递归的好处,在后面再说。
下面给出一个递归的例子:功能需求是打印输入的字符串n次 。
# 函数功能:打印输入的s字符串n次
def print_n(s, n):
if n<= 0:
return
print(s)
print_n(s, n-1)
print_n('abc', 3)
当然,对于这样简单的例子,使用for循环会更容易。但后面会发现有一些情况下,使用for循环很难写,但使用递归会更简单 。
无限递归
当递归函数没有设计退出机制时,就会陷入无限递归的情况。当然,在大多数程序环境中,无限递归的函数并不会真的永远执行,以Python举例,它会在递归深度达到上限时报告一个出错消息:
RuntimeError: Maximum recursion depth exceeded
键盘输入
Python提供了一个内置函数input来从键盘获取输入 ,并等待用户输入一些东西 。用户的输入以回车键代表结束 ,程序会恢复运行,input以字符串的形式 返回用户输入的内容。
text = input()
print(text)
若输入what are you waiting for?,并按下回车后,则会在终端输出相同的字符串。

当然,我们通常希望从用户那里获得输入之前,能给用户一个提示信息,以告诉用户他们希望输入什么,在此,input函数可以接受一个字符串参数作为提示:
text = input('what...is your name?\n')
print(text)
其结果如下,会先显示一行输入提示信息:

需要注意的是,一般提示信息最后会带上一个’\n’,这是为了让用户的输入显示在提示信息的下一行 。
第六章
isinstance()检查输入的类型
很多时候我们会要求函数的形参输入等必须为整数,这时候isinstance()就为我们提供了方便,其用法示例如下:
if isinstance(n, int): #如果n是整数,则执行后续语句
....
调试的思想
如果一个函数无法正常工作,那么我们在调试的时候应该考虑三种可能性:
(1)函数获得的实参有问题——打印输入看看;
(2)函数本身有问题——输出中间变量查看;
(3)函数的返回值有问题——输出中间变量查看;
第八章
搜索
搜索实际上就是遍历一个序列,当找到目标时返回。
in在字符串中的妙用
in实际上是一个布尔操作符,它可以操作于两个字符串上,如果第一个字符串被第二个完全包含,则返回True,否则返回False:
'a' in 'banana'
>>>True
第十章:列表
sum在列表中的妙用
在Python中,对列表元素累加是如此常见的操作,以至于Python提供了一个内置函数sum():
t = [1,2,3]
sum(t)
>>>6
从列表中删除元素的方法
有三种方法:(1)如果知道要删除元素的下标,并且需要返回删除元素的值,可以使用pop()方法;(2)如果知道要删除元素的值,可以使用remove()方法;(3)如果知道要删除元素的下标,且不需要返回删除元素的值,可以用del方法,这也是比较常用的。
>>>t = ['a', 'b', 'c']
>>>x = t.pop(1)
>>>t
['b', 'c']
>>>x
'a'
>>>t = ['a', 'b', 'c']
>>>t.remove('a')
>>>t
['b', 'c']
>>>t = ['a', 'b', 'c']
>>>del t[0:1]
>>>t
['c']
列表和字符串
二者并不等同 。常用的转换函数是list(),将一个字符串转换成一个列表:
>>>s = 'spam'
>>>t = list(s)
>>>t
['s','p','a','m']
第十二章:字典
字典是Python最好的语言特性之一,它是很多算法的基本构建块。
字典类似于列表,但它更加通用。列表的下标为固定的整数,而字典的下标和值一样,几乎可以是任何类型,包括列表 。
字典的下标被称之为“键”,每个键都会和一个值相关,和列表一样。更专业地说,字典体现了键到值的映射 。
创建一个字典
两种方法:
第一种方法:通过dict()内置函数
eng = dict() # 创建一个空字典
eng['one'] = 'uno' # 字典属于可变对象,可通过这种方式直接添加新项
第二种方法:直接用{}创建:
eng = {'one':'uno', 'two':'dos'}
需要注意的是,字典不同于列表,它是无序的,即如果你打印出一个字典,键值对的排列顺序很可能不会像你创建时一样 。
访问字典中的元素
上面已经提到,字典是无序的,所以它不能通过整数下标来访问。
相应的,字典通过键来查找对应的值:
>>>eng = {'one':'uno', 'two':'dos'}
>>>eng['two']
'dos'
len和in在字典中的应用
len在字典中返回的是键值对的数量:
>>>eng = {'one':'uno', 'two':'dos'}
>>>len(eng)
2
如何查看一个变量是否是字典的键或值
用in操作符判断一个变量是否是字典的键 ,它告诉你当前变量是否是字典中的键 (注意这里是是否和键相同,而不是是否和值相同!)
>>>eng = {'one':'uno', 'two':'dos'}
>>>'one' in eng
True
通过values方法和in方法结合来判断变量是否是字典的值:values方法会返回字典的值集合:
>>>vals = eng.values() # 会返回一个值集合
>>>'uno' in vals
True
循环在字典中的应用
在for循环中使用字典,则会遍历字典的键,需要注意的是,键的出现同样没有特定的顺序:
>>> eng = {'o':'a', 'c':'b','n':'c'}
>>> for i in eng:
>>> print(i,eng[i])
n c # 可以发现,并不是按定义的键的顺序输出的,而是按随机顺序遍历所有键
o a
c b
字典使用案例:作为计数器
给定一个字符串,计算每个字母出现的次数:
def count(str):
d = {}
for i in str: # 对字符串进行遍历,统计其每个元素
if i in d: # 判断该字母是否在字典中,若在则其值加1
d[i] = d[i] + 1
elif i not in d: # 若不在则其值初始化为1
d[i] = 1
return d
a = count('aabc')
print(a)
输出结果为:
{'a': 2, 'c': 1, 'b': 1}
已知“值”,如何查找“键”
已知键的情况下,找到其对应的值是非常简单的。那么反过来呢,如果有值v,而想找到键k时怎么办?
这里一般会有两个问题:首先,可能存在多个键映射到同一个v值上,此时需要你自己决定是随机挑一个还是建立一个列表保存所有键;其次,并没有这种反向查找的简单语法,需要使用搜索实现。
def search(d,v): # 反向查找
key = []
for k in d: # 遍历字典
if d[k]==v: # 如果值对应上
key.append(k)
if key is None: # 如果没有任何值对应上,返回一个异常
raise LookupError # 抛出一个异常
else:
return key
d = {'a':1,'b':2,'c':3,'d':3}
v = 3
key = search(d,v)
print(key)
*********输出结果**********
['d', 'c']
元组
元组和列表的区别仅在于它是不可变 的,元组按照整数下标索引,其值也可以是任意类型。
创建一个元组
和列表、字典一样,用()即可创建:
>>> t1 = ('lupins', 'a', 'b')
>>> print(t1)
('lupins', 'a', 'b')
但需要注意的是,若要新建一个只包含一个元素的元组 ,需要在后面添加一个逗号 ;而只用括号括起来的单独的值并不是元组 。
>>> t2 = ('lupins',) # 有逗号,返回的是一个元组
('lupins',) # 元组
>>> t3 = ('lupins') # 无逗号,返回的是一个字符串,而不是元组
lupins # 字符串
所以,这也是为什么常常看到(value,)这种只用一个元素元组会有一个额外的逗号的原因了。
访问元组中的元素
大多数列表操作都适用于元组:
>>> t = ('a','b','c') # 返回的是一个元组
>>> print(t)
('a', 'b', 'c')
>>> print(t[0]) # 访问单个元素
a
>>> print(t[1:3]) # 切片访问
('b', 'c')
>>> for i in range(len(t)): # for循环遍历
print(i, t[i])
0 a
1 b
2 c
>>> t[0] = 'd' # 元组是不可变的,不允许单个值的修改
TypeError: 'tuple' object does not support item assignment
函数具有多个返回值时元组常常作为接受返回值的数据类型
>>> def test(a, b):
>>> return a,b
>>> c = test(1, 2) # 当用一个变量接受多个返回值时,这个变量会被赋予元组的数据类型
>>> print(type(c))
<class 'tuple'>
>>> e, f = test(1, 2)
<class 'int'> <class 'int'>
用元组充当可变数量形参
函数可以接受不定个数的参数 ,以*开头的参数名会收集所有的参数到一个元组上,例如,下面的printall函数会接受任意个数的参数并打印它们:
>>> def printall(*args):
>>> print(args)
>>> print(type(args)) # 把所有形参都收集到args元组上;
>>> printall(1,'a','b')
(1, 'a', 'b')
<class 'tuple'>
很多内置函数都使用可变长参数元组,例如max和min都可以接收任意个数的参数:
>>> max(1,2,3,4)
4
zip函数在元组中的应用
zip是一个常用的内置函数,它的名字取自于拉链(zipper)。和它的名字一样,它的意思就是将两行链牙交替连接 起来。具体来说,它的作用是接收两个或多个序列,并返回一个由各个序列的元素交替组成 的元组迭代器 ,代码示例如下,:
>>> s = 'abc'
>>> t = [0,1,2]
>>> zip(s,t)
>>> print(zip)
<zip object at 0x000001C6FCC7CA08> # 返回zip对象,它的本质是一种迭代器
使用zip对象最常用的方式是在for循环中,因为它是一个迭代器,着重理解“交替连接 ”,:
>>> s = 'abc'
>>> t = [0,1,2]
>>> a = zip(s,t)
>>> for pair in a:
>>> print(pair)
('a', 0) # s的第一个元素和t的第一个元素构成第一个元组
('b', 1) # s的第二个元素和t的第二个元素构成第二个元组
('c', 2) #...
如果需要使用列表的操作符和方法,可以将zip对象制作成一个列表,这样也可以很清晰地看懂zip是如何进行交替连接成元组的 :
>>> s = 'abc'
>>> t = [0,1,2]
>>> a = list(zip(s,t))
>>> print(a)
[('a', 0), ('b', 1), ('c', 2)]
那么,如果两个序列长度不一致怎么办?按最短的那个来 :
>>> s = 'ab'
>>> t = [0,1,2]
>>> a = list(zip(s,t))
>>> print(a)
[('a', 0), ('b', 1)]
可以用for循环访问这种元组组成的列表 :
>>> t = [('a', 0), ('b',1),('c',2)]
>>> for a, b in t:
>>> print(a, b)
a 0
b 1
c 2
字典和元组
字典有一个item方法可以返回一个元组组成的迭代器,其中每个元组就是一个键值对:
>>> d = {'a':0,'b':1,'c':2}
>>> t = d.items()
>>> print(t)
dict_items([('b', 1), ('c', 2), ('a', 0)])
>>> for key, value in d.items(): # 由于字典是无序的,所以输出也是无序的
>>> print(key, value)
b 1
c 2
a 0
同样地,可以使用一个元组列表来初始化一个新的字典:
>>> t = [('a',0),('c',2),('b',1)]
>>> d = dict(t)
>>> print(d)
{'b': 1, 'a': 0, 'c': 2}
由此,组合dict和zip就可以得到一个简洁的创建字典的方法:
>>> d = dict(zip('abc', range(3)))
>>> print(d)
{'c': 2, 'b': 1, 'a': 0}
如何选择不同的数据结构
字符串相比其他序列有更明显的限制,因为它是不可变的,且元素必须是字符。如果你需要修改一个字符串中的字符,可能需要使用字符的列表。
列表比元组更加通用,因为它是可变的。但是在某些环境中可能需要用到元组,比如返回值、可变形参数等。
运算重载在列表、元组、字符串中的运用
包括+,*,>或<;
>>> (0,1,2)<(0,3,4) # 按第一个不同元素的大小返回值
True
用os模块操作文件
关于类,常见而又不熟悉的操作
__str__方法进行调试
当打印对象时,会输出__str__的返回值。
>>> class Test():
>>> def __str__(self):
>>> return 'abc'
>>> test = Test()
>>> print(test) # 打印对象
abc
多态函数
能处理多个输入数据结构的函数称之为多态函数,比如下面的记录字母出现次数的函数能同时处理列表、元组、字典,那它就是多态的:
def histogram(s):
d = dict()
for c in s:
if c not in d:
d[c] = 1
else:
d[c] = d[c] + 1
return d
