《Python 基础教程》学习笔记,要点整理。
有些地方翻译有严重的错误,譬如异常一章最后应该是表达要去习惯使用 try/except,而不是“if/else”。没有怎么讲集合 set 的相关应用,其他的部分还不错(前 10 章还过得去)。类讲得很一般。另外还有些问题见: https://book.douban.com/review/5604726/
不过介绍了一些 Pythonic 的写法,对阅读他人的代码和写好 Python 有一些帮助。关于字符串和异常的一些细节讲得不错。
基础知识
3.0 之前的 Python 的除法,/
结果与精度较高的操作数相同:
|
|
或者:
|
|
round()
四舍五入为最接近的整数。
floor()
向下取整的函数,ceil()
向上取整:
|
|
Python2 对于虚数的支持:
|
|
即使不导入 cmath
模块,Python2 也是支持虚数运算的:
|
|
保存并执行程序
用命令行执行 Python 脚本
|
|
如果你有多个版本的 Python,最好强调一下 Python 的版本究竟是 python2
还是 python3
让脚本像普通程序一样运行
如果希望运行普通程序一样运行 Python 脚本,在类 Unix 系统下,在代码的首行加入:
|
在实际运行脚本之前,必须让脚本文件拥有可执行的属性,在命令行中首先:
|
|
然后就可以像执行普通程序一样执行它了:
|
|
如果执行不了的话,就试试:
|
|
甚至可以重命名程序,去掉 .py
,执行:
|
|
字符串
repr
会创建一个字符串,它以合法的 Python 表达式的形式来表示值。repr(x)
的功能也可以用一对反引号实现:
|
|
具体实例如下:
|
|
不过 Python3.0 之后就不再用反引号了,因此,即使在旧代码中看到了反引号,也应该坚持用 repr()
。
input()
与 raw_input()
input()
会假设用户输入的是合法的 Python 表达式:
|
|
要求用户带着引号输入他们的名字有点过分,因此,这就需要使用 raw_input()
,它会把所有的输入当作原始数据,然后将其放入字符串中:
|
|
在 Python 3.0 之后:
|
|
数据结构
通用序列操作
分片
步长不可以是 0
,但步长可以是负数,即从右往左提取元素:
|
|
对于一个正数步长,Python 会从序列的头部开始向右提取元素(无论始末的索引是正数还是负数),直到最后一个元素(不包括);而对于负数步长,则是从序列的尾部开始向左提取元素,直到第一个元素(不包括)。
相加
相乘
in
判断
|
|
前两个例子测试了 ‘w’ 和 ‘x’ 是否在字符串 permissions
中。在类 UNIX 系统中,这两行代码可以作为查看文件可写和可执行权限的脚本。最后一个例子可以作为垃圾邮件过滤器的一部分。
长度,最大值,最小值
len()
, max()
, min()
list
pop()
是唯一一个既能修改列表又能返回除了 None
之外的值的方法。
字符串
字符串格式化
如果右操作数是远足,其中每一个元素都会被单独格式化,每个值都需要一个对应的转换说明符。
|
|
常用字符串方法
find()
方法
find()
可以在一个较长的字符串中查找子串,返回子串所在位置的最左端索引。如果没有找到则返回 -1
。
|
|
注意:find()
并不返回 boolean
值,如果返回 0
,即在字符串开始处找到子串。
join()
方法
需要添加的队列元素都必须是字符串:
|
|
split()
方法
join()
的逆方法,将字符串分割成序列。不提供分隔符时,将所有空格作为分隔符。
|
|
###replace()
方法
返回某字符串的所有匹配项均被替换之后得到字符串。
|
|
strip()
方法
返回去除两侧(不包括内部)空格的字符串,它和 lower()
一起使用的话就可以很方便地对比输入的和存储的值。
|
|
也可以去掉指定的字符,列为参数即可:
|
|
字典
使用
字典的格式化字符串
|
|
执行结果:
|
|
clear()
方法
清空字典
|
|
copy()
方法
浅拷贝,深拷贝要 from copy import deepcopy
fromkeys()
方法
使用给定的键建立新字典,每个键默认的对应值是 None
,或者自己提供默认值:
|
|
get()
和 setdefault()
当使用 get()
访问一个不存在的键时,没有任何异常,而得到了 None
值。还可以自定义 “默认”值,替换 None
。如果键存在,就像普通的字典查询一样:
|
|
setdefault()
类似于 get()
,如果字典不含给定键,可以设定相应的键值。
|
|
items()
和 iteritems()
后者往往更加高效:
|
|
与之对应的还有 keys()
和 iterkeys()
,values()
和 itervalues()
。
pop()
和 popitem()
|
|
popitem()
会随机弹出一个元素。
update()
方法
提供的字典项中的项会被添加到原来的字典中,如果有相同的键则会覆盖。
|
|
条件、循环和其他语句
导入
|
|
只有确定自己想要从给定的模块中导入所有功能时,才应该使用最后一个版本。但是如果两个模块都有 open()
,只需用第一种方法导入,然后:
|
|
但是还可以在语句末尾增加 as
子句,在该子句后给出名字,或为整个模块提供别名:
|
|
也可以为函数提供别名:
|
|
赋值魔法
序列解包
|
|
事实上,这里所做的事情叫做 序列解包 (sequence unpacking)——将多个值的序列解开,然后放到变量的序列中。更形象一点地表示出来就是:
|
|
当函数或者方法返回元组(或者其他序列或可迭代对象)时,这个特性尤其有用:
|
|
它允许函数返回一个以上的值并且打包成元组,然后通过一个赋值语句很容易进行访问。所解包的序列中的元素数量必须和放置在赋值符号 =
左边的变量数量完全一致,否则 Python 会在赋值时引发异常:
|
|
注意:Python3 有另外一个解包的特性,可以像函数的参数列表一样使用星号运算符。
|
|
条件和条件语句
下面这些值在作为布尔表达式时,会被解释器看作 False
:
|
|
尽管上述都被认为是假值,也就是说 bool([]) == bool("")
,
避免将 is
运算符用于比较类似数值和字符串这类不可变值,由于 Python 内部操作这些对象的方式的原因,使用 is
运算符的结果是不可预测的。
一些迭代工具
并行迭代
内建的 zip()
可以用来进行并行迭代,可以把两个序列“压缩”在一起,然后返回一个元组的列表:
|
|
zip()
可以作用于任意多的序列,并且可以应付不等长的序列——当最短的序列“用完”的时候就会停止。
编号迭代
enumerate()
可以在提供索引的地方迭代索引-值对,比如下面这个简化的屏蔽关键字的程序片段:
|
|
翻转和排序迭代
reversed()
和 sorted()
同列表的 reverse()
和 sort()
方法类似,但作用于任何序列或可迭代对象上,不是原地修改对象,而是返回翻转或排序后的版本。sorted()
返回列表,reversed()
返回一个可迭代对象。
使用 break
|
|
循环中的 else
子句
|
|
列表推导式
|
|
得到那些名字中的首字母相同的男孩和女孩。
由于它会检查每个可能的配对,这个方法效率不高,有一个更高效的方法。
|
|
其他
使用 del
语句不仅会移除一个对象的引用,也会移除那个名字本身。在 Python 中是没有办法删除值的,Python 解释器会用垃圾回收收回内存。
执行和求值字符串
执行一个字符串的语句是 exec
(在 Python3.0 之后,exec()
是一个函数而不是语句):
|
|
为了安全起见,可以增加一个字典,起到命名空间的作用。可以通过增加 in scope
来实现:
|
|
这样,潜在的破坏性代码并不会覆盖 sqrt()
,原来的函数依然可以正常工作,而通过 exec
赋值的变量 sqrt
只在它的作用域内有效。
eval()
会计算 Python 表达式(以字符串形式书写),并且返回结果值:
|
|
抽象
内建的 callable()
可以用来判断函数是否可调用:
|
|
在 Python3.0 之后,
callable()
不再可用,需要用表达式hasattr(func, __call__)
作用域
如果的确需要的话,可以用 globals()['globalParamName']
来获取全局变量值,该函数的近亲是 vars()
,可以返回全局变量的字典(locals()
返回局部变量的字典)。
|
|
global x
告诉 Python x
是一个全局变量。
函数式编程
|
|
面向对象
面向对象最重要的优点:
- 继承 Inheritance:以普通的类为基础建立专门的类对象。
- 封装 Encapsulation:对外部世界隐藏对象的工作细节。
- 多态 Polimorphism:可以对不同类的对象使用同样的操作。
前面带有下划线的名字都不会被带星号的 import
语句 from module import *
导入。
子类可以将超类更加具体化,将其他类名写在 class
语句后的圆括号内可以指定超类:
|
|
查看一个类是否是另一个的子类,可以使用内建的 issubclass()
|
|
用特殊属性 __bases__
来查看已知类的基类们。
如果使用 __metaclass__ = type
或从 object
继承的方式来定义新式类,那么可以使用 type(s)
查看实例的类。
多重继承
谨慎使用!
如果一个方法从多个超类继承(也就是说有两个具有相同名字的不同方法),那么必须要注意超类的顺序:先继承的类中的方法会重写后继承的类中的方法。
异常
Python 用 异常对象 来表示异常情况。遇到错误后,会引发异常。如果异常对象并未被处理或捕捉,程序就会用所谓的 回溯(Traceback,一种错误信息)终止执行。
引发异常
为了引发异常,可以使用一个类(应该是 Exception
的子类)或者实例参数调用 raise
语句。使用类时,程序会自动创建实例。
|
|
捕捉异常并处理
为了捕捉异常并且做出一些错误处理(本例中只是输出一些更友好的错误信息),可以:
|
|
看起来用 if
语句检查 y
值更简单一些,本例中这样做的确很好,但是如果需要给程序加入更多的除法,那么就得给每个除法加个 if
语句,而使用 try/except
的话只需要一个错误处理器。
传递异常
用不带参数的 raise
来传递异常。在函数内引发异常,它就会被传递到函数被调用的地方。
考虑一下一个能处理 ZeroDivisionError
的计算器类。如果这个行为被激活,那么计算器就会打印错误信息,而不是让异常传播。如果在与用户进行交互的过程中使用,那么这就十分有必要。但是如果是在程序内部使用,引发异常会更利于程序做出相应处理——比如将错误抛出,让调用该段代码的地方处理该错误:
|
|
多个 except
子句
异常处理不会搞乱原来的代码,而增加一大堆 if
语句检查可能的错误情况会让代码相当难读。
|
|
用一个块捕捉多个异常
用元组列出,注意不要忘记了元组的圆括号:
|
|
捕捉对象
如果希望在 except
子句中访问异常对象本身,可以使用两个参数(注意,就算要捕捉到多个异常,也只需向 except
子句提供一个参数——一个元组)。比如,如果想让程序继续运行,但是又因为某种原因想记录下错误(比如只是打印给用户看)。下面的示例程序会打印异常(如果发生的话),但是程序会继续运行:
|
|
运行结果如下:
|
|
注意
在 Python3.0 之后,except
子句写作except (ZeroDivisionError, TypeError) as e:
。
全捕获
总有些异常无法被预料到,如果要捕获所有异常,可以在 except
子句中忽略所有的异常类
|
|
警告:
像这样捕捉所有异常是非常危险的,因为它会隐藏所有程序员未想到并且未做好准备处理的错误,它同样会捕捉用户终止执行的Ctrl+C
企图,以及用sys.exit
函数终止程序的企图,等等。这时用except Exception, e
会更好些,或者对异常对象e
进行一些检查。
万事大吉
可以给 try/except
语句加上 else
子句。如果没有引发异常,else
语句就会被执行。
|
|
运行后会得到:
|
|
看下面这个示例:
|
|
只要有错误发生,程序就会不断要求重新输入。
最后
无论是否发生异常,finally:
中的子句都会执行。
异常之禅
如果知道某段代码可能会导致某种异常,而又不希望程序以堆栈跟踪的形式终止,那么就根据需要添加 try/except
或者 try/finally
语句(或者它们的组合)进行处理。
有些时候,条件语句可以实现和异常处理同样的功能,但是条件语句可能在自然性和可读性上差血。而从另一方面来看,某些程序中使用 if/else
实现会比使用 try/except
要好。
假设有一个字典,我们希望打印出存储在特定的键下面的值。如果该键不存在,那么什么也不做。代码可能像下面这样写:
|
|
如果给程序提供包含名字 Throatwobbler Mangrove 和年龄 42(没有职业)的字典的函数,会得到如下输出:
|
|
如果添加了职业 camper,会得到如下输出:
|
|
代码非常直观,但是不够简洁。程序会两次查找 occupation
键——其中一次用来查看键是否存在(在条件语句中),另一次获得值。
另一个解决方案如下:
|
|
注意:
这里打印职业时用的+
而不是,
,否则字符串'Occupation:
在异常引发之前就会被输出。
try/except
语句比 if/else
语句更加 Pythonic。
请求宽恕易于请求许可。
在做一件事时去处理可能发生的错误,而不是在开始做事之前做大量的检查。
魔法方法
重写构造方法
推荐使用第二种——Python3.0 之后普遍使用的方法。
调用未绑定的超类构造方法
使用 ParentClass.__init__(self)
。
|
|
运行结果:
|
|
使用 super()
使用 super(CurrentClass, self)
。该函数返回对象的任何方法都是父类的方法。
|
|
Python3.0 之后,
super()
可以不用任何参数。
super()
返回一个 super
对象,这个对象负责进行方法解析,当对其特性进行访问时,它会查找所有超类(以及超类的超类),直到找到所需的特性为止(或者引发一个 AttributeError
异常)。
成员访问
基本的序列和映射规则
The word protocol is often used in Python to describe the rules governing some form of behavior. This is somewhat similar to the notion of interfaces. The protocol says something about which methods you should implement and what those methods should do. Because polymorphism in Python is based on only the object’s behavior (and not on its ancestry, for example, its class or superclass, and so forth), this is an important concept: where other languages might require an object to a certain class or to implement a certain interface, Python often simply requires it to follow some given protocol. So, to be a sequence, all you have to do is follow the sequence protocol.
protocol
说明了应该实现何种方法和这些方法应该做什么。比如只需要遵守序列给定的 protocol
,就可以成为一个序列。
序列和映射是对象的集合。为了实现它们基本的规则,如果对象是不可变的,那么就需要使用两个魔法方法,如果是可变的则需要使用 4 个。
__len__(self)
:返回集合中所含项目的数量。如果__len__
返回 0(并且没有实现重写该行为的__nonzero__
)对象会被当作一个布尔变量中的假值(空的列表,元组,字符串和字典等)进行处理。__getitem__(self, key)
:这个方法返回与所给键对应的值。对于序列,键是0
到n-1
的整数(或相应的负数,x[-n]
和x[len(x)-n]
一样),对于映射来说,键更加自由。__setitem__(self, key, value)
__delitem__(self, key)
:在对一部分对象使用del
语句时被调用,同时必须删除和元素相关的键。这个方法也是为可修改的对象定义的。
附加要求:
- 如果键是不合适的类型(例如,对序列使用字符串作为键),会引发一个
TypeError
异常。 - 如果序列的索引是正确的类型,但超出了范围,应该引发一个
IndexError
。
实现一个理论上无限大的算术序列:
|
|
没有实现 __del__()
的原因是希望删除元素是非法的,执行结果:
|
|
一般避免使用 isinstance()
函数(因为类或类型检查与 Python 中多态的目标背道而驰)。因为 Python 的 language reference 上明确指出索引必须是整数(包括长整数),所以上面的代码才会如此使用。遵守标准是使用类型检查的(很少的)正当理由之一。
属性
property()
函数
|
|
执行结果
|
|
静态方法和类成员方法
静态方法的定义没有 self
参数,且能够被类本身直接调用。类方法在定义hi需要名为 cls
的类似于 self
的参数,可以直接被类调用,cls
参数是自动被绑定到类的。
|
|
手动包装和替换方法的技术看起来有点单调,在 Python2.4 之后,引入了叫作 装饰器 的新语法。使用艾特操作符 @,在方法(或函数)的的上方将装饰起列出,从而指定一个或者更多的装饰器(多个装饰器在应用时的顺序与指定顺序相反)。
|
|
执行结果:
|
|
__getattr__()
, __setattr__()
等
为了在访问特性的时候可以执行代码,必须使用一些魔法方法。
__getattribute__(self, name)
:当特性name
被访问时自动被调用(只能在新式类中使用)。__getattr__(self, name)
:当特性name
被访问且对象没有相应的特性时被自动调用。__setattrj__(self, name, value)
:当试图给特性name
赋值时会被自动调用。__delattr__(self, name)
: 当试图删除特性name
时被自动调用。
尽管和使用 property
相比有点复杂(而且在某些方面效率更低),但这特殊方法是很强大的,因为可以对处理很多属性的方法进行再编码。
|
|
注意:
__setattr__()
方法在所涉及到的属性不是size
时也会被调用。因此,这个方法必须考虑新增属性的情况。为了避免__setattr__()
方法被再次调用(从而使程序陷入死循环),__dict__()
被用来代替普通的属性赋值操作。__getattr__()
只在普通的属性没有被找到的时候调用,这就是说如果给定的名字不是size
,这个特性不存在,这个方法会引发一个AttributeError
异常。
就像死循环陷阱和
__setattr__()
有关系,还有一个陷阱和__getattribute__()
有关系。因为__getattribute__()
拦截所有特性的访问,也拦截对__dict__
的访问!访问__getattribute__()
中与self
相关的特性时,使用超类的__getattribute__()
方法(使用super()
)是唯一安全的途径。
迭代器
迭代器规则
迭代——重复做一些事很多次。到现在为止只是在 for
循环中对序列和字典进行迭代,但实际上也能对其他的对象进行迭代:实现 __iter__()
方法的对象。
__iter__()
返回一个迭代器 iterator,所谓的迭代器就是具有 next()
的对象。在调用 next()
时,迭代器会返回它的下一个值。如果 next()
方法被调用,但迭代器没有值可以返回,就会引发 StopIteration
异常。
在 Python3.0 之后,迭代器对象应该实现
__next__()
,而不是next()
。
一个实现了 __iter__()
方法的对象是可迭代的,一个实现了 next()
方法的对象则是迭代器。
迭代可以不必一次性将所有值算出并存储,而是可以一个接一个地计算值:
|
|
内建函数 iter()
可以从可迭代对象中获得迭代器:
|
|
当然也可以用 list()
构造方法显示地将迭代器转换为列表。另外,在 Python3.0 之后,应该用 print(next(it))
。
生成器
用圆括号返回生成器
|
|
用 yield
|
|
任意包含 yield
语句的函数成为生成器。它不是像 return
那样返回值,而是每次产生多个值。
递归生成器
上一节创建的生成器只能处理两层嵌套,为了处理嵌套使用了两个 for
循环。如果要处理任意层的嵌套该怎么办?例如,可能要使用其来表示树形结构。每层嵌套需要增加一个 for
循环,但因为不知道有几层嵌套,所以必须把解决方案变得更灵活——求助于 递归:
|
|
There are two reasons why you shouldn’t iterate over string-like objects in the flatten function. First, you want to treat string-like objects as atomic values, not as sequences that should be flattened. Second, iterating over them would actually lead to infinite recursion because the first element of a string is another string of length one, and the first element of that string is the string itself!
为了处理上述问题,则必须在生成器开始处添加一个检查语句。试着将传入的对象和一个字符串拼接,看看会不会出现 TypeError
,这是检查一个对象是不是 string-like 的最简单、最快速的方法。
|
|
如果表达式 nested + ''
引发了一个 TypeError
,它就会被忽略。然而如果没有引发 TypeError
,那么内层 try
语句中的 else
子句会引发一个它自己的 TypeError
异常。继而完整地返回这个字符串,而不是遍历该字符串。
八皇后问题
有一个棋盘和 8 个要放到上面的皇后,唯一的要求皇后之间不能形成威胁,也就是说任意两个皇后不能在同一行,不能在同一列,也不能在同一条斜线上。
这是一个经典的回溯问题,首先尝试放置第一个皇后(在第一行),然后放置第二个,依此类推。如果发现不能放置下一个皇后,就回溯到上一步,试着将皇后放到其他的位置。最后,或者尝试完所有的可能或者找到解决方案。
状态表示
用一个元组来表示皇后的位置,比如 state[0] == 3
,那么就表示在第 1 行的皇后是在第 4 列。
寻找冲突
已知的皇后的位置被传递给 conflict()
(以状态元组的形式),然后由函数判断下一个皇后的位置会不会有新的冲突。
|
|
abs(state[i]-nextX) in (0, nextY-i)
表示如果下一个皇后和正在考虑的前一个皇后的水平距离为 0(列相同)或者等于垂直距离(在一条对角线上)就返回 True
,否则就返回 False
。
Base Case
最后一个皇后,根据其他皇后的位置生成它自己能占据的所有位置(可能没有)。
|
|
需要递归的情况
在完成基本情况时,递归函数会假定(通过归纳)所有的来自之前位置的结果都是可行的,因此这里要做的就是为前面的 queens()
中 if len(state) == num - 1
增加 else
子句。
那么递归调用返回什么结果呢?
想得到的是已确定的所有的皇后的位置,假定将位置信息作为一个元组返回。在这种情况下,需要修改基本情况也返回一个长度为 1 的元组。
这样,程序从递归调用的返回值得到已确定的所有皇后的位置(用元组所表示),从而为后面的皇后提供相应的合法位置信息,并且将当前合法的位置信息添加到元组中传递给后面的皇后。
|
|
for pos
和 if not conflict
部分和前面的代码相同,因此可以稍微简化一下代码。添加一些默认的参数:
|
|
注意 queens(num, state + (pos, ))
和 yield (pos, ) + result
中的 pos
在加法中在前还是在后。
queens(num, state + (pos, ))
中 state
是已确定的状态,pos
是新加入的位置信息,(pos, )
在 state
后面。
yield (pos, ) + result
中,由于递归调用,最先返回的 result
是最后一行的皇后位置,向前递推,故 pos
在 result
前面。
当然这个方法的效率不高,不过是一个很好的递归回溯,深度优先的范例,完整代码如下:
|
|
下面是 网友做的一行解决八皇后问题
|
|
Batteries Included
这里中文版翻译有问题:标题 Batteries Included 不是什么“充电时刻”,是:
The Python source distribution has long maintained the philosophy of “batteries included” – having a rich and versatile standard library which is immediately available, without making the user download separate packages. This gives the Python language a head start in many projects.
摘自 PEP 206 – Python Advanced Library
模块中有什么
dir()
函数
返回一个模块所有变量、函数、类等等:
|
|
__all__
是什么
是对外的接口,如果导入 from copy import *
实际上就是导入的:
|
|
如果想要导入如 PyStringMap
,需要用 from copy import PyStringMap
专门导入才行。
测试
测试驱动开发
覆盖度工具,比如 trace.py 程序,可以使用 import trace
导入,使用 help(trace)
查看帮助。
过程如下:
- 指出需要的新特性。记录下来,然后为其编写一个测试。
- 编写特性的概要代码,这样程序就可以运行而没有任何语法等方面的错误,但是测试会失败。看到测试失败是很重要的,这样就能确定测试 可以失败(因为如果测试代码本身有问题,可能导致无论什么情况下,测试都会成功)。
- 为特性的概要编写
dummy code
,能满足测试要求就行。不用准确地实现功能,只要保证测试可以通过即可。这样就可以保证开发时总是能通过测试了。 - 完成代码。删除
dummy code
。
单元测试工具
doctest
:用来检查文档unittest
:通用测试框架
黄金法则 “使其工作、使其更好、使其更快”——单元测试让程序可以工作,源代码检查让程序更好,最后,性能分析会让程序更快。
源代码检查工具
- PyChecker
- PyLint
分析
标准库中包含了一个叫 profile
的分析模块,还有一个更快的嵌入式 C 语言版本,叫作 hotshot
(Python3 中相应的替代模块叫作 cProfile
)。使用分析程序非常简单:
|
|