python基础
数据类型和变量、字符串和编码、list和tuple、条件判断、循环、dict和set
函数
函数的参数
位置参数:def person(name,age):
默认参数(默认参数必须指向不变对象!如None)、fuc(a=0)
可变参数(*num)
:可变参数允许你传入0个或任意个参数,这些可变参数在函数调用时自动组装为一个tuple。例如 calc(1,2,3)
关键参数(name,age,**kw)
:关键字参数允许你传入0个或任意个含参数名的参数,这些关键字参数在函数内部自动组装为一个dict,例如 persion('Bob',35,city='Beijing')
命名关键字参数(name,age,*,city,job)
:如果要限制关键字参数的名字,就可以用命名关键字参数。*后面的参数被视为命名关键字参数。例如 person('Jack', 24, city='Beijing', job='Engineer')
。如果参数中已经有了一个可变参数,后面跟着的命名关键字参数就不需要一个特殊的分隔符*了。如 def person(name, age, *args, city, job)
⚠️关键参数/命名关键字参数调用时必须给定相应的参数名
参数组合:在Python中定义函数,可以用必选参数、默认参数、可变参数、关键字参数和命名关键字参数,这5种参数都可以组合使用。但是请注意,参数定义的顺序必须是:必选参数、默认参数、可变参数、命名关键字参数和关键字参数。
⚠️对于任意函数,都可以通过类似func(*args, **kw)
的形式调用它,无论它的参数是如何定义的。前者会自动分配参数给 位置参数 和 默认参数 以及 可变参数,后者可自动分配参数给 命名关键字参数 和 关键字参数。
要注意定义可变参数和关键字参数的语法:
*args
是可变参数,args接收的是一个tuple;
**kw
是关键字参数,kw接收的是一个dict。
递归函数
在函数内部,可以调用其他函数。如果一个函数在内部调用自身本身,这个函数就是递归函数。
1 | def fact(n): |
⚠️使用递归函数需要注意防止栈溢出。在计算机中,函数调用是通过栈(stack)这种数据结构实现的,每当进入一个函数调用,栈就会加一层栈帧,每当函数返回,栈就会减一层栈帧。由于栈的大小不是无限的,所以,递归调用的次数过多,会导致栈溢出。
解决递归调用栈溢出的方法是通过尾递归优化,事实上尾递归和循环的效果是一样的,所以,把循环看成是一种特殊的尾递归函数也是可以的。尾递归是指,在函数返回的时候,调用自身本身,并且,return语句不能包含表达式。这样,编译器或者解释器就可以把尾递归做优化,使递归本身无论调用多少次,都只占用一个栈帧,不会出现栈溢出的情况。
例如:
1 | def fact(n): |
汉诺塔问题…
python高级特性
本节包括多个python的高级特性介绍,这是与其他编程语言有明显区别的地方,主要包括slice、iteration、列表生成式、生成器、迭代器
切片(slice)
针对于list,如L = [i for i in range(10)],取最后一个元素 L[-1],取最后三个元素 L[-7:],每2个取一个 L[::2],使用L[:]可以复制一个list
针对于tuple,也可以同样操作。字符串也可以是一种list,用切片的方式去substring
迭代
对于dict,可以使用以下方法来对keys集合进行迭代
1 | d = {'a':1,'b':2} |
若想对valu进行迭代,可以使用for value in d.values()
,如果要同时对key和value进行迭代,可以使用for k,v in d.items()
,由于字符串也是可迭代对象,所以也可以用同样的方式来遍历字符串中所有的字符。
判断一个对象是否是可迭代对象:通过collections模块中的iterable类型判断:
1 | from collections import Iterable |
最后一个小问题,如果要对list实现类型Java中下标循环怎么办?python内置的enumerate函数可以把list变成索引-元素对,这就可以在for循环中同时迭代索引和元素本身。
1 | for i,value in enumerate(['A','B','C']) |
列表生成器
list(range(5))
可以得到[0,1,2,3,4]
[x*x for x in range(5)]
可以得到[0,1,4,9,16]
[x*x for x in range(5) if x%2!=0]
可以得到[1,9]
[m + n for m in 'ABC' for n in 'XYZ']
可以得到两层循环的全排列:['AX', 'AY', 'AZ', 'BX', 'BY', 'BZ', 'CX', 'CY', 'CZ']
1 | #练习,如果list中既包含字符串,又包含整数,如何将list中所有的字符串全部转换为小写字母 |
生成器(generator)
Motivation:通过列表生成式我们可以直接创建一个列表。但是,受到内存限制,列表容量肯定是有限的。而且,创建一个包含100万个元素的列表,不仅占用很大的存储空间,如果我们仅仅需要访问前面几个元素,那后面绝大多数元素占用的空间都白白浪费了。
所以,如果列表元素可以按照某种算法推算出来,那我们是否可以在循环的过程中不断推算出后续的元素呢?这样就不必创建完整的list,从而节省大量的空间。在Python中,这种一边循环一边计算的机制,称为生成器:generator。
1 | for x in range(10)] L = [x * x |
创建L
和g
的区别仅在于最外层的[]
和()
,L
是一个list,而g
是一个generator。我们可以通过next(g)来打印generator中的每一个元素。
1 | for x in range(10)) g = (x * x |
斐波拉契数列的实现用列表生成器无法实现,但是利用函数可以很容易打印出来:
1 | def fib(num): |
迭代器(iterator)
我们已经知道,可以直接作用于for
循环的数据类型有以下几种:
一类是集合数据类型,如list
、tuple
、dict
、set
、str
等;
一类是generator
,包括生成器和带yield
的generator function。
这些可直接作用于for循环对象统称为可迭代对象:Iterable
可以使用from collections import Iterable
,利用isinstance(object,Iterable)
判断是否为可迭代对象。
而生成器(generator)不仅可以用于for循环,还可以使用next()
函数不断调用并返回下一个值,直到最后抛出StopIteraction
错误。可以被next()
函数调用并不断返回下一个值的对象称为迭代器:Iterator
可以使用from collections import Iterator
,利用isinstance(object,Iterator)
判断是否为迭代器对象
1 | from collections import Iterator |
⚠️为什么list
、dict
、str
等数据类型不是Iterator
?
这是因为Python的Iterator
对象表示的是一个数据流,Iterator对象可以被next()
函数调用并不断返回下一个数据,直到没有数据时抛出StopIteration
错误。可以把这个数据流看做是一个有序序列,但我们却不能提前知道序列的长度,只能不断通过next()
函数实现按需计算下一个数据,所以Iterator
的计算是惰性的,只有在需要返回下一个数据时它才会计算。
Iterator
甚至可以表示一个无限大的数据流,例如全体自然数。而使用list是永远不可能存储全体自然数的。
函数式编程
本节主要包含高阶函数、返回函数、匿名函数、装饰器、偏函数。
函数式编程就是一种抽象程度很高的编程范式,纯粹的函数式编程语言编写的函数没有变量,因此,任意一个函数,只要输入是确定的,输出就是确定的,这种纯函数我们称之为没有副作用。而允许使用变量的程序设计语言,由于函数内部的变量状态不确定,同样的输入,可能得到不同的输出,因此,这种函数是有副作用的。
高阶函数
变量可以指向函数:
1 | # 以python内置求绝对值函数abs(): |
函数名就是变量:
1 | # 数名其实就是指向函数的变量!对于abs()这个函数,完全可以把函数名abs看成变量,它指向一个可以计算绝对值的函数! |
传入函数
1 | # 既然变量可以指向函数,函数的参数又能接收变量,那么可以将一个函数当作参数传入另外一个函数中,这种接收函数为参数的函数为高阶函数。 |
把函数作为参数传入,这样的函数称为高阶函数,函数式编程就是指这种高度抽象的编程范式。
map/reduce
函数
map()
函数接收两个参数,一个是函数,一个是Iterable
,map
将传入的函数依次作用到序列的每个元素,并把结果作为新的Iterator
返回。
1 | def f(x): |
reduce
把一个函数作用在一个序列[x1, x2, x3, ...]
上,这个函数必须接收两个参数,reduce
把结果继续和序列的下一个元素做累积计算。
1 | from functools import reduce |
1 | from functools import reduce |
filter
函数
filter()
也接收一个函数和一个序列。和map()
不同的是,filter()
把传入的函数依次作用于每个元素,然后根据返回值是True
还是False
决定保留还是丢弃该元素。
1 | def not_empty(s): |
注意到filter()
函数返回的是一个Iterator
,也就是一个惰性序列,所以要强迫filter()
完成计算结果,需要用list()
函数获得所有结果并返回list。
思考:用filter求素数(埃式筛法)、筛选回数(回数是指从左向右读和从右向左读都是一样的数)
sorted
1 | 36, 5, -12, 9, -21]) sorted([ |
练习:假设有一组tuple表示学习名字和成绩:
1 | L = [('Bob', 75), ('Adam', 92), ('Bart', 66), ('Lisa', 88)] |
返回函数
函数作为返回值
高阶函数除了可以接受函数作为参数以为,还可以吧函数作为结果值返回,如:
1 | def calc_sum(*args): |
在函数lazy_sum
中又定义了函数sum
,并且,内部函数sum
可以引用外部函数lazy_sum
的参数和局部变量,当lazy_sum
返回函数sum
时,相关参数和变量都保存在返回的函数中,这种称为“闭包(Closure)”的程序结构拥有极大的威力。
闭包
在闭包结构中,注意到返回的函数在其定义内部引用了外部函数的局部变量args
,所以,当一个函数返回了一个函数后,其内部的局部变量还被新函数引用。
1 | def count(): |
⚠️ 返回闭包时牢记一点:返回函数不要引用任何循环变量,或者后续会发生变化的变量。
1 | def count(): |
匿名函数
关键字lambda
表示匿名函数
如lambda x:x*x
,其中冒号前面的x表示参数,匿名函数有个限制,就是只能有一个表达式,不用写return
,返回值就是该表达式的结果。匿名函数也是一个函数对象,也可以把匿名函数赋值给一个变量,再利用变量来调用该函数:
1 | lambda x: x * x f = |
装饰器
在代码运行期间动态增加功能的方式(也可以理解为不更改原来的函数),称之为“装饰器”(Decorator)。
1 | # 例如下面的函数,现需要增强now()的函数功能,例如需要在函数调用前后自动打印日志,但是不希望更改now()函数的定义。 |
使用装饰器来实现上述需求:
1 | # 这里我们就需要使用到decorator,其就是一个返回函数的高阶函数。 |
如果需要给decorator传入参数,则参考之前返回函数中的用法,需要编写一个返回decorator的高阶函数。
1 | def log(text): |
以上两种decorator的定义都没有问题,但还差最后一步。因为我们讲了函数也是对象,它有__name__
等属性,但你去看经过decorator装饰之后的函数,它们的__name__
已经从原来的'now'
变成了'wrapper'
,因为返回的那个wrapper()
函数名字就是'wrapper'
,所以,需要把原始函数的__name__
等属性复制到wrapper()
函数中,否则,有些依赖函数签名的代码执行就会出错。
不需要编写wrapper.__name__ = func.__name__
这样的代码,Python内置的functools.wraps
就是干这个事的,所以,一个完整的decorator的写法如下:
1 | import functools |
练习
设计一个装饰器(decorator),它可以作用于任何函数上,并打印该函数的执行时间
1 | import functools |
偏函数
Python的functools
模块提供了很多有用的功能,其中一个就是偏函数(Partial function)。要注意,这里的偏函数和数学意义上的偏函数不一样。
1 |
|
扩展
1 | # 增加可变参数定义 |
模块
在Python中,一个.py文件就称之为一个模块(Module)。
使用模块的好处:1、最大的好处是大大提高了代码的可维护性。其次,编写代码不必从零开始。2、使用模块还可以避免函数名和变量名冲突。
为了避免模块名冲突,Python又引入了按目录来组织模块的方法,称为包(Package)。引入了包以后,只要顶层的包名不与别人冲突,那所有模块都不会与别人冲突。现在,abc.py
模块的名字就变成了mycompany.abc
,类似的,xyz.py
的模块名变成了mycompany.xyz
。
⚠️每一个包目录下面都会有一个__init__.py
的文件,这个文件是必须存在的,否则,Python就把这个目录当成普通目录,而不是一个包。__init__.py
可以是空文件,也可以有Python代码,因为__init__.py
本身就是一个模块,而它的模块名就是mycompany
。
使用模块
以内建的sys
模块为例:
1 | #!/usr/bin/env python3 |
导入sys
模块后,我们就有了变量sys
指向该模块,利用sys
这个变量,就可以访问sys
模块的所有功能。
sys
模块有一个argv
变量,用list存储了命令行的所有参数。argv
至少有一个元素,因为第一个参数永远是该.py文件的名称,例如:
运行python3 hello.py
获得的sys.argv
就是['hello.py']
;
运行python3 hello.py Michael
获得的sys.argv
就是['hello.py', 'Michael]
。
⚠️当我们在命令行运行hello
模块文件时,Python解释器把一个特殊变量__name__
置为__main__
,而如果在其他地方导入该hello
模块时,if
判断将失败,因此,这种if
测试可以让一个模块通过命令行运行时执行一些额外的代码,最常见的就是运行测试。如果import hello,则需要使用hello.test()
才能调用test方法
关于__name__==__main__
的作用:
__name__==__main__
即让模块可导入到别的模块,也可以该模块自己执行测试。
当在module2中调用module1时,不会执行module1中的__name__==__main__
,在python交互环境下,导入module2模块后使用,也不会执行module1中__name__==__main__
的部分,只用来临时测试使用,即运行python3 module1
时会执行。
作用域
在一个模块中,我们可能会定义很多函数和变量,但有的函数和变量我们希望给别人使用,有的函数和变量我们希望仅仅在模块内部使用。在Python中,是通过_
前缀来实现的。
类似__xxx__
这样的变量是特殊变量,可以被直接引用,但是有特殊用途,例如__name__
类似_xxx
和__xxx
这样的函数或变量就是非公开的(private),不应该被直接引用,比如_abc
,__abc
等;
1 | def _private_1(name): |
在模块里公开greeting()
函数,而把内部逻辑用private函数隐藏起来了,这样,调用greeting()
函数不用关心内部的private函数细节,这也是一种非常有用的代码封装和抽象的方法,即:外部不需要引用的函数全部定义成private,只有外部需要引用的函数才定义为public。
安装第三方模块
如果调用自己的模块显示默认路径下没有该模块,这是因为默认情况下,python解释器会搜索当前目录、所有已安装的内置模块和第三方模块,搜索路径存放在sys
模块的path
变量中。如果需要添加自己的搜索目录,有两种方法:
1 | import sys |
另外一种方法即设置环境变量PYTHONPATH
,该环境变量的内容会被自动添加到模块搜索路径中。
面向对象编程(OOP)
面向对象编程——Object Oriented Programming,简称OOP,是一种程序设计思想。OOP把对象作为程序的基本单元,一个对象包含了数据和操作数据的函数。
数据封装、继承、多态是面向对象的三大特点
类和实例
面向对象最重要的概念就是类(Class)和实例(Instance),必须牢记类是抽象的模板,比如Student类,而实例是根据类创建出来的一个个具体的“对象”,每个对象都拥有相同的方法,但各自的数据可能不同。
1 | class Student(object): |
在创建类的时候,可以把一些属性强制填写进去,通过一个特殊的__init__
方法:
仍以Student类为例,在Python中,定义类是通过class
关键字:
class
后面紧接着是类名,即Student
,类名通常是大写开头的单词,紧接着是(object)
,表示该类是从哪个类继承下来的,继承的概念我们后面再讲,通常,如果没有合适的继承类,就使用object
类,这是所有类最终都会继承的类。
定义好了Student
类,就可以根据Student
类创建出Student
的实例,创建实例是通过类名+()实现的:
1 | bart = Student() |
可以看到,变量bart
指向的就是一个Student
的实例,后面的0x10a67a590
是内存地址,每个object的地址都不一样,而Student
本身则是一个类。
可以自由地给一个实例变量绑定属性,比如,给实例bart
绑定一个name
属性:
1 | 'Bart Simpson' bart.name = |
由于类可以起到模板的作用,因此,可以在创建实例的时候,把一些我们认为必须绑定的属性强制填写进去。通过定义一个特殊的__init__
方法,在创建实例的时候,就把name
,score
等属性绑上去:
1 | class Student(object): |
和普通的函数相比,在类中定义的函数只有一点不同,就是第一个参数永远是实例变量self
,并且,调用时,不用传递该参数。
数据封装
面向对象编程的一个重要特点就是数据封装。在上面的Student
类中,每个实例就拥有各自的name
和score
这些数据。我们可以通过函数来访问这些数据,比如打印一个学生的成绩:
1 | def print_score(std): |
1 | class Student(object): |
访问限制
在Class内部,可以有属性和方法,而外部代码可以通过直接调用实例变量的方法来操作数据,这样,就隐藏了内部的复杂逻辑。但是,从前面Student类的定义来看,外部代码还是可以自由地修改一个实例的属性,此时将属性设置为私有(private)即可,即在变量前面加__
在Python中,变量名类似__xxx__
的,也就是以双下划线开头,并且以双下划线结尾的,是特殊变量,特殊变量是可以直接访问的,不是private变量,所以,不能用__name__
、__score__
这样的变量名。
⚠️Python中对类的私有变量保护措施:双下划线开头的实例变量是不是一定不能从外部访问呢?其实也不是。不能直接访问__name
是因为Python解释器对外把__name
变量改成了_Student__name
,所以,仍然可以通过_Student__name
来访问__name
变量。
最后注意一个错误:
1 | 'Bart Simpson', 59) bart = Student( |
——— 看到这里我觉得python是非常有趣的。
继承和多态
在OOP程序设计中,当我们定义一个class的时候,可以从某个现有的class继承,新的class称为子类(Subclass),而被继承的class称为基类、父类或超类(Base class、Super class)。
继承有什么好处?最大的好处是子类获得了父类的全部功能。由于Animial
实现了run()
方法,因此,Dog
和Cat
作为它的子类,什么事也没干,就自动拥有了run()
方法。继承的第二个好处需要我们对代码做一点改进。
当子类和父类都存在相同的run()
方法时,我们说,子类的run()
覆盖了父类的run()
,在代码运行的时候,总是会调用子类的run()
。这样,我们就获得了继承的另一个好处:多态。
理解多态
1 | a = list() # a是list类型 |
多态的具体好处:
1 | # 我们来定义一个接受animal类型的变量 |
对于一个变量,我们只需要知道它是Animal
类型,无需确切地知道它的子类型,就可以放心地调用run()
方法,而具体调用的run()
方法是作用在Animal
、Dog
、Cat
还是Tortoise
对象上,由运行时该对象的确切类型决定,这就是多态真正的威力:调用方只管调用,不管细节,而当我们新增一种Animal
的子类时,只要确保run()
方法编写正确,不用管原来的代码是如何调用的。这就是著名的“开闭”原则:
1、对扩展开放:允许新增Animal
子类;2、对修改封闭:不需要依赖Animal类型的run_twice()
等函数
静态语言 vs 动态语言
对于静态语言(Java)来说,如果需要传入Animal
类型,则传入的对象必须是Animal
类型或者它的子类,否则,将无法调用run()
方法。
对于Python这样的动态语言来说,则不一定需要传入Animal
类型。我们只需要保证传入的对象有一个run()
方法就可以了。(这么神奇!)
1 | class Timer(object): |
这就是动态语言的“鸭子类型”,它并不要求严格的继承体系,一个对象只要“看起来像鸭子,走起路来像鸭子”,那它就可以被看做是鸭子。
Python的“file-like object“就是一种鸭子类型。对真正的文件对象,它有一个read()
方法,返回其内容。但是,许多对象,只要有read()
方法,都被视为“file-like object“。许多函数接收的参数就是“file-like object“,你不一定要传入真正的文件对象,完全可以传入任何实现了read()
方法的对象。
动态语言的鸭子类型特点决定了继承不像静态语言那样是必须的。
获取对象信息
1、使type()
函数
1 | type(abs) |
2、使用isinstance()
对于class的继承关系来说,使用type()
很不方便,可以使用isinstance()
函数
1 | a = Animal() |
3、使用dir()函数
要想获得一个对象的所有属性和方法,可以使用dir()函数:
1 | 'ABC') dir( |
仅仅把属性和方法列出来是不够的,配合getattr()
、setattr()
以及hasattr()
,我们可以直接操作一个对象的状态:
1 | class MyObject(object): |
一个正确用法的例子:
1 | def readImage(fp): |
实例属性和类属性
1 | class Student(object): |
在编写程序的时候,千万不要对实例属性和类属性使用相同的名字,因为相同名称的实例属性将屏蔽掉类属性,调利用实例对象访问属性时,只会返回实力属性。但是当你删除实例属性后,再使用相同的名称,访问到的将是类属性。
面向对象高级编程
在Python中,面向对象还有很多高级特性,允许我们写出非常强大的功能。接下来会讨论多重继承、定制类、元类等概念。
使用__slot__
对于python这种动态语言,可以在创建类的实例后,给实例绑定一个属性、方法。但是这只对当前实例有用,对类创建的其他实例无效。
1 | class Student(object): |
如果想所有实例均可调用,给class绑定方法:
1 | def set_score(self, score): |
但是,如果我们想要限制实例的属性怎么办?比如,只允许对Student实例添加name
和age
属性。
1 | class Student(object): |
使用__slots__
要注意,__slots__
定义的属性仅对当前类实例起作用,对继承的子类是不起作用的。除非在子类中也定义__slots__
,这样,子类实例允许定义的属性就是自身的__slots__
加上父类的__slots__
。
使用@property
我们在定义类的时候,如果我们把属性直接暴露在外面:
1 | s = Student() |
但是这种方法无法检查属性值是否有效,所以我们常设置该属性为私有属性,定义相应的get和set方法来分别获取和设置该属性:
1 | class Student(object): |
但是,上面的方法在调用的时候又太过繁琐,设置一个属性需要调用其对应的方法。有没有既能检查参数,又可以用类似属性这样简单的方式来访问类的变量呢?在之前我们学过decorator(装饰器),可以在不改变方法实现的情况下,给函数动态的添加功能。对于类的方法,装饰器一样起作用。Python内置的@property
装饰器就是负责把一个方法变成属性调用的:
1 | class Student(object): |
我们可以定义只读属性,即只定义getter方法,不定义setter方法就是一个只读属性:
1 | class Student(object): |
@property
广泛应用在类的定义中,可以让调用者写出简短的代码,同时保证对参数进行必要的检查
多重继承
对动物animal创建一个类,然后dog、bat等都可以继承于animal这个类。如果我们还考虑“能跑”这个功能,此时定义好Runable
,对于需要该功能的动物,就多继承一个。例如:
1 | class Dog(Mammal, Runnable): |
这就是多重继承,一个子类就可以同时获得多个父类的所有功能。
在设计类的继承关系时,通常,主线都是单一继承下来的。但是,如果需要“混入”额外的功能,通过多重继承就可以实现。这种设计通常称之为MixIn。由于python允许使用多重继承,因此,MixIn就是一种常见的设计,只允许单一继承的语言(如Java)不能使用MixIn的设计。
MixIn的目的就是给一个类增加多个功能,这样,在设计类的时候,我们优先考虑通过多重继承来组合多个MixIn的功能,而不是设计多层次的复杂的继承关系。Python自带了TCPServer
和UDPServer
这两类网络服务,而要同时服务多个用户就必须使用多进程或多线程模型,这两种模型由ForkingMixIn
和ThreadingMixIn
提供。通过组合,我们就可以创造出合适的服务来。
定制类
自定义__slots__
、__str__
、__iter__
、__getitem__
、__getattr__
、__call__
等方法
使用枚举类
1 | from enum import Enum |
使用元类
type()
、定义metaclass
错误、调试和测试
Python内置了一套异常处理机制,来帮助我们进行错误处理。
错误处理
使用try…except…finally…
和其他高级语言一样,python也内置了一套try...except...finally...
的错误处理机制。其中,finally
后面的语句一定执行,如:
1 | try: |
Python的错误其实也是class,所有的错误类型都继承自BaseException
,所以在使用except
时需要注意的是,它不但捕获该类型的错误,还把其子类也“一网打尽”。
使用try...except
捕获错误还有一个巨大的好处,就是可以跨越多层调用,比如函数main()
调用foo()
,foo()
调用bar()
,结果bar()
出错了,这时,只要main()
捕获到了,就可以处理:
1 | def foo(s): |
调用栈
在程序出错后,如果错误没有被捕获,它会一直往上抛,最后被python解释器捕获,打印一个错误信息,然后程序退出。如果不捕获错误,自然可以让Python解释器来打印出错误堆栈,但程序也被结束了。既然我们能捕获错误,就可以把错误堆栈打印出来,然后分析错误原因,同时,让程序继续执行下去。
Python内置的logging
模块可以非常容易地记录错误信息:
1 | # err_logging.py |
抛出错误
如果要抛出错误,首先根据需要,可以定义一个错误的class,选择好继承关系,然后,用raise
语句抛出一个错误的:
1 | # err_raise.py |
只有在必要的时候才定义我们自己的错误类型。如果可以选择Python已有的内置的错误类型(比如ValueError
,TypeError
),尽量使用Python内置的错误类型。
调试
1、直接print()
把可能有问题的变量打印出来。
2、断言 assert
: 凡是用print()
来辅助查看的地方,都可以用断言(assert)来替代
程序中如果到处充斥着assert
,和print()
相比也好不到哪去。不过,启动Python解释器时可以用-o
参数来关闭assert
,关闭后,你可以把所有的assert
语句当成pass
来看。
3、logging
:该方法不会抛出错误,而且可以输出到文件
4、pdb
:启动python的调试器pdb,让程序以单步方式运行,可以随时查看运行状态;或者import pdb
,在可能出错的地方放一个pdb.set_trace()
,就可以设置一个断点,程序会自动在该位置暂停并进入pdb调试环境,可以用命令p
查看变量,或者用命令c
继续运行。
单元测试
“测试驱动开发(TDD:Test-Driven Development)”
文档测试
……
IO编程
由于cpu和内存的速度远远高于外设的速度,所以在IO编程中,就存在速度严重不匹配的问题。1、同步IO(cpu等待磁盘写操作,完成后继续执行后面的操作);2、异步IO(cpu不等待)
很明显,异步IO的程序性能要远远高于同步IO,但是异步IO的缺点是编程模型复杂。要判断IO操作何时执行完,需要采用回调模式(执行完之后主动到程序);或者是轮询模式(间断性的询问执行状态)。
文件读写
Python内置了读写文件的函数,用法和C是兼容的。
读写文件前,我们先必须了解一下,在磁盘上读写文件的功能都是由操作系统提供的,现代操作系统不允许普通的程序直接操作磁盘,所以,读写文件就是请求操作系统打开一个文件对象(通常称为文件描述符),然后,通过操作系统提供的接口从这个文件对象中读取数据(读文件),或者把数据写入这个文件对象(写文件)。
读文件
1 | 'filename.txt'.txt', 'r') f = open( |
调用read()
会一次性读取文件的全部内容,如果文件有10G,内存就爆了,所以,要保险起见,可以反复调用read(size)
方法,每次最多读取size个字节的内容。另外,调用readline()
可以每次读取一行内容,调用readlines()
一次读取所有内容并按行返回list
。因此,要根据需要决定怎么调用。
file-like Object
像open()
函数返回的这种有个read()
方法的对象,在Python中统称为file-like Object。除了file外,还可以是内存的字节流,网络流,自定义流等等。file-like Object不要求从特定类继承,只要写个read()
方法就行。StringIO
就是在内存中创建的file-like Object,常用作临时缓冲。
二进制文件
之前默认都是读取文本文件,并且是UTF-8编码的文本文件。要读取二进制文件,比如图片、视频等等,用'rb'
模式打开文件即可:
1 | '/Users/michael/test.jpg', 'rb') f = open( |
字符编码
读取非UTF-8编码的文本文件,需要给open()
函数传入encoding
参数,例如,读取GBK编码的文件
1 | '/Users/michael/gbk.txt', 'r', encoding='gbk') f = open( |
写文件
写文件和读文件是一样的,唯一区别是调用open()
函数时,传入标识符'w'
或者'wb'
表示写文本文件或写二进制文件:
1 | '/Users/michael/test.txt', 'w') f = open( |
StringIO和BytesIO
StringIO是file-like object,其是在内存中读写str,要把str写入StringIO,我们需要先创建一个StringIO:
1 | from io import StringIO |
要想读取StringIO,可以用一个str初始化StringIO,然后axing读文件一样读取:
1 | from io import StringIO |
StringIO操作的只能是str,如果要操作二进制数据,就需要使用BytesIO。
1 | from io import BytesIO |
操作文件和目录
Python内置的os
模块也可以直接调用操作系统提供的接口函数。
1 | import os |
操作系统和目录的函数一部分放在os
模块中,一部分放在os.path
模块中。
1 | # 查看当前目录的绝对路径: |
把两个路径合成一个时,不要直接拼字符串,而要通过os.path.join()
函数,这样可以正确处理不同操作系统的路径分隔符。同样的道理,要拆分路径时,也不要直接去拆字符串,而要通过os.path.split()
函数。
对于文件操作,使用以下函数:
1 | # 对文件重命名: |
但是复制文件的函数在os
中不存在。原因是复制文件并非由操作系统提供的系统调用。在shutil
模块提供了copyfile()
的函数,你还可以在shutil
模块中找到很多实用函数,它们可以看做是os
模块的补充。
1 | import os |
序列化
在程序运行的过程中,所有的变量都是在内存中,例如
1 | d = dict(name='Bob',age=20,score=80) |
可以随时修改变量,比如把name
改成'Bill'
,但是一旦程序结束,变量所占用的内存就被操作系统全部回收。如果没有把修改后的'Bill'
存储到磁盘上,下次重新运行程序,变量又被初始化为'Bob'
。
我们把变量从内存中变成可存储或传输的过程称之为序列化,在Python中叫pickling,在其他语言中也被称之为serialization,marshalling,flattening等等,都是一个意思。
把变量内容从序列化的对象重新读到内存里称之为反序列化,即unpickling。
1 | import pickle |
通过序列化和反序列化操作,可以在程序重新启动后,将变量内容重新加载,只是此时的变量和原来的变量完全不相干的对象,它们只是内容相同而已。
但是,pickle序列化存在的问题和别的编程语言特有的序列化问题一样,它只能作用于python,并且不同版本的python彼此都不兼容。因此,只能使用pickle保存不重要的数据,反序列化不成功也没关系。
JSON
如果我们要在不同的编程语言之间传递对象,就必须把对象序列化为标准格式,比如XML,但更好的方法是序列化为JSON,因为JSON表示出来就是一个字符串,可以被所有语言读取,也可以方便地存储到磁盘或者通过网络传输。JSON不仅是标准格式,并且比XML更快,而且可以直接在Web页面中读取,非常方便。
1 | import json |
JSON反序列化为python对象,使用loads()或者load()方法,前者把JSON的字符串反序列化,后者从file-like object中读取字符串并反序列化:
1 | '{"age": 20, "score": 88, "name": "Bob"}' json_str = |
python的dict可以直接序列化为JSON的{}
,但是,很多时候我们用class
表示对象,直接使用dumps()无法将closs
进行序列化。
1 | # 需要先将student实例转换为dict,然后再序列化 |
但是,按照这样的方法,如果遇到一个其他class,照样无法序列化为JSON,所以,可以把任意class的实例变为dict:
1 | lambda obj:obj.__dict__)) print(json.dumps(s,default= |
同样道理,如果需要将JSON反序列化为一个student对象实例,loads()方法先转换为一个dict对象,然后,传入object_hook函数负责把dict转换为student实例:
1 | def dict2stu(d): |
进程和线程
现代操作系统如linux、windows、Mac OS等,都支持“多任务”。
所谓的多任务,就是操作系统可以同时运行多个任务。单核CPU轮流让各个任务交替执行来实现“多任务”同时执行。
真正的并行执行多任务只能在多核CPU上实现,由于任务数远远多于CPU的核心数量,所以,操作系统也会自动把多个任务轮流调度到每个核心执行。
对于操作系统来说,每一个任务就是一个进程(process),而进程中的“子任务”称为线程(thread)
前面编写的所有的Python程序,都是执行单任务的进程,也就是只有一个线程。如果我们要同时执行多个任务怎么办?
1、启动多个进程,每个进程虽然只有一个线程,但多个进程可以一块执行多个任务。
2、种方法是启动一个进程,在一个进程内启动多个线程,这样,多个线程也可以一块执行多个任务。
3、启动多个进程,每个进程再启动多个线程,这样同时执行的任务就更多了,当然这种模型更复杂,实际很少采用。
多进程
Unix/Linux操作系统提供了一个fork()
系统调用,它非常特殊。普通的函数调用,调用一次,返回一次,但是fork()
调用一次,返回两次,因为操作系统自动把当前进程(称为父进程)复制了一份(称为子进程),然后,分别在父进程和子进程内返回。