《Python学习手册》读书笔记【转载】
转载:http://www.cnblogs.com/wuyuegb2312/archive/2013/02/26/2910908.html
之前为了编写一个svm分词的程序而简单学了下Python,觉得Python很好用,想深入并系统学习一下,了解一些机制,因此开始阅读《Python学习手册(第三版)》。如果只是想快速入门,我在这里推荐了几篇文章,有其他语言编程经验的人简单看一看就可以很快地开始编写Python程序了。
黑体表示章节, 下划线表示可以直接在原文对应位置查到的专有技术名词。
原书配套答案请到http://www.hzbook.com/Books/4572.html下载,简单注册即可。
第三章 如何运行程序
import进行模块导入只能运行一次,多次运行需使用reload。
模块往往是变量名的封装,被认为是命名空间。例如:
#myfile.py title = "test" >>>import myfile >>>print myfile.title test
替代方案是from,下面有同样的效果:
>>>from myfile import title >>>print tittle test
from myfile import * 则可以把myfile所有变量全部导入(第19章内容)。
第四章 介绍Python对象类型
虽然字符串支持多种操作,但是它具有不可变性,即原字符串不能改变,只能用新字符串作为结果赋予一个变量。下面是一个试图改变原字符串的操作及报错信息:
>>> s="spam" >>> s[0] = 'z' Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: 'str' object does not support item assignment
第五章 数字
str和repr显示格式
>>>num = 1/3.0 >>>num 0.33333333333333331 >>>print num 333333333333 >>>repr(num) '0.33333333333333331' #交互模式回显 >>>str(num) '333333333333' #打印语句
浮点数运算在精确方面有缺陷。这和硬件有关,打印结果也不能完全解决。
>>> 0.1+0.1+0.1-0.3 5.551115123125783e-17 >>> print 0.1+0.1+0.1-0.3 5.55111512313e-17
使用小数对象可以进行修正
>>> from decimal import Decimal >>> Decimal('0.1') + Decimal('0.1') + Decimal('0.1') - Decimal('0.3') Decimal('0.0')
第六章 动态类型简介
a = 3这个语句实际上执行了三个步骤:创建一个对象代表值3;如果还未创建,创建一个变量a;将变量与新的对象3连接。这时,变量a成为了对象3的一个引用,也可以看做是指针。
类型属于对象,而不是变量,这就很好理解为什么Python中同一个变量名可以作为不同类型的对象的引用了。
在这种机制下,每个对象都有一个引用计数器,当计数器为0时就被系统回收,这便是Python中对象的垃圾收集的方法了。
不同变量引用同一个数字或字符串时,对变量操作(eg.a=3 a=a+2)只是创建了一个新的对象并使它引用新对象,这也是上一章提到的字符串不能改动的原因。而对于一些类型来说,有的操作确实能改变对象,如下所示:
#situation 1 >>>L1=[2,3,4] >>>L2=L1 >>>L1=24 >>>L2 [2,3,4] #situation 2 >>L1=[2,3,4] >>>L2=L1 >>>L2[0]=24 >>>L1 [24,3,4]
为了让两个变量使用不同的对象,可以拷贝对象,使用L2=L1[:]来代替L2=L1即可。对于字典则使用D.copy()方法。标准库的copy模块提供了一个对任意对象的调用方法,下面两种方式的区别暂不讨论:
import copy X = copy.copy(Y) #表层拷贝 X = copy.deepcopy(Y) #深拷贝
这里就出现了个问题,两个引用是否是同一对象?可以用下面的方式判断:
>>>L=[1,2,3] >>>M=L >>>L== M True >>>L is M True
负值索引相当于从末尾倒数。-1就是最后一个元素的索引。对于s="spam",-5是个非法的索引值。分号:前的空值表示从第一个开始,后的空值表示直到最后一个。
第七章 字符串
单双引号是一样的,这样允许用户不使用转移字符来实现带有单或双引号的字符串。个人认为避免了按shift才能使用双引号“的麻烦。
>>>'knight"s ',"knight's" ('knight"s ',"knight's")
此外,合并相邻的字符串常量,如'knight"s ' "knight's"(中间有空格)会显示为'knight"s knight\'s'。可见,最外层是单引号,为了保持原内容,Python把单引号里的单引号改写成了转义字符,这个例子和书上的不同,更有助于理解。转义字符和C很类似,多了几种;但是Python里没有空字符串,Python为每个字符串保存了内容和长度。同时,如果一个字符串中没有合法的转义编码出现在"\"后,那么它将在字符串中保留反斜线。抑制转义的方法是在字符串前加r,如r"C:\new\text.dat",此时的\n和\t就不会被当做是转义字符,同时,这样做也不必把\改写成\\。
三重引号适用于多行的字符串的直接输入而不使用转义字符。利用三重引号也可以实现类似C中/* */注释掉代码的目的。
Unicode字符串通过在前面加u获得。
扩展分片是第三个索引,用作步进。这时完整的分片形式为X[I:J:K],其中步进为K。当步进取-1时,可以把字符串反转,很神奇的方法。
利用分片,可以对字符串进行修改,即把新字符串加到原字符串上,再把原字符串切掉。
字符串格式化的用法与C的printf很像,不同之处在于所有参数外需要加一个(),形成%(arg1,arg2,arg3)的形式。格式化代码请参考原书表格,通用结构:%[(name)][flags][width][.precision]code,其中name可以是字典名,这时在参数表里提供这个字典的键即可。
既然字符串是对象,那么它就有对应的方法。书上介绍了修改字符串的replace()、查找find()、把每个元素取出创建列表的list()、把列表合并成字符串的join()(可以作为list()的反操作)、提取组件的split()。
第八章 列表
用中括号表示列表,列表的组成对象是有序的,组成列表的各个对象允许不同。
用大括号表示字典,字典的组成对象是无序的,字典键的搜索方式是哈希搜索,速度很快。
可以用字典来模拟列表:使用序数作为字典的索引即可。类似地,字典可以用来表示一些稀疏矩阵。
字典的get方法用于避免不存在的键,如果键不存在,返回值是0。
字典接口是使用方式类似字典并且实际工作都和字典一样的一些Python扩展程序的接口。
第9章 元组、文件及其他
用小括号表示元组,元组不能原处修改。
为了避免只含一个元素的元组被当做表达式,使用一个逗号,写为(40,)。逗号可以帮助识别元组,下面的也是元组的表示方式:
t = 0,'Ni',1.2,3
从文件中读取的是字符串,需要作为其他类型来操作时必须转换。
eval()用来把字符串作为对象,因此也可以达到执行Python的任何表达式。
pickle模块可以直接在文件中存储几乎任何Python对象。
struct模块提供了二进制数据的打包。打包+存入文件,读取文件+解包。
在“赋值VS引用”这一节,对于复合方式的赋值,修改其成员会导致所有使用该成员的对象的改变。直观来看就是下面:
>>>X = [1,2,3] >>>L = ['a', X, 'b'] >>>D = {'x':X, 'y':2} >>>X[1] = 'surprise' >>>L ['a', [1,'surprise',3], 'b'] >>>D {'x':[1,'surprise',3], 'y':2}
这是一个陷阱,为了避免这种情况,根据具体类型使用拷贝(比如分片、copy方法)而不是引用。
Python内部暂时存储并重复使用短字符串,因此对同样内容的字符串,is判定可能根据其长度为True(字符串较短时)或False(字符串较长时)。不同类型的比较(用==进行)的判定方式不一样。
还有其他内置类型陷阱,如重复能增加层次深度,循环数据结构L=L.append(L)
第二部分练习题
2.(摘自附录B)分片运算超出边界(例如,L[-1000:100])可工作,因为Python会缩放超出边界的分片(必要时,限制值可设为零和序列长度)。以翻转的方式提取序列是行不通的(较低边界值比较高边界值更大,例如,L[3:1])。你会得到空分片([ ]),因为Python会缩放分片限制值,以确定较低边界永远比较高边界小或相等(例如,L[3:1]会缩放成L[3:3],空的插入点是在偏移值3处)。Python分片一定是从左至右抽取,即使你用负号索引值也是这样(会先加上序列长度转换成正值)。注意到,Python 2.3的第三限制值分片会稍微修改此行为:L[3:1:-1]的确是从右至左抽取。
3.索引运算、分片运算以及del:对于L=[1,2,3,4], L[2] = []只能把3变为[],而L[2:3] = []却能删掉第三项。
4.X,Y = Y,X,左边视为两个对象,右边视为一个元组,这个表达式交换了两个引用。
第10章 Python语句简介
绝大多数的Python程序每行一个语句,不需要分号。Python的风格就是完全不要分号,虽然在语句末加上分号也能通过。唯一需要分号的情况是一行中多个语句的分隔符。相反地,括号可以使一个语句分隔成很多行,比如用列表直观地定义一个矩阵时。反斜线\也可以达到这个目的。
嵌套代码只需要保持缩进一致即可。Python是WYSIWYG语言(what you see is what you get,所见即所得)
第11章 赋值、表达式和打印
形如X+=Y的赋值语句称为增强赋值语句,它有三个优点:程序员输入减少,左侧只需要计算一次(X=X+Y中X计算两次),优化技术会自动选择(支持原处修改的类型可以直接原处修改)。
单一下划线开头的变量名不会被from module import *这样的语句导入。前后都有双下划线的变量名是系统定义的变量名,对解释器有特殊意义。双下划线开头但结尾没有双下划线的变量是类的本地变量(参考第19章)。
表达式语句通常用于执行可原处修改列表的列表方法,即对于列表L,L.append(3)是正确的,而L=L.append(4)是错误的,第二个式子右边返回的是None。
标准输出的重定向方法:
import sys sys.stdout = open('log.txt','a') ... print x,y,x #写入log.txt
为了避免忘记恢复sys.stdout,写入log.txt也可以用:
log = open('log.txt','a') print >>log, x, y, x
第12章 if测试
类似于C,Python的布尔运算or是短路运算,而它返回第一个为真的操作对象,或者是第二个为假的对象。[ ] or { } 将返回{ }。
if选择分支有以下几种等价形式:
#最常见的分支形式 if X: A = Y else: A = Z #Python2.5以后引入 A = Y if X else Z #Python2.5以前(以后也兼容) #需要理解and和or的运算和返回值规则 A = ((X and Y) or Z) #列表形式 A = [Z,Y][bool(X)]
第13章 while和for循环
pass语句是无运算的占位符,为了表示语法需要语句并且还没有任何实用的语句可写时就可以使用它。
while和for循环都有一个可选的else语句,在循环条件不满足时且没有用break结束循环时使用。
C语言形式的 while((x = next()) != NULL) { ...process x...}在Python里行不通:C语言赋值语句会返回赋值后的值,而Python赋值语句只是语句,不是表达式。
Python的迭代协议:有next方法的对象会前进到下一个结果,而在末尾时引发StopIteration。所有的迭代工具内部工作都调用next,并捕捉StopIteration异常来确定何时离开。
用for修改列表时,for x in L:x+=1是无法修改的,因为它修改的是循环变量x而不是列表L。应该使用L[i] +=1的索引来控制修改。
zip()可以用于for并行修改多个对象时的情况(按最短的截断)。它也可以用来再循环中建立列表:for (k,v) in zip(keys, vals):D[k]=v。
enumerate()用于产生偏移和元素:for (offset,item) in enumerate(S): print offset,item
基本的列表解析:L=[x+10 for x in L]
扩展的列表解析,删除文件中的换行符:
lines = [line.rstrip() for line in open('script1.py') if line[0] =='p']
从文件中逐行读取文本行的最佳方法是不要刻意去读:
for line in open('script1.py'): print line.upper()
第十四章 文档
__doc__属性封装了对象上的文档,通过它可以查看(比如函数的)注释。
文档字符串被认为最是用于较大、功能性的文档,而#最好只限于关于费解的表达式或语句的微型文档。PyDoc系统能够将前者取出并显示。
第十五章 函数基础
def是可执行代码,直到运行了def时其定义的函数才开始存在。
由于函数的参数没有类型规定,因此可以很方便地实现多态:
>>>def times(x,y): ... return x*y ... >>>times(2,4) 8 >>>times('Ni',4) 'NiNiNiNi'
第十六章 作用域与参数
变量名解析的LEGB原则:变量名引用分为三个作用域进行查找,本地作用域(L,每次调用函数时创建)、上一级调用的本地作用域(E)、全局作用域(G,模块作用域)、内置作用域(B,预定义的变量名如open)。仅对简单变量生效,对于特定对象的变量如object.spam,查找规则规则完全不同。
内置作用域是一个名为__builtin__的内置模块,import后才可以使用,这时可以用dir(__buildin__)查看预定义的变量名。根据LEGB原则,在本地作用域定义一个新的open = 'spam'会导致open()函数不能被调用。
global用于全局变量声明。
作者认为在模块导入时,导入其他模块的模块拥有了对其他模块变量的修改权,这使得被导入模块的维护变得复杂:维护者不知道第二个模块什么情况下会修改变量。因此最好的解决办法是不这样做,在文件间进行通信的最好办法就是通过调用函数,传递参数,然后获得返回值(用函数提供修改变量的接口,并且告诉维护者这个变量可以被其他模块改变)。
工厂函数(又称闭合),是能记住嵌套作用域的变量值的函数。示例:
>>> def maker(N): ... def action(X): ... return X ** N ... return action ... >>> f = maker(2) >>> f #显示f的引用 <function action at 0xb7738294> >>> f(3) 9 >>> f(4) 16 >>> g = maker(3) >>> g(3) 27 >>> f(3) 9
对于函数参数,不可变参数是通过传值来传递,可变对象(列表、字典等)是通过传引用进行传递的。
多个返回值的常用return方式是使用元组。
函数参数的四种匹配方式:位置匹配、关键字参数、所有对象、所有关键字/值:
>>>def f(a,b,c):print a,b,c #常规匹配 >>>f(1,2,3) 1 2 3 >>>f(c=3,b=2,a=1) #关键字匹配 1 2 3 >>>f(1,c=3,b=2) #位置+关键字匹配 1 2 3 >>>def f(a,b=2,c=3):print a,b,c #默认参数 >>>f(1) 1 2 3 >>>f(a=1) 1 2 3 >>>def f(*args):print args # 任意参数* >>>f() () >>>f(1) (1,) >>>f(1,2,3,4) >>>def f(**args):print args # 任意参数**,把参数组装成为字典 >>>f() {} >>>f(a=1,b=2) {'a'=1,'b'=2}
相反地,调用函数在参数变量前面加*可以分解参数。
参数匹配顺序:调用和定义中先是非关键字参数(name)、然后是关键字参数(name=value)、*name最后是**name。
第17章 函数的高级话题
def f(x,y,z):return x+y+z和f= lambda x,y,z:x+y+z会达到同样的效果。lambda是一个表达式,而不是语句,允许出现在def不能出现的地方。正是因为这个特点,lambda比def的使用更加灵活,比如编写跳转表(也即行为的列表或字典):L=[(lambda x:x**2),[(lambda x:x**3),[(lambda x:x**4)]。出于代码易读性的考虑,应尽量避免嵌套的lambda。
apply的介绍略过,它可以用*和**型参数代替。(似乎在Python3.0以上版本已废弃,待确认)
map(func,arg)可以很方便的用函数func处理列表类型的数据,而自己编写类似的功能需要使用for来完成。
filter和reduce这两个函数工具分别用于列表过滤和列表全元素的逐个运算。
关于列表解析,带if条件的之前已提过,不再重复。for的应用示例:
>>> res = [x+y for x in [0,1,2] for y in [100,200,300]] >>> res [100, 200, 300, 101, 201, 301, 102, 202, 302]
更进一步的嵌套:
>>>[(x,y) for x in range(5) if x%2 ==0 for y in range(5) if y%2==0]
作者在这里开了个小玩笑:“而map和filter的等效形式往往更复杂也会有深层的嵌套,这里不进行说明,将这部分代码留给禅师、前LISP程序员以及犯罪神经病作为练习”。
生成器函数与一般函数不同之处在于,它yield而不是return一个值,并把自己挂起,现场保存在下一次调用。为与列表解析相区分,可以使用圆括号作为生成器表达式:
>>> for num in (x **2 for x in range(4)): ... print '%s,%s' %(num,num/2.0) ... 0,0.0 1,0.5 4,2.0 9,4.5
一个测试不同的迭代方法的小程序。当然,对于不同的操作,不同方法的相对速度可能不一样,不存在所有情况下都最快的“最优方法”:
#file timerseqs.py import time,sys reps = 1000 size = 10000 def tester(func, *args): startTime = time.time() for i in range(reps): func(*args) elapsed = time.time() - startTime return elapsed def forStatement(): res = [] for x in range(size): res.append(abs(x)) def listComprehension(): res = [abs(x) for x in range(size)] def mapFunction(): res = map(abs, range(size)) def generatorExpression(): res = list(abs(x) for x in range(size)) print sys.version tests = (forStatement, listComprehension, mapFunction,generatorExpression) for testfunc in tests: print testfunc.__name__.ljust(20), '=>',tester(testfunc)
陷阱:本地变量是静态检测的。这意味着如果在模块里定义了X=99,def一个函数print X后又在函数里X=88,那么就会报错。
陷阱:默认对象在def时赋值,而不是调用函数时赋值。
第18章 模块:宏伟蓝图
Python进行import时搜索目录的顺序:主目录、PYTHONPATH环境变量目录、标准库目录、.pth目录。
第19章 模块代码编写基础
将会被用于导入的模块文件命名需要以.py做结尾。
当两个不同模块使用了相同的变量名时,不能用from,只能用import。
(本章大部分内容都在第三章介绍过)
第20章 模块包
import时列出路径名称,以点号相隔:import dir1.dir2.mod。这与平台无关,import不能使用平台特定的路径表达方式。同时,这也表明文件名省略了.py的原因。另外,dir1和dir2中必须包含一个__init__.py文件(可以为空,Python首次进入其所在目录时会执行它的内容)。每次使用路径必须完整输入,使用import dir1.dir2.mod as mod中定义的mod代替前面过长的路径名可以解决这个问题。
个人认为,模块包是为了方便同名模块的使用不发生混淆的方式,这是软件开发时所需要的。
第21章 高级模块话题
_X的命名方式可以防止from *导入这个变量,然而这种方法不能阻止其他导入方式的导入,并不是一些面向对象语言中的私有声明。
__all__会列出from *复制的变量名,与_X正相反。同样只对from *有效,不是私有声明。
from __feature__ import featurename还不是很理解,好象是用选用扩展功能的方式开启特殊的代码编译。
模块可以通过检测自己的__name__是否为"__main__"确定它是在执行还是被导入。这样可以让模块在扮演两种不同角色时发挥不同功能。
相对导入:路径以一个点开始,定位同一个包的模块。可以开启__feature__中强迫导入的绝对性。很类似于Linux,两个点表示上一级路径。
陷阱一:顶层代码的语句次序。被import时模块的顶层代码会立即执行,此时它所引用后文定义的变量将无效。
陷阱二:字符串变量是不能直接用于import语句的。可以使用exec "import" + modname来使用字符串modname。这样做仍然有个缺点,每次执行时必须编译import语句。更好的代替方案是string = __import__(modname),然后把string单列一行执行即可。
陷阱三:from复制变量名而不是拷贝。
#nested1.py X = 99 def printer():print X #nested2.py from nested1 import X,printer X = 88 printer() %python nested2.py 99
陷阱四:reload不影响from导入。为了更新变量,使用.运算符来导入和修改其他模块的变量。
陷阱五:reload、from及交互模式测试。这部分比较有启发性,建议在原书仔细阅读,简要概括就还是:导入后(模块)要重载(模块),重载后还要重新执行import(变量)。reload和from的合作并不完美,最佳原则是使用reload和import来启动程序。
陷阱六:重载没有传递性。重载A不会重载A中import的B和C。需要这种功能时可以自己编写一个通用工具。
陷阱七:递归导入。
第22章 OOP:宏伟蓝图
属性通常是在class语句中通过赋值语句添加在类中,而不是在定义类时嵌入。因此对没有赋值的对象属性的访问会出错。
类方法函数第一个参数通常为self(调用时不指明),但不一定叫self,位置是关键(来自习题5)。作为类方法直接调用时,需指明实例的名称(24章)。
Python的OOP模型其实就是在对象树中搜索属性。
(笔者有部分OOP基础,因此本章具体理论和理解略去)
第23章 类代码编写基础
类其实也是一种对象。
在类定义外创建的函数也可以成为方法:
>>>def upperName(self): ... return self.name.upper() >>>rec.method = upperName
第24章 类代码编写细节
和def一样,class也是可执行代码,运行时才会产生类对象。
调用超类的构造器是可以的,在子类的构造方法中使用Super.__init__()即可。
抽象超类有的方法没有提供实现,而是由子类提供。
类的运算符重载通过修改诸如__add__(对应于+)等方法来实现。具体细节请参考原书。下面是一个修改__iter__获得用户定义的迭代器的例子:
__setattr__的修改可以模拟实现成员变量私有性,这里不贴书中的源码了。
右侧方法如__radd__中,self在右侧,和__add__相反。
__call__可以拦截调用,用使用函数的方法使用类。对改写了__call__的类prod,实例化x = prod(2),x(3)可以直接使用。
__del__是析构器,但在Python中很少使用析构方法。
命名空间其实是普通的字典。
第25章 类的设计
无绑定类方法对象无self必须明确提供实例对象做第一个参数,绑定实例方法对象用self+函数对,不用传递实例。
委托是指把对象包装在代理类中。
#trace.py class wrapper: def __init__(self,object): self.wrapped = object def __getattr__(self,attrname): print 'Trace:',attrname return getattr(self.wrapped,attrname) >>>from trace import wrapper >>>x = wrapper([1,2,3]) >>>x.append(4) Trace:append >>>x.wrapped [1,2,3,4]
组合是一种技术,让控制器类嵌入和引导一群对象,并自行提供接口。
(这一章主要内容是帮助读者从面向过程向面向对象过渡,而且比较浅显,在这方面作者推荐继续去读设计模式的书效果会更好,这里就不详细介绍了)
第26章 类的高级主题
伪私有属性:将开头两个下划线的变量名前再加上_类名。仍然不是真正的私有。
新式类从内置类型创建子类,或者直接用object作为超类。3.0以后所有类自动成为新式类。钻石继承在新式类里从括号最右开始搜索,这与经典类正相反。为了解决继承不同类同名变量冲突,可以进行强制规定,如attr = B.attr。
__slots__用来限制类的实例能有的合法属性集。
内容属性使用拦截的方式来提供属性,但是它本身不是成员变量。类似于改写__getattr__,使用property()进行。
静态方法和类方法分别需要调用staticmethod和classmethod两个函数,前者调用不需要实例(实例调用时),后者把类传入类方法第一个参数。
函数装饰器在def上一行用@标明,有点像包裹函数,@A @B @C后def f()相当于f=A(B(C(f)))。
第27章 异常基础
try/except可以用于捕捉异常并从异常中恢复,而try/final可以保证无论是否发生异常,终止行为都一定会进行。二者也可以合并使用(2.5版以后)。else在不发生异常时执行。except有几种分句形式(请参考原书)。
rasie、assert用于触发异常。raise后不带参数表示重新引发当前异常(第28章)。
with/as可以用作try/final的替代方案。as后面是with后表达式的赋值对象。
第28章 异常对象
字符串异常(myexc = "My exception string";raise myexc)已经在3.0以后消失,现在常用的是基于类的异常。类异常比字符串异常方便之处在于,可以在原始版本中用超类定义异常,在后续版本中使用子类来描述新的异常,这为版本维护提供了极大的方便。字符串异常的判断方式是is而不是==(常见陷阱,29章)。
第29章 异常的设计
嵌套的try,引发异常时except会回到先前进入但未离开的try,而finally不会停止传递。
用try进行调试的方式,在错误发生时程序仍处于激活状态,可以进行其他的测试而不是重新开始:
try: ...run program... except: import sys print 'uncaught!', sys.exc_info()[0], sys.exc_info()[1]
#sys.exc_info有专门一小节讲解,无异常返回3个None
#反之返回type value tracebck