Python高级
面向对象编程
以一个例子来说明面向过程和面向对象在程序流程上的不同之处
假设我们要处理学生的成绩表,表示学生的成绩,面向过程的程序可以用dict表示:
std1 = { 'name': 'Michael', 'score': 98 } std2 = { 'name': 'Bob', 'score': 81 }而处理学生成绩可以通过函数实现,比如打印学生的成绩:
def print_score(std): print('%s: %s' % (std['name'], std['score']))面向对象的程序设计思想,我们首选思考的不是程序的执行流程,而是
Student这种数据类型应该被视为一个对象,这个对象拥有name和score这两个属性(Property)。如果要打印一个学生的成绩,首先必须创建出这个学生对应的对象,然后,给对象发一个print_score消息,让对象自己把自己的数据打印出来class Student(object): def __init__(self, name, score): self.name = name self.score = score def print_score(self): print('%s: %s' % (self.name, self.score))给对象发消息实际上就是调用对象对应的关联函数,我们称之为对象的方法(Method)。面向对象的程序写出来就像这样:
bart = Student('Bart Simpson', 59) lisa = Student('Lisa Simpson', 87) bart.print_score() lisa.print_score()
类和实例
类是抽象的模板,实例是类创建的一个个具体的“对象”
和普通的函数相比,在类中定义的函数只有一点不同,就是第一个参数永远是实例变量
self,并且,调用时,不用传递该参数定义类是通过
class关键字:class Student(object): passclass后面紧接着是类名,即Student,类名通常是大写开头的单词,紧接着是(object),表示该类是从哪个类继承下来的,通常,没有合适的继承类,就使用object类,是所有类最终都会继承的类由于类可以起到模板的作用,因此,在创建实例的时候,把我们认为必须绑定的属性强制填写进去。通过定义一个特殊的
__init__方法,在创建实例的时候,就把name,score等属性绑上去:class Student(object): def __init__(self, name, score): self.name = name self.score = score注意
__init__方法第一个参数永远是self,表示创建的实例本身,因此,在__init__方法内部,就可以把各种属性绑定到self,self就指向创建的实例本身有
__init__方法,创建实例时候,就不能传入空的参数了,必须传入与__init__方法匹配的参数,但self不需要传,Python解释器自己会把实例变量传进去:>>> bart = Student('Bart Simpson', 59) >>> bart.name 'Bart Simpson' >>> bart.score 59
访问限制
如果要让内部属性不被外部访问,可以把属性的名称前加上两个下划线
__class Student(object): def __init__(self, name, score): self.__name = name self.__score = score def print_score(self): print('%s: %s' % (self.__name, self.__score))>>> bart = Student('Bart Simpson', 59) >>> bart.__name Traceback (most recent call last): File "<stdin>", line 1, in <module> AttributeError: 'Student' object has no attribute '__name'
外部代码要获取name和score,给Student类增加
get_name和get_score的方法:class Student(object): ... def get_name(self): return self.__name def get_score(self): return self.__score要允许外部代码修改score怎么办?可以再给Student类增加
set_score方法:class Student(object): ... def set_score(self, score): self.__score = score双下划线开头的实例变量是不是一定不能从外部访问呢?其实也不是。不能直接访问
__name是因为Python解释器对外把__name变量改成了_Student__name,所以,仍然可以通过_Student__name来访问__name变量:>>> bart._Student__name 'Bart Simpson'但是强烈建议你不要这么干,因为不同版本的Python解释器可能会把
__name改成不同的变量名注意下面的这种错误写法:
>>> bart = Student('Bart Simpson', 59) >>> bart.get_name() 'Bart Simpson' >>> bart.__name = 'New Name' # 设置__name变量! >>> bart.__name 'New Name'表面上,外部代码“成功”地设置了
__name变量,但实际上这个__name变量和class内部的__name变量不是一个变量!内部的__name变量已经被Python解释器自动改成了_Student__name,而外部代码给bart新增了一个__name变量。不信试试:>>> bart.get_name() # get_name()内部返回self.__name 'Bart Simpson'
继承和多态
继承:
- 最大的好处是子类获得了父类的全部功能
- 第二个好处需要我们对代码做一点改进,子类的
run()可覆盖父类的run() - 如果一个实例的数据类型是某个子类,那它的数据类型也可以被看做是父类
多态
- 当我们需要传入
Dog、Cat、Tortoise……时,我们只需要接收Animal类型就可以了,因为Dog、Cat、Tortoise……都是Animal类型,然后,按照Animal类型进行操作即可。由于Animal类型有run()方法,因此,传入的任意类型,只要是Animal类或者子类,就会自动调用实际类型的run()方法
- 当我们需要传入
静态语言VS动态语言
静态语言(例如Java)来说,如果需要传入
Animal类型,则传入的对象必须是Animal类型或者它的子类,否则,将无法调用run()方法Python这样的动态语言来说,则不一定需要传入
Animal类型。我们只需要保证传入的对象有一个run()方法就可以了:class Timer(object): def run(self): print('Start...')这就是动态语言的“鸭子类型”,它并不要求严格的继承体系,一个对象只要“看起来像鸭子,走起路来像鸭子”,那它就可以被看做是鸭子
获得对象信息
当我们拿到一个对象的引用时,如何知道这个对象是什么类型、有哪些方法呢?
判断对象类型,使用
type()函数基本类型都可以用
type()判断:>>> type(123) <class 'int'>一个变量指向函数或者类,也可以用
type()判断:>>> type(abs) <class 'builtin_function_or_method'>>>> type(123)==type(456) True >>> type(123)==int True如果要判断一个对象是否 是函数怎么办?可以使用
types模块中定义的常量:>>> import types >>> def fn(): ... pass ... >>> type(fn)==types.FunctionType True >>> type(abs)==types.BuiltinFunctionType True >>> type(lambda x: x)==types.LambdaType True
对class的继承关系来说,判断class的类型,使用
isinstance()函数回顾上次的例子,如果继承关系是:
object -> Animal -> Dog -> Husky先创建3种类型的对象:
>>> a = Animal() >>> d = Dog() >>> h = Husky()验证如下
>>> isinstance(d, Dog) and isinstance(d, Animal) True
类似
__xxx__的属性和方法在Python中都是有特殊用途的,比如__len__方法返回长度。在Python中,如果你调用len()函数试图获取一个对象的长度,实际上,在len()函数内部,它自动去调用该对象的__len__()方法我们自己写的类,如果也想用
len(myObj)的话,就自己写一个__len__()方法:>>> class MyDog(object): ... def __len__(self): ... return 100 ... >>> dog = MyDog() >>> len(dog) 100
要获得一个对象的所有属性和方法,可以使用
dir()函数,它返回一个包含字符串的list,比如,获得一个str对象的所有属性和方法:>>> dir('ABC') ['__add__', '__class__',..., '__subclasshook__', 'capitalize', 'casefold',..., '
实例属性和类属性
Python是动态语言,根据类创建的实例可以任意绑定属性
给实例绑定属性的方法是通过实例变量,或者通过
self变量:class Student(object): def __init__(self, name): self.name = name s = Student('Bob') s.score = 90
类本身需要绑定一个属性,直接class中定义属性,这是类属性,归
Student类所有:class Student(object): name = 'Student'当我们定义了一个类属性后,这个属性虽然归类所有,但类的所有实例都可以访问到
相关测试如下
>>> class Student(object): ... name = 'Student' ... >>> s = Student() # 创建实例s >>> print(s.name) # 打印name属性,因为实例并没有name属性,所以会继续查找class的name属性 Student >>> print(Student.name) # 打印类的name属性 Student >>> s.name = 'Michael' # 给实例绑定name属性 >>> print(s.name) # 由于实例属性优先级比类属性高,因此,它会屏蔽掉类的name属性 Michael >>> print(Student.name) # 但是类属性并未消失,用Student.name仍然可以访问 Student >>> del s.name # 如果删除实例的name属性 >>> print(s.name) # 再次调用s.name,由于实例的name属性没有找到,类的name属性就显示出来了 Student
面向对象高级编程
使用
__slots__正常情况下,当我们定义了一个
class,创建了一个class的实例后,我们可以***给该实例绑定任何属性和方法***,这就是动态语言的灵活性先定义
class:class Student(object): pass给实例绑定一个属性:
>>> s = Student() >>> s.name = 'Michael' # 动态给实例绑定一个属性 >>> print(s.name) Michael给实例绑定一个方法:
>>> def set_age(self, age): # 定义一个函数作为实例方法 ... self.age = age ... >>> from types import MethodType >>> s.set_age = MethodType(set_age, s) # 给实例绑定一个方法 >>> s.set_age(25) # 调用实例方法 >>> s.age # 测试结果 25但是,给一个实例绑定的方法,对另一个实例是不起作用的:
>>> s2 = Student() # 创建新的实例 >>> s2.set_age(25) # 尝试调用方法 Traceback (most recent call last): File "<stdin>", line 1, in <module> AttributeError: 'Student' object has no attribute 'set_age为了给所有实例都绑定方法,可以给
class绑定方法:>>> def set_score(self, score): ... self.score = score ... >>> Student.set_score = set_score给
class绑定方法后,所有实例均可调用:>>> s.set_score(100) >>> s.score 100 >>> s2.set_score(99) >>> s2.score 99通常情况下,上面的
set_score方法可以直接定义在class中,但动态绑定允许我们在程序运行的过程中动态给class加上功能,这在静态语言中很难实现
**想要限制实例的属性**怎么办?比如:只允许对Student实例添加
name和age属性。达到限制,Python允许在定义
class的时候,定义一个特殊的__slots__变量限制该
class实例能添加的属性:class Student(object): __slots__ = ('name', 'age') # 用tuple定义允许绑定的属性名称由于
'score'没有被放到__slots__中,所以不能绑定score属性,试图绑定score将得到AttributeError的错误。>>> s = Student() # 创建新的实例 >>> s.name = 'Michael' # 绑定属性'name' >>> s.age = 25 # 绑定属性'age' >>> s.score = 99 # 绑定属性'score' Traceback (most recent call last): File "<stdin>", line 1, in <module> AttributeError: 'Student' object has no attribute 'score'使用
__slots__要注意,__slots__定义的属性仅对当前类实例起作用,对继承的子类是不起作用的:>>> class GraduateStudent(Student): ... pass ... >>> g = GraduateStudent() >>> g.score = 9999
使用@property
通常情况,绑定属性时,如果我们直接把属性暴露出去,虽然写起来很简单,但是,没办法检查参数
s = Student() s.score = 9999不合逻辑。为限制
score,可以通过set_score()设置成绩,再通过get_score()来获取成绩,这样,在set_score()方法里,就可以检查参数class Student(object): def get_score(self): return self._score def set_score(self, value): if not isinstance(value, int): raise ValueError('score must be an integer!') if value < 0 or value > 100: raise ValueError('score must between 0 ~ 100!') self._score = value现在,对任意的
Student实例进行操作,就不能随心所欲地设置score>>> s = Student() >>> s.set_score(60) # ok! >>> s.get_score() 60 >>> s.set_score(9999) Traceback (most recent call last): ... ValueError: score must between 0 ~ 100!
有没有既能检查参数,又可以用类似属性这样简单的方式来访问类的变量?
对类的方法,装饰器起作用。Python内置
@property装饰器负责把方法变属性调用把一个getter方法变成属性,只需要加上
@property就可以了,此时,@property本身又创建了另一个装饰器@score.setter,负责把一个setter方法变成属性赋值,于是,我们就拥有一个可控的属性操作class Student(object): @property def score(self): return self._score @score.setter def score(self, value): if not isinstance(value, int): raise ValueError('score must be an integer!') if value < 0 or value > 100: raise ValueError('score must between 0 ~ 100!') self._score = value>>> s = Student() >>> s.score = 60 # OK,实际转化为s.score(60) >>> s.score # OK,实际转化为s.score() 60 >>> s.score = 9999 Traceback (most recent call last): ... ValueError: score must between 0 ~ 100!注意
@property,在对实例属性操作时,是通过getter和setter方法来实现可以定义只读属性,只定义getter方法,不定义setter方法就是一个只读属性
class Student(object): @property def birth(self): return self._birth @birth.setter def birth(self, value): self._birth = value @property def age(self): return 2015 - self._birth上面的
birth是可读写属性,而age就是一个只读属性,因为age可以根据birth和当前时间计算出来要特别注意:属性的方法名不要和实例变量重名。例如,以下的代码是错误的:
class Student(object): # 方法名称和实例变量均为birth: @property def birth(self): return self.birth因调用
s.birth,先转换方法调用,在执行return self.birth,又视访问self属性,又转换调用self.birth(),造成无限递归,导致栈溢出RecursionError
多重继承
通常情况分析,
Animal类层次的设计,假设我们要实现以下4种动物:- Dog - 狗狗;
- Bat - 蝙蝠;
- Parrot - 鹦鹉;
- Ostrich - 鸵鸟。
如果按照哺乳动物和鸟类归类,我们可以设计出这样的类的层次:
┌───────────────┐ │ Animal │ └───────────────┘ │ ┌────────────┴────────────┐ │ │ ▼ ▼ ┌─────────────┐ ┌─────────────┐ │ Mammal │ │ Bird │ └─────────────┘ └─────────────┘ │ │ ┌─────┴──────┐ ┌─────┴──────┐ │ │ │ │ ▼ ▼ ▼ ▼ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ Dog │ │ Bat │ │ Parrot │ │ Ostrich │ └─────────┘ └─────────┘ └─────────┘ └─────────┘是如果按照“能跑”和“能飞”来归类,我们就应该设计出这样的类的层次:
┌───────────────┐ │ Animal │ └───────────────┘ │ ┌────────────┴────────────┐ │ │ ▼ ▼ ┌─────────────┐ ┌─────────────┐ │ Runnable │ │ Flyable │ └─────────────┘ └─────────────┘ │ │ ┌─────┴──────┐ ┌─────┴──────┐ │ │ │ │ ▼ ▼ ▼ ▼ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ Dog │ │ Ostrich │ │ Parrot │ │ Bat │ └─────────┘ └─────────┘ └─────────┘ └─────────┘如果要把上面的两种分类都包含进来,我们就得设计更多的层次:
- 哺乳类:能跑的哺乳类,能飞的哺乳类;
- 鸟类:能跑的鸟类,能飞的鸟类。
这么一来,类的层次就复杂了:
┌───────────────┐ │ Animal │ └───────────────┘ │ ┌────────────┴────────────┐ │ │ ▼ ▼ ┌─────────────┐ ┌─────────────┐ │ Mammal │ │ Bird │ └─────────────┘ └─────────────┘ │ │ ┌─────┴──────┐ ┌─────┴──────┐ │ │ │ │ ▼ ▼ ▼ ▼ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ MRun │ │ MFly │ │ BRun │ │ BFly │ └─────────┘ └─────────┘ └─────────┘ └─────────┘ │ │ │ │ │ │ │ │ ▼ ▼ ▼ ▼ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ Dog │ │ Bat │ │ Ostrich │ │ Parrot │ └─────────┘ └─────────┘ └─────────┘ └─────────┘正确的做法是采用多重继承。首先,主要的类层次仍按照哺乳类和鸟类设计:
class Animal(object): pass # 大类: class Mammal(Animal): pass class Bird(Animal): pass # 各种动物: class Dog(Mammal): pass class Bat(Mammal): pass class Parrot(Bird): pass class Ostrich(Bird): pass我们要给动物再加上
Runnable和Flyable的功能class Runnable(object): def run(self): print('Running...') class Flyable(object): def fly(self): print('Flying...')对于需要
Runnable功能的动物,就多继承一个Runnable,例如Dog:class Dog(Mammal, Runnable): pass对于需要
Flyable功能的动物,就多继承一个Flyable,例如Bat:class Bat(Mammal, Flyable): pass
Mixln
定义:设计类的继承关系时,通常,主线都是单一继承下来的,例如,
Ostrich继承自Bird。但是,如果需要“混入”额外的功能,通过多重继承就可以实现,比如,让Ostrich除了继承自Bird外,再同时继承Runnable目的:给一个类增加多个功能,在设计类的时候,优先考虑通过多重继承来组合多个MixIn的功能,而不是设计多层次的复杂的继承关系
为了更好地看出继承关系,我们把
Runnable和Flyable改为RunnableMixIn和FlyableMixIn。类似的,你还可以定义出肉食动物CarnivorousMixIn和植食动物HerbivoresMixIn,让某个动物同时拥有好几个MixIn:class Dog(Mammal, RunnableMixIn, CarnivorousMixIn): pass
定制类
__str__先定义一个
Student类,打印一个实例:>>> class Student(object): ... def __init__(self, name): ... self.name = name ... >>> print(Student('Michael')) <__main__.Student object at 0x109afb190>只需要定义好
__str__()方法,返回一个好看的字符串就可以了:>>> class Student(object): ... def __init__(self, name): ... self.name = name ... def __str__(self): ... return 'Student object (name: %s)' % self.name ... >>> print(Student('Michael')) Student object (name: Michael)细心的朋友会发现直接敲变量不用
print,打印出来的实例还是不好看:>>> s = Student('Michael') >>> s <__main__.Student object at 0x109afb310>因直接显示变量调用的不是
__str__(),而是__repr__(),两者区别是__str__()返回用户看到的字符串,__repr__()返回程序开发者看到的字符串,__repr__()是为调试服务的。解决办法是再定义一个
__repr__()。但是通常__str__()和__repr__()代码都是一样的,所以,有个偷懒的写法:class Student(object): def __init__(self, name): self.name = name def __str__(self): return 'Student object (name=%s)' % self.name __repr__ = __str__
__iter__如果一个类想被用于
for ... in循环,类似list或tuple那样,就必须实现一个__iter__()方法,该方法返回一个迭代对象,然后,Python的for循环就会不断调用该迭代对象的__next__()方法拿到循环的下一个值,直到遇到StopIteration错误时退出循环以斐波那契数列为例,写一个Fib类,可以作用于for循环:
class Fib(object): def __init__(self): self.a, self.b = 0, 1 # 初始化两个计数器a,b def __iter__(self): return self # 实例本身就是迭代对象,故返回自己 def __next__(self): self.a, self.b = self.b, self.a + self.b # 计算下一个值 if self.a > 100000: # 退出循环的条件 raise StopIteration() return self.a # 返回下一个值Fib实例作用于for循环:
>>> for n in Fib(): ... print(n) ... 1 1 2 3 5 ... 46368 75025
__getitem__Fib实例虽能作用for循环,看起来和list像,但是,把它当成list来使用还是不行
比如,取第5个元素:
>>> Fib()[5] TypeError: 'Fib' object does not support indexing表现得像list那样按照下标取出元素,需要实现
__getitem__()方法:class Fib(object): def __getitem__(self, n): a, b = 1, 1 for x in range(n): a, b = b, a + b return a现在,就可以按下标访问数列的任意一项了:
>>> f = Fib() >>> f[0] 1 >>> f[1] 1 >>> f[2] 2 >>> f[3] 3 >>> f[10] 89 >>> f[100] 573147844013817084101但list有个神奇的切片方法:
>>> list(range(100))[5:10] [5, 6, 7, 8, 9]对于Fib报错。因是
__getitem__()传入参数可是int,也可是切片对象slice:class Fib(object): def __getitem__(self, n): if isinstance(n, int): # n是索引 a, b = 1, 1 for x in range(n): a, b = b, a + b return a if isinstance(n, slice): # n是切片 start = n.start stop = n.stop if start is None: start = 0 a, b = 1, 1 L = [] for x in range(stop): if x >= start: L.append(a) a, b = b, a + b return L现在试试Fib的切片:
>>> f = Fib() >>> f[0:5] [1, 1, 2, 3, 5] >>> f[:10] [1, 1, 2, 3, 5, 8, 13, 21, 34, 55]
__getattr__正常情况下,调用类的方法或属性时,如不存在,就报错。比如定义
Student类:class Student(object): def __init__(self): self.name = 'Michael'调用
name属性,没问题,但是,调用不存在的score属性,就有问题了:>>> s = Student() >>> print(s.name) Michael >>> print(s.score) Traceback (most recent call last): AttributeError: 'Student' object has no attribute 'score'避免这个错误,除了可以加上一个
score属性外,Python还有另一个机制,那就是写一个__getattr__()方法,动态返回一个属性。修改如下:class Student(object): def __init__(self): self.name = 'Michael' def __getattr__(self, attr): if attr=='score': return 99当调用不存在属性,比如
score,Python解释器会试图调用__getattr__(self, 'score')来尝试获得属性,这样,我们就有机会返回score的值:>>> s = Student() >>> s.name 'Michael' >>> s.score 99返回函数也是完全可以的:
class Student(object): def __getattr__(self, attr): if attr=='age': return lambda: 25只是调用方式要变为:
>>> s.age() 25注意到任意调用如
s.abc都会返回None,这是因为我们定义的__getattr__默认返回就是None。要让class只响应特定的几个属性,我们就要按照约定,抛出AttributeError的错误:class Student(object): def __getattr__(self, attr): if attr=='age': return lambda: 25 raise AttributeError('\'Student\' object has no attribute \'%s\'' % attr)只有在没有找到属性的情况下,才调用
__getattr__使用举例
很多网站都搞REST API,比如新浪微博、豆瓣啥的,调用API的URL类似:
要写SDK,给每个URL对应的API都写一个方法,那得累死,而且,API一旦改动,SDK也要改。
利用完全动态的
__getattr__,我们可以写出一个链式调用:class Chain(object): def __init__(self, path=''): self._path = path def __getattr__(self, path): return Chain('%s/%s' % (self._path, path)) def __str__(self): return self._path __repr__ = __str__>>> Chain().status.user.timeline.list '/status/user/timeline/list'
__call__实例有自己的属性和方法,当我们调用实例方法,可
instance.method()调用任何类,只需要定义一个
__call__()方法,就可以直接对实例进行调用- 请看示例:
class Student(object): def __init__(self, name): self.name = name def __call__(self): print('My name is %s.' % self.name)调用方式如下:
>>> s = Student('Michael') >>> s() # self参数不要传入 My name is Michael.
枚举类
需要更精确地控制枚举类型,可以从
Enum派生出自定义类:from enum import Enum, unique @unique class Weekday(Enum): Sun = 0 # Sun的value被设定为0 Mon = 1 Tue = 2 Wed = 3 Thu = 4 Fri = 5 Sat = 6@unique装饰器可以帮助我们检查保证没有重复值。访问这些枚举类型可以有若干种方法:
>>> day1 = Weekday.Mon >>> print(day1) Weekday.Mon >>> print(Weekday.Tue) Weekday.Tue >>> print(Weekday['Tue']) Weekday.Tue >>> print(Weekday.Tue.value) 2 >>> print(day1 == Weekday.Mon) True >>> print(day1 == Weekday.Tue) False >>> print(Weekday(1)) Weekday.Mon >>> print(day1 == Weekday(1)) True >>> Weekday(7) Traceback (most recent call last): ... ValueError: 7 is not a valid Weekday >>> for name, member in Weekday.__members__.items(): ... print(name, '=>', member) ... Sun => Weekday.Sun Mon => Weekday.Mon Tue => Weekday.Tue Wed => Weekday.Wed Thu => Weekday.Thu Fri => Weekday.Fri Sat => Weekday.Sat可见,既可以用成员名称引用枚举常量,又可以直接根据value的值获得枚举常量
使用元类(还没有记录)
错误调试和测试
错误:
- 程序编写有问题造成:比如本来应该输出整数结果输出了字符串,这种错误我们通常称之为bug,bug是必须修复的。
- 有的错误是用户输入造成:比如让用户输入email地址,结果得到一个空字符串,这种错误可以通过检查用户输入来做相应的处理
- 还有错误是完全无法在程序运行过程中预测:比如写入文件的时候,磁盘满了,写不进去了,这类错误也称为异常,在程序中通常是必须处理的
错误处理
程序运行的过程中,如果发生了错误,可以事先约定返回一个错误代码,在操作系统提供的调用中,返回错误码非常常见。比如打开文件的函数
open(),成功时返回文件描述符(就是一个整数),出错时返回-1try机制try: print('try...') r = 10 / 0 print('result:', r) except ZeroDivisionError as e: print('except:', e) finally: print('finally...') print('END')当认为某些代码可能会出错,可以用
try来运行这段代码,如果执行出错,则后续代码不会继续执行,而是直接跳转至错误处理代码,即except语句块,执行完except后,如果有finally语句块,则执行finally语句块,至此,执行完毕try... except: division by zero finally... END此外,如果没有错误发生,可以在
except语句块后面加一个else,当没有错误发生时,会自动执行else语句:try: print('try...') r = 10 / int('2') print('result:', r) except ValueError as e: print('ValueError:', e) except ZeroDivisionError as e: print('ZeroDivisionError:', e) else: print('no error!') finally: print('finally...') print('END')Python的错误其实也是class,所有的错误类型都继承自
BaseException,所以使用except时需要注意,它不但捕获该类型错误,还把其子类也“一网打尽”。比如:try: foo() except ValueError as e: print('ValueError') except UnicodeError as e: print('UnicodeError')第二个
except永远也捕获不到UnicodeError,因为UnicodeError是ValueError的子类,如果有,也被第一个except给捕获了Python所有错误都从
BaseException类派生,常见错误类型和继承关系:https://docs.python.org/3/library/exceptions.html#exception-hierarchy
try...except捕获错误还有一个巨大的好处,可以跨越多层调用比如函数
main()调用bar(),bar()调用foo(),结果foo()出错了,这时,只要main()捕获到了,就可以处理:def foo(s): return 10 / int(s) def bar(s): return foo(s) * 2 def main(): try: bar('0') except Exception as e: print('Error:', e) finally: print('finally...')不需要在每个可能出错的地方去捕获错误,只要在合适的层次去捕获错误就可以了。这样一来,就大大减少了写
try...except...finally的麻烦
调用栈机制
如果错误没有被捕获,会一直往上抛,最后被Python解释器捕获,打印一个错误信息,然后程序退出。来看看
err.py:# err.py: def foo(s): return 10 / int(s) def bar(s): return foo(s) * 2 def main(): bar('0') main()执行,结果如下:每个调用错误都显示,最后才有结果
$ python3 err.py Traceback (most recent call last): File "err.py", line 11, in <module> main() File "err.py", line 9, in main bar('0') File "err.py", line 6, in bar return foo(s) * 2 File "err.py", line 3, in foo return 10 / int(s) ZeroDivisionError: division by zero出错并不可怕,可怕的是不知道哪里出错了。解读错误信息是定位错误的关键。
记录错误
不捕获错误,自然可以让Python解释器来打印出错误堆栈,但程序也被结束
既然能捕获错误,就可把错误堆栈打印,然后分析错误原因,让程序继续执行下去
Python内置的
logging模块可以非常容易地记录错误信息:import logging def foo(s): return 10 / int(s) def bar(s): return foo(s) * 2 def main(): try: bar('0') except Exception as e: logging.exception(e) main() print('END')同样是出错,但程序打印完错误信息后会继续执行,并正常退出:
$ python3 err_logging.py ERROR:root:division by zero Traceback (most recent call last): File "err_logging.py", line 13, in main bar('0') File "err_logging.py", line 9, in bar return foo(s) * 2 File "err_logging.py", line 6, in foo return 10 / int(s) ZeroDivisionError: division by zero END通过配置,
logging还可以把错误记录到日志文件里,方便事后排查
raise抛出错误:自己编写函数作为也可以抛出错误首先根据需要,可以定义一个错误的class,选择好继承关系,然后,用
raise语句抛出一个错误的实例:class FooError(ValueError): pass def foo(s): n = int(s) if n==0: raise FooError('invalid value: %s' % s) return 10 / n foo('0')执行,可以最后跟踪到我们自己定义的错误:
$ python3 err_raise.py Traceback (most recent call last): File "err_throw.py", line 11, in <module> foo('0') File "err_throw.py", line 8, in foo raise FooError('invalid value: %s' % s) __main__.FooError: invalid value: 0另一种错误处理的方式
def foo(s): n = int(s) if n==0: raise ValueError('invalid value: %s' % s) return 10 / n def bar(): try: foo('0') except ValueError as e: print('ValueError!') raise bar()在
bar()函数中,我们明明已经捕获了错误,但是,打印一个ValueError!后,又把错误通过raise语句抛出去捕获错误目的只是记录一下,便于后续追踪。但是,由于当前函数不知道应该怎么处理该错误,所以,最恰当的方式是继续往上抛,让顶层调用者去处理
ValueError! Traceback (most recent call last): File "script.py", line 14, in <module> bar() File "script.py", line 9, in bar foo('0') File "script.py", line 4, in foo raise ValueError('invalid value: %s' % s) ValueError: invalid value: 0 Exited with error status 1raise语句不带参数,会把当前错误原样抛出。此外,except中raise一个Error,还可以把一种类型的错误转化成另一种类型:try: 10 / 0 except ZeroDivisionError: raise ValueError('input error!')只要是合理的转换逻辑就可以,但是,决不应该把一个
IOError转换成毫不相干的ValueError
若忽略
raise运行结果如下ValueError!
调试
第一种简单直接,用
print()把可能有问题的变量打印出来def foo(s): n = int(s) print('>>> n = %d' % n) return 10 / n def main(): foo('0') main()$ python err.py >>> n = 0 Traceback (most recent call last): ... ZeroDivisionError: integer division or modulo by zeroprint()最大的坏处是将来还得删掉它,想想程序里到处都是print(),运行结果也会包含很多垃圾信息断言
assert用
print()来辅助查看的地方,都可以用断言(assert)来替代:def foo(s): n = int(s) assert n != 0, 'n is zero!' return 10 / n def main(): foo('0')assert的意思是,表达式n != 0应该是True,否则,根据程序运行的逻辑,后面的代码肯定会出错如果断言失败,
assert语句本身就会抛出AssertionError程序中到处充斥
assert,启动Python解释器时可以用-O参数来关闭assert:$ python -O err.py Traceback (most recent call last): ... ZeroDivisionError: division by zero断言的开关“-O”是英文大写字母O,不是数字0。
关闭后,你可以把所有的
assert语句当成pass来看
logginglogging不会抛出错误,而且可以输出到文件import logging logging.basicConfig(level=logging.INFO) s = '0' n = int(s) logging.info('n = %d' % n) print(10 / n)$ python err.py INFO:root:n = 0 Traceback (most recent call last): File "err.py", line 8, in <module> print(10 / n) ZeroDivisionError: division by zerologging好处,允许指定记录信息的级别,有debug,info,warning,error等几个级别,当指定level=INFO时,logging.debug就不起作用了。同理,指定level=WARNING后,debug和info就不起作用了。这样一来,可以放心地输出不同级别的信息,也不用删除,最后统一控制输出哪个级别的信息
pdb启动Python的调试器pdb,让程序以单步方式运行,可以随时查看运行状态
# err.py s = '0' n = int(s) print(10 / n)$ python -m pdb err.py > /Users/michael/Github/learn-python3/samples/debug/err.py(2)<module>() -> s = '0'参数
-m pdb启动后,pdb定位下一步要执行的-> s = '0'。输入命令l来查看(Pdb) l 1 # err.py 2 -> s = '0' 3 n = int(s) 4 print(10 / n)输入命令
n可以单步执行代码:(Pdb) n > /Users/michael/Github/learn-python3/samples/debug/err.py(3)<module>() -> n = int(s) (Pdb) n > /Users/michael/Github/learn-python3/samples/debug/err.py(4)<module>() -> print(10 / n)任何时候都可以输入命令
p 变量名来查看变量:(Pdb) p s '0' (Pdb) p n 0输入命令
q结束调试,退出程序:(Pdb) q
单元测试
测试驱动的开发模式的好处是确保程序模块的行为符合我们设计的测试用例
举例
mydict.py代码如下:class Dict(dict): def __init__(self, **kw): super().__init__(**kw) def __getattr__(self, key): try: return self[key] except KeyError: raise AttributeError(r"'Dict' object has no attribute '%s'" % key) def __setattr__(self, key, value): self[key] = value为了编写单元测试,我们需要引入Python自带的
unittest模块,编写mydict_test.py如下:import unittest from mydict import Dict class TestDict(unittest.TestCase): def test_init(self): d = Dict(a=1, b='test') self.assertEqual(d.a, 1) self.assertEqual(d.b, 'test') self.assertTrue(isinstance(d, dict)) def test_key(self): d = Dict() d['key'] = 'value' self.assertEqual(d.key, 'value') def test_attr(self): d = Dict() d.key = 'value' self.assertTrue('key' in d) self.assertEqual(d['key'], 'value') def test_keyerror(self): d = Dict() with self.assertRaises(KeyError): value = d['empty'] def test_attrerror(self): d = Dict() with self.assertRaises(AttributeError): value = d.empty解释
编写单元测试时,我们需要编写一个测试类,从
unittest.TestCase继承。以
test开头的方法就是测试方法,不以test开头的方法不被认为是测试方法,测试的时候不会被执行。对每一类测试都需要编写一个
test_xxx()方法。由于unittest.TestCase提供了很多内置的条件判断,我们只需要调用这些方法就可以断言输出是否是我们所期望的。最常用的断言就是assertEqual():self.assertEqual(abs(-1), 1) # 断言函数返回的结果与1相等另一种重要的断言就是期待抛出指定类型的Error,比如通过
d['empty']访问不存在的key时,断言会抛出KeyError:with self.assertRaises(KeyError): value = d['empty']运行单元测试
- 最简单的运行方式是在
mydict_test.py的最后加上两行代码:
if __name__ == '__main__': unittest.main()就可以把
mydict_test.py当做正常的python脚本运行:$ python mydict_test.py- 另一种方法是在命令行通过参数
-m unittest直接运行单元测试:
$ python -m unittest mydict_test ..... ---------------------------------------------------------------------- Ran 5 tests in 0.000s OK这是推荐的做法,因为这样可以一次批量运行很多单元测试,并且,有很多工具可以自动来运行这些单元测试
- 最简单的运行方式是在
setUp()和tearDown()这两个方法会分别在每调用一个测试方法的前后分别被执行
设想你的测试需要启动一个数据库,这时,就可以在
setUp()方法中连接数据库,在tearDown()方法中关闭数据库,这样,不必在每个测试方法中重复相同的代码:class TestDict(unittest.TestCase): def setUp(self): print('setUp...') def tearDown(self): print('tearDown...')再次运行看看每个测试方法调用前后是否会打印出
setUp...和tearDown...
文档测试
Python内置的“文档测试”(doctest)模块可以直接提取注释中的代码并执行测试
# mydict2.py class Dict(dict): ''' Simple dict but also support access as x.y style. >>> d1 = Dict() >>> d1['x'] = 100 >>> d1.x 100 >>> d1.y = 200 >>> d1['y'] 200 >>> d2 = Dict(a=1, b=2, c='3') >>> d2.c '3' >>> d2['empty'] Traceback (most recent call last): ... KeyError: 'empty' >>> d2.empty Traceback (most recent call last): ... AttributeError: 'Dict' object has no attribute 'empty' ''' def __init__(self, **kw): super(Dict, self).__init__(**kw) def __getattr__(self, key): try: return self[key] except KeyError: raise AttributeError(r"'Dict' object has no attribute '%s'" % key) def __setattr__(self, key, value): self[key] = value if __name__=='__main__': import doctest doctest.testmod()运行
python mydict2.py:$ python mydict2.py什么输出也没有。这说明我们编写的doctest运行都是正确的。如果程序有问题,比如把
__getattr__()方法注释掉,再运行就会报错:$ python mydict2.py ********************************************************************** File "/Users/michael/Github/learn-python3/samples/debug/mydict2.py", line 10, in __main__.Dict Failed example: d1.x Exception raised: Traceback (most recent call last): ... AttributeError: 'Dict' object has no attribute 'x' ********************************************************************** File "/Users/michael/Github/learn-python3/samples/debug/mydict2.py", line 16, in __main__.Dict Failed example: d2.c Exception raised: Traceback (most recent call last): ... AttributeError: 'Dict' object has no attribute 'c' ********************************************************************** 1 items had failures: 2 of 9 in __main__.Dict ***Test Failed*** 2 failures.注意到最后3行代码。当模块正常导入时,
doctest不会被执行。只有在命令行直接运行时,才执行doctest。所以,不必担心doctest会在非测试环境下执行