Python3 面向对象编程笔记

第4章 异常捕获

在本章中将学习异常,特殊的错误对象只有在合理的时候才需要特别处理,将会学习:

  1. 如何找到异常出现的原因
  2. 遇到异常时如何恢复
  3. 如何以不同的方式处理不同的异常
  4. 遇到异常时如何清理
  5. 创建新的异常类型
  6. 在控制流中使用异常语法

抛出异常

本质上,异常只是一个对象,有很多不同的异常类,但是他们都继承子同一个异常类BaseException。一些常见的异常:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
In [1]: x = 5 / 0
---------------------------------------------------------------------------
ZeroDivisionError Traceback (most recent call last)
<ipython-input-1-663c7a933a87> in <module>()
----> 1 x = 5 / 0

ZeroDivisionError: division by zero

In [2]: lst = [1]

In [3]: print(lst[3])
---------------------------------------------------------------------------
IndexError Traceback (most recent call last)
<ipython-input-3-7a094246b9ab> in <module>()
----> 1 print(lst[3])

IndexError: list index out of range

In [4]: lst + 2
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
<ipython-input-4-ffe4e6e220bf> in <module>()
----> 1 lst + 2

TypeError: can only concatenate list (not "int") to list

In [5]: lst.add
---------------------------------------------------------------------------
AttributeError Traceback (most recent call last)
<ipython-input-5-50e4efd52cad> in <module>()
----> 1 lst.add

AttributeError: 'list' object has no attribute 'add'

In [6]: d = {'a':'b'}

In [7]: d['df']
---------------------------------------------------------------------------
KeyError Traceback (most recent call last)
<ipython-input-7-4b3d04097323> in <module>()
----> 1 d['df']

KeyError: 'df'

python中大部分的错误类都继承自 Exception(它又继承自 BaseException )

抛出一个异常

当我们的程序比如遇到不合法的输入时,需要抛出异常可以像下面这样,利用 raise

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
In [2]: class EvenOnly(list):
...:
...: def append(self, integer):
...: if not isinstance(integer, int):
...: raise TypeError("Only integers can be added")
...: if integer % 2:
...: raise ValueError("Only even numbers can be added")
...: super().append(integer)
...:

In [3]: e = EvenOnly()

In [4]: e.append("a string")
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
<ipython-input-4-9cff98c34dae> in <module>()
----> 1 e.append("a string")

<ipython-input-2-7af07cf6ebb7> in append(self, integer)
3 def append(self, integer):
4 if not isinstance(integer, int):
----> 5 raise TypeError("Only integers can be added")
6 if integer % 2:
7 raise ValueError("Only even numbers can be added")

TypeError: Only integers can be added

In [5]: e.append(3)
---------------------------------------------------------------------------
ValueError Traceback (most recent call last)
<ipython-input-5-d9ea7f347873> in <module>()
----> 1 e.append(3)

<ipython-input-2-7af07cf6ebb7> in append(self, integer)
5 raise TypeError("Only integers can be added")
6 if integer % 2:
----> 7 raise ValueError("Only even numbers can be added")
8 super().append(integer)
9

ValueError: Only even numbers can be added

异常的作用

当抛出一个异常时,该异常后面所有的代码都将不会执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

def no_return():
print("I am about to raise an exception")
raise Exception("This is always raised")
print("This line will never execute")


def call_exception():
print("call_exception starts here...")
no_return()
print("an exception was raised...")


if __name__ == "__main__":
call_exception()

# call_exception starts here...
# I am about to raise an exception
# Traceback(most recent call last):
# File "test.py", line 15, in < module >
# call_exception()
# File "test.py", line 10, in call_exception
# no_return()
# File "test.py", line 4, in no_return
# raise Exception("This is always raised")
# Exception: This is always raised

处理异常

当我们需要处理一个异常时,可以像下面这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
def funy_division2(anumber):
try:
if anumber == 13:
raise ValueError("13 is an unlucky number")
return 100 / anumber
except (ZeroDivisionError, TypeError):
return "Enter a number other than zero"


for val in (0, "hello", 50.0, 13):
# print end设定尾部的符号
print(f"Testiong {val}", end=" ")
print(funy_division2(val))

# Testiong 0 Enter a number other than zero
# Testiong hello Enter a number other than zero
# Testiong 50.0 2.0
# Testiong 13 Traceback(most recent call last):
# File "test.py", line 13, in < module >
# print(funy_division2(val))
# File "test.py", line 4, in funy_division2
# raise ValueError("13 is an unlucky number")
# ValueError: 13 is an unlucky number

我们知道了如何处理异常,但是想要对不同的异常作出不同的反应,或者想要针对某种异常执行某些操作之后传递给上层函数,就像从来没有处理过一样,解决办法分别就是利用exceptraise

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
def funy_division2(anumber):
try:
if anumber == 13:
raise ValueError("13 is an unlucky number")
return 100 / anumber
except ZeroDivisionError:
return "Enter a number other than zero"
except TypeError:
return "Enter a numberical value"
except ValueError:
print("No, No, not 13!")
raise


for val in (0, "hello", 50.0, 13):
# print end设定尾部的符号
print(f"Testiong {val}", end=" ")
print(funy_division2(val))

# Testiong 0 Enter a number other than zero
# Testiong hello Enter a numberical value
# Testiong 50.0 2.0
# Testiong 13 No, No, not 13!
# Traceback(most recent call last):
# File "test.py", line 18, in < module >
# print(funy_division2(val))
# File "test.py", line 4, in funy_division2
# raise ValueError("13 is an unlucky number")
# ValueError: 13 is an unlucky number

如果我们在捕获TypeError之前,捕获了Exception,那么就只有捕获Exception的代码执行。

利用上述的特性,我们可以在处理完一个特殊的异常后,最后统一用Exception捕获其余的异常。

通常和捕获异常使用的还有aselsefinally

finally下的代码无论在什么条件下都会执行,如果我们需要在代码执行完成后执行特定的任务将非常有用,一些常见的例子:

  1. 清楚打开的数据库连接
  2. 关闭打开的文件
  3. 向网络发送一次关闭握手

finally语句对于我们在try中执行return语句也非常重要,finally中的代码任然会在返回值之前执行

比如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def test():
try:
10 / 0
print('10/0')
except ZeroDivisionError:
print('0')
return 1
print('1')
finally:
print('finally exec')

test()

# 0
# finally exec

异常的层级

大部分的异常类都继承自Exception,但是除了下面这两个异常类:

  1. SystemExit,在程序自然退出时抛出,通常是在代码中调用了sys.exit函数,设计这个异常的目的是,在程序最终退出之前完成清理工作
  2. KeyboardInterrupt,常见于命令行程序,通常是ctrl + c

异常之间的层级关系:

第4章-异常层级

当我们用except:从句而不添加任何类型的异常时,将会捕获所有BaseException的子类,也就是会捕获所有异常

定义我们的异常

异常类的名字通常用于说明发生了什么错误,而且可以先初始化函数中传入任何参数来提供额外的信息

通常我们自定义的异常类继承Exception,而不是BaseException,因为BaseException无法被except Exception从句捕获

Exception.__init__方法设计成接受任意参数并将它们作为一个元组保存在一个名为args的属性当中,这使得我们可以更容易的定义新的异常,而不需要重写__init__方法

比如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class InvalidWithdrawal(Exception):
def __init__(self, balance, amount):
super().__init__(f"account doesn't have {amount}")
self.balance = balance
self.amount = amount

def overage(self):
return self.amount - self.balance


print(InvalidWithdrawal(25, 20).args)
# ("account doesn't have 20",)

raise InvalidWithdrawal(25, 20)
# Traceback(most recent call last):
# File "test.py", line 13, in < module >
# raise InvalidWithdrawal(25, 20)
# __main__.InvalidWithdrawal: account doesn't have 20

当我们需要处理异常时可以这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class InvalidWithdrawal(Exception):
def __init__(self, balance, amount):
super().__init__(f"account doesn't have {amount}")
self.balance = balance
self.amount = amount

def overage(self):
return self.amount - self.balance


try:
raise InvalidWithdrawal(25, 50)
except InvalidWithdrawal as e:
print(f"catch err {e.overage()}")

可以像对待其他对象一样对待异常类,可以为他添加属性和方法

使用自定义异常的真正优势在于创建供他人使用的框架、库或者API上

Python程序员倾向于追随“请求谅解,而不是许可”的原则,也就是说,他们先执行代码,然后解决错误。认为没有必要去花费cpu资源去检查一些很少才会出现的情况。比如下面的两段代码:

1
2
3
4
5
6
7
8
9
10
11
12
def divide_with_exception(number, divisor):
try:
number / divisor
except ZeroDivisionError:
print("You can't divide by zero")


def divide_with_if(number, divisor):
if divisor == 0:
print("You can't divide by zero")
else:
number / divisor

两段代码都是可以执行的,但是Python程序员应该更倾向于写第一种方式的代码

案例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
import hashlib


class User:

def __init__(self, username, password):
self.username = username
self.password = self._encrypt_pw(password)
self.is_logged_in = False

def _encrypt_pw(self, password):
hash_string = (self.username + password)
hash_string = hash_string.encode("utf-8")
return hashlib.sha256(hash_string).hexdigest()

def check_password(self, password):
encrypted = self._encrypt_pw(password)
return encrypted == self.password


class AuthException(Exception):

def __init__(self, username, user=None):
super().__init__(username, user)
self.username = username
self.user = user


class UsernameAlreadyExists(AuthException):
pass


class PasswordTooShort(AuthException):
pass


class Authenticator:

def __init__(self):
self.users = {}

def add_user(self, username, password):
if username in self.users:
raise UsernameAlreadyExists(username)
if len(password) < 6:
raise PasswordTooShort(username)
self.users[username] = User(username, password)

def login(self, username, password):
try:
user = self.users[username]
except KeyError:
raise InvalidUsername(username)

if not user.check_password(password):
raise InvalidPassword(username, user)

user.is_logged_in = True
return True

def is_logged_in(self, username):
if username in self.users:
return self.users[username].is_logged_in
return False


class InvalidUsername(AuthException):
pass


class InvalidPassword(AuthException):
pass


class Authorizor:

def __init__(self, authenticator):
self.authenticator = authenticator
self.permissions = {}

def add_permission(self, perm_name):
try:
perm_set = self.permissions[perm_name]
except KeyError:
self.permissions[perm_name] = set()
else:
raise PermissionError("Permission Exists")

def permit_user(self, perm_name, username):
try:
perm_set = self.permissions[perm_name]
except KeyError:
raise PermissionError("Permission does not exist")
else:
if username not in self.authenticator.users:
raise InvalidUsername(username)
perm_set.add(username)

def check_permission(self, perm_name, username):
if not self.authenticator.is_logged_in(username):
raise NotLoggedInError(username)
try:
perm_set = self.permissions[perm_name]
except KeyError:
raise PermissionError("Permission does not exist")
else:
if username not in perm_set:
raise NotPermittedError(username)
else:
return True


class PermissionError(Exception):
pass


class NotLoggedInError(AuthException):
pass


class NotPermittedError(AuthException):
pass


authenticator = Authenticator()
authorizor = Authorizor(authenticator)

authenticator.add_user("joe", "joepassword")

authorizor.add_permission("paint")

authorizor.check_permission("paint", "joe")
# Traceback (most recent call last):
# File "test.py", line 129, in <module>
# authorizor.check_permission("paint", "joe")
# File "test.py", line 101, in check_permission
# raise NotLoggedInError(username)
# __main__.NotLoggedInError: ('joe', None)

print(authenticator.is_logged_in("joe"))
# False

print(authenticator.login("joe", "joepassword"))

authorizor.check_permission("paint", "joe")
# Traceback (most recent call last):
# File "test.py", line 145, in <module>
# authorizor.check_permission("paint", "joe")
# File "test.py", line 108, in check_permission
# raise NotPermittedError(username)
# __main__.NotPermittedError: ('joe', None)

authorizor.check_permission("mix", "joe")
# Traceback (most recent call last):
# File "test.py", line 103, in check_permission
# perm_set = self.permissions[perm_name]
# KeyError: 'mix'

# During handling of the above exception, another exception occurred:

# Traceback (most recent call last):
# File "test.py", line 153, in <module>
# authorizor.check_permission("mix", "joe")
# File "test.py", line 105, in check_permission
# raise PermissionError("Permission does not exist")
# __main__.PermissionError: Permission does not exist

authorizor.permit_user("paint", "joe")
print(authorizor.check_permission("paint", "joe"))
# True