ThinkChat2.0新版上线,更智能更精彩,支持会话、画图、阅读、搜索等,送10W Token,即刻开启你的AI之旅 广告
[TOC] 程序非常脆弱。如果代码总是返回有效的结果,当然最好。但有时无法计算出一个有效的结果。例如,除以零是不可能的,或者访问只有5个元素列表中的第8个元素。 </b> 在过去,解决这个问题的唯一方法是严格检查每个函数的输入,确保它们有意义。通常,函数会返回特殊的值,用来指示错误情况;例如,他们可能将返回一个负数表示一个正值无法被计算。不同的数字可能意味着出现了不同的错误。任何调用此函数的代码都必须显式检查错误情况并采取相应措施。很多代码都懒得这么做,然后程序崩溃了。然而,在面向对象的世界里,情况并非如此。 </b> 在这一章中,我们将研究**异常**,特殊错误对象只需要在合理的时候处理就好了。我们将涵盖: * 如何导致异常发生 * 发生异常时如何恢复 * 如何以不同的方式处理不同的异常类型 * 发生异常时进行清理 * 创建新类型的异常 * 使用异常语法进行工作流控制 ## 抛出异常 原则上,异常只是一个对象。有许多不同的异常类,我们还可以轻松定义更多自己的异常类。他们唯一拥有的共同点是它们继承自一个名为`BaseException`的内置类。当这些异常对象在程序流程控制中被处理时,它们会变得非常特别。当异常发生时,所有应该发生的事情都不会发生,除非它应该在异常发生时发生。有道理吗?别担心,是这样的! </b> 导致异常发生的最简单的方法就是做一些傻事!给自己一些机会这样做,就可以看到异常输出。例如,任何时候Python在你的程序中遇到了一行它无法理解的代码,它就会产生`SyntaxError`的语法错误,这就是一种异常。下面是一个常见的例子: ``` >>> print "hello world" File "<stdin>", line 1 print "hello world" ^ SyntaxError: invalid syntax ``` 该打印`print`语句是Python 2和早期版本中的有效命令,但是在Python 3中,因为`print`现在是一个函数,所以我们必须将参数包含在括号里。因此,如果我们将前面的命令键入Python 3解释器中,我们将得到语法错误。 </b> 除了`SyntaxError`异常,我们还可以处理一些其他常见的异常,如下例所示: ``` >>> x = 5 / 0 Traceback (most recent call last): File "<stdin>", line 1, in <module> ZeroDivisionError: int division or modulo by zero >>> lst = [1,2,3] >>> print(lst[3]) Traceback (most recent call last): File "<stdin>", line 1, in <module> IndexError: list index out of range >>> lst + 2 Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: can only concatenate list (not "int") to list >>> lst.add Traceback (most recent call last): File "<stdin>", line 1, in <module> AttributeError: 'list' object has no attribute 'add' >>> d = {'a': 'hello'} >>> d['b'] Traceback (most recent call last): File "<stdin>", line 1, in <module> KeyError: 'b' >>> print(this_is_not_a_var) Traceback (most recent call last): File "<stdin>", line 1, in <module> NameError: name 'this_is_not_a_var' is not defined >>> ``` 有时这些异常是我们程序中某些错误的标志(在这种情况下,我们转到指定的行号并修复它们),但是它们也发生在合法的情况下。`ZeroDivisionError`并不总是意味着我们收到无效输入。这也可能意味着我们收到了不同的输入。用户可能错误地或故意地输入了零,或者它可能表示合法的值,如一个空的银行账户或一个新生儿的年龄。 </b> 你可能已经注意到前面所有的内置异常都以`Error`结尾。在Python中,错误和异常这两个词几乎可以互换使用。错误有时被认为比异常更可怕,但它们会被以完全相同的方式处理。事实上,前面示例中的所有错误类都以`Exception`(扩展`BaseException`)作为它们的超类。 ### 抛出一个异常 我们很快就将处理异常,但是在这之前,让我们看看,如果我们正在编写一个程序,当输入是无效的,我们应该如何通知用户(或调用一个函数)呢?如果我们也能用和Python使用的一样的机制,那不是很好吗?好吧,我们可以这样做!这里有一个给列表添加元素的简单类,只有当它们是偶数的整数时,才会被添加到列表中: ``` 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) ``` 这个类扩展了内置列表,正如我们在第2章“Python中的对象”所讨论,我们重写了`append`方法来检查两个条件,以确保新增元素是偶数。我们首先检查输入是否是`int`类型的实例,然后使用模数运算符,以确保它可被2整除。如果不能满足这两个条件中的任何一个时,`raise`关键字就会抛出异常。`raise`关键字后面只是简单地跟着作为异常的对象。在前面的例子中,两个对象是从内置类`TypeError` 和`ValueError`中新构造出来的。异常对象也可以很容易地是我们自己创建的新异常类的实例(我们将很快看到),或者一个在其他地方定义的异常,甚至一个先前抛出和处理的异常对象。如果我们在Python解释器测试这个类,我们可以看到,当异常出现时,它像以前一样输出有用的错误信息: ``` >>> e = EvenOnly() >>> e.append("a string") Traceback (most recent call last): File "<stdin>", line 1, in <module> File "even_integers.py", line 7, in add raise TypeError("Only integers can be added") TypeError: Only integers can be added >>> e.append(3) Traceback (most recent call last): File "<stdin>", line 1, in <module> File "even_integers.py", line 9, in add raise ValueError("Only even numbers can be added") ValueError: Only even numbers can be added >>> e.append(2) ``` > 虽然这个类在演示异常时是有效的,但实际上它并没有什么作用。仍然有可能使用索引或切片将其他值添加到列表中。这些问题可以通过重写其他适当的方法来避免,其中一些方法是双下划线方法。 ### 异常的影响 当异常被抛出时,它似乎会立即停止程序执行。抛出异常后,那些在异常之后应该运行的任何代码都不会被执行,除非异常得到处理,否则程序将退出并显示一条错误消息。看看下面这个简单的函数: ``` def no_return(): print("I am about to raise an exception") raise Exception("This is always raised") print("This line will never execute") return "I won't be returned" ``` 如果我们执行这个函数,我们会看到第一个`print`调用被执行,然后抛出异常。第二个`print`语句永远不会被执行,`return`语句也不会被执行: ``` >>> no_return() I am about to raise an exception Traceback (most recent call last): File "<stdin>", line 1, in <module> File "exception_quits.py", line 3, in no_return raise Exception("This is always raised") Exception: This is always raised ``` 此外,如果我们有一个函数调用另一个抛出异常的函数,在第二个函数被调用之后,第一个函数中将不会执行下去。抛出异常会通过函数调用停止所有执行堆栈,直到它被处理或强制解释器退出。为了演示,让我们添加调用前一个函数的第二个函数: ``` def call_exceptor(): print("call_exceptor starts here...") no_return() print("an exception was raised...") print("...so these lines don't run") ``` 当我们调用这个函数时,我们看到第一个`print`语句被执行了,然后`no_return`函数的第一行语句也被执行了。但是一旦抛出异常,剩下的语句就都没有被执行了: ``` >>> call_exceptor() call_exceptor starts here... I am about to raise an exception Traceback (most recent call last): File "<stdin>", line 1, in <module> File "method_calls_excepting.py", line 9, in call_exceptor no_return() File "method_calls_excepting.py", line 3, in no_return raise Exception("This is always raised") Exception: This is always raised ``` 我们很快就会看到,当解释器实际上没有抄近路立即离开时,我们可以对任一方法中的异常做出反应并进行处理。事实上,异常在最初被提出后,可以在任何级别进行处理。 </b> 从下到上查看异常的输出(称为回溯),注意这两种方法是如何列出的。在`no_return`中,最先会抛出异常。然后,在它上面,我们看到在`call_exceptor`中,那个讨厌的`no_return`函数被调用后,异常冒泡到调用方法中。从在那里,它又上升了一级到主解释器,它不知道 还有什么可做的,于是放弃并打印了一份回溯。 ### 处理异常 现在让我们看看异常硬币的另一面。如果我们遇到异常情况下,我们的代码应该如何反应或从中恢复?我们一般这样处理异常情况,通过使用`try ... except`包装任何可能引发异常的代码(无论它本身是异常代码,还是对内部可能引发异常的任何函数或方法的调用)。最基本的语法如下: ``` try: no_return() except: print("I caught an exception") print("executed after the exception") ``` 如果我们运行这个使用已存在`no_return`函数简单的脚本,`no_return`总会抛出异常,我们得到这样的输出: ``` I am about to raise an exception I caught an exception executed after the exception ``` `no_return`函数愉快地通知我们,它将抛出异常,是的,它抛出了,但我们愚弄了它,并抓住了这个异常。一旦抓住这个异常,我们就能清理干净异常(在这种情况下,通过输出我们正在处理的情况),继续运行,不受攻击性函数干扰。`no_return`函数中的剩余代码仍未执行,但是调用该函数的代码能够恢复并继续。 </b> 请注意`try`和`except`周围的缩进。`try`子句包装任何可能会引发异常的代码。`except`子句然后回到和`try`子句相同的缩进水平。任何处理异常的代码都缩进在`except`子句之后。然后,正常代码恢复到原始缩进级别。 </b> 前面代码的问题是它捕获所有类型的异常。如果我们正在编写一些可以引发`TypeError`和`ZeroDivisionError`的代码,我们可能想抓住`ZeroDivisionError`错误,但是让`TypeError`传播到控制台。你能猜出语法吗? </b> 有一个相当愚蠢的函数可以做到这一点: ``` def funny_division(divider): try: return 100 / divider except ZeroDivisionError: return "Zero is not a good idea!" print(funny_division(0)) print(funny_division(50.0)) print(funny_division("hello")) ``` 该功能通过`print`语句进行测试,这些语句显示其行为符合预期: ``` Zero is not a good idea! 2.0 Traceback (most recent call last): File "catch_specific_exception.py", line 9, in <module> print(funny_division("hello")) File "catch_specific_exception.py", line 3, in funny_division return 100 / anumber TypeError: unsupported operand type(s) for /: 'int' and 'str'. ``` 第一行输出显示,如果我们输入0,我们会被正确地嘲笑。如果我们用有效的数字调用(注意它不是整数,但仍然是有效的除数),它工作正常。但是如果我们输入一个字符串(你还在想如何得到一个`TypeError`错误,是吗?),它失败了,并抛出一个异常。如果我们用了一个空的没有指定`ZeroDivisionError`错误的`except`语句,它会指控我们当我们发送一个字符串时除以零,这根本不是一个正确的行为。 </b> 我们甚至可以捕捉两个或更多不同的异常,并用相同的方法处理它们。下面是一个引发三种不同类型异常的例子。它使用相同的异常处理程序处理`TypeError`和`ZeroDivisionError`,但如果你提供了数字13,它会抛出一个`ValueError`: ``` def funny_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("Testing {}:".format(val), end=" ") print(funny_division2(val)) ``` 底部的for循环遍历几个测试输入并打印结果。如果你想知道`print`语句中的`end`参数的意义,它实际就是一个换行符。程序运行如下: ``` Testing 0: Enter a number other than zero Testing hello: Enter a number other than zero Testing 50.0: 2.0 Testing 13: Traceback (most recent call last): File "catch_multiple_exceptions.py", line 11, in <module> print(funny_division2(val)) File "catch_multiple_exceptions.py", line 4, in funny_division2 raise ValueError("13 is an unlucky number") ValueError: 13 is an unlucky number ``` 数字0和字符串都被`except`子句捕获,并且打印错误信息。数字13的异常没有被捕获,因为它是一个`ValueError`异常,它不包括在正在处理的异常类型中。这一切都很好,但是如果我们想捕捉不同的异常并对它们做些不同的处理呢?或者也许我们想对异常做些事情然后允许它继续冒泡到父函数,就好像它从来没有被捕捉到?我们不需要任何新的语法来处理这些情况。将`except`子句堆栈是可能的,但只有第一个匹配将被执行。对于第二个问题,如果在异常处理程序中包含`raise`,不带参数的`raise`关键字将重新抛出最后一个异常。观察下面的代码: ``` def funny_division3(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 numerical value" except ValueError: print("No, No, not 13!") raise ``` 最后一行重新抛出`ValueError`异常,所以在输出`No, No, not 13!`之后,会再次抛出异常;我们仍然会在控制台上获得原始堆栈跟踪。 </b> 如果我们像前面的例子那样堆叠异常子句,那么只有第一个匹配子句将会运行,即使还有其他子句也满足匹配的条件。如何处理多个匹配子句呢?请记住,异常是对象,因此可以子类化。正如我们将在下一节中看到的,大多数异常都扩展自`Exception`类(它本身是从`BaseException`派生的)。如果我们在捕获`TypeError`之前捕捉到`Exception`,那么只有`Exception`处理程序被执行,因为`TypeError`继承自`Exception`。 </b> 这在我们想要处理一些特定异常的情况下会很有用,然后在更一般的情况处理所有剩余的异常。我们可以在捕获所有特定异常之后,简单的捕捉`Exception`,并作为一般情况处理。 </b> 有时,当我们捕获异常时,我们需要一个`Exception`对象本身的引用。这种情况经常发生在我们自定义包含参数的异常,但也可能出现与标准异常相关的情况。大多数异常类在它们的构造函数中接受一组参数,我们可能想要访问这些异常处理程序中的属性。如果我们定义自己的异常类,我们甚至可以在捕捉异常时,调用它的自定义方法。将捕获到的异常作为变量的语法为是使用`as`关键字: ``` try: raise ValueError("This is an argument") except ValueError as e: print("The exception arguments were", e.args) ``` 如果我们运行这个简单的片段,它会打印出我们传递到`ValueError`的初始化字符串参数值。(译注:结果如下图) ![](https://box.kancloud.cn/ef1012191a0675de10feac78d8027b4a_419x88.png) </b> 我们已经在处理异常的语法上看到了一些变化,但是我们仍然不知道,不管异常是否发生的情况下,如何执行代码。我们也没有指定,在没有异常情况下,应该执行的代码。还有两个关键词,`finally`和`else`,可以提供缺失的部分。这两个关键字都不接受任何额外的参数。以下示例随机选择异常并抛出它,然后运行一些不那么复杂的`Exception`处理程序说明新引入的语法: ``` import random some_exceptions = [ValueError, TypeError, IndexError, None] try: choice = random.choice(some_exceptions) print("raising {}".format(choice)) if choice: raise choice("An error") except ValueError: print("Caught a ValueError") except TypeError: print("Caught a TypeError") except Exception as e: print("Caught some other error: %s" % ( e.__class__.__name__)) else: print("This code called if there is no exception") finally: print("This cleanup code is always called") ``` 如果我们运行这个例子——它展示了几乎所有可以想到的异常处理场景—有几次,我们每次都会得到不同的输出,具体取决于随机选择哪个异常。以下是一些运行示例: ``` $ python finally_and_else.py raising None This code called if there is no exception This cleanup code is always called $ python finally_and_else.py raising <class 'TypeError'> Caught a TypeError This cleanup code is always called $ python finally_and_else.py raising <class 'IndexError'> Caught some other error: IndexError This cleanup code is always called $ python finally_and_else.py raising <class 'ValueError'> Caught a ValueError This cleanup code is always called ``` 注意`finally`子句中的`print`语句在任何情况下都执行了。当我们需要在之后执行某些任务时,这非常有用,当我们想在代码已经运行完毕(即使发生了异常)后执行某些特定的任务。一些常见的例子包括: * 清理打开的数据库连接 * 关闭打开的文件 * 通过网络发送结束握手的命令 当我们从`try`子句内部执行`return`语句时,`finally`子句也非常重要。`finally`处理程序仍将在`return`语句之前执行。 </b> 此外,当没有异常发生时,请注意输出:`else`和`finally`子句仍然被执行。`else`子句似乎是多余的,因为仅当没有异常时才应该执行的代码,其实可以放在整个`try...except`语句块之后。不同之处在于,如果你真的把`else`块放在`try...except`语句块之后,如果异常被捕获并处理,`else`块仍将被执行。我们稍后会继续讨论使用异常作为流控制的情况。 </b> 可以省略`try`块之后的`except`、`else`和`finally`子句中的任何一个(尽管只出现`else`是无效的)。如果包含它们中的几个,则为`except`子句必须放在最前面,然后是`else`子句,最后是`finally`子句。`except`子句的顺序通常从最特殊到最一般。 (译注:如果只保留`else`) ![](https://box.kancloud.cn/3f2ae683e7e6b465d397aa6019f83793_440x213.png) (译注:如果只保留`finally`) ![](https://box.kancloud.cn/b5f2c8a40fccac16dd9015afdaae7e23_445x229.png) ### 异常层次结构 我们已经看到了几个最常见的内置异常,你可能会在常规的Python开发过程中遇到其他情况。正如我们之前注意到的,大多数异常都是`Exception`类的子类。但并非所有的异常都是如此。`Exception`类本身实际上继承自一个名为`BaseException`的类。事实上,所有异常都必须扩展自`BaseException`类或一个`BaseException`类的子类。 </b> 有两个关键异常,`SystemExit`和`KeyboardInterrupt`,直接从`BaseException`类而不是从`Exception`类那里继承的。`SystemExit`是当程序自然退出时抛出的异常,通常是因为我们在代码中调用了`sys.exit`函数(例如,当用户在菜单项选择退出时,单击窗口上的“关”按钮,或输入关闭命令关闭服务器)。该异常旨在允许我们在程序最终退出前清理代码,我们通常不需要显式处理它(因为清理代码发生在finally子句中)。 </b> 如果我们处理它,通常会重新抛出异常,因为捕捉它会阻止程序退出。当然,在某些情况下,我们可能想要要阻止程序退出,例如,如果有未保存的更改,并且我们希望提示用户是否真的想退出。通常,如果我们处理`SystemExit`,都是因为我们想用它做一些特别的事情,或者直接预测它。我们尤其不希望它意外地被包含正常异常的子句所捕获。这也是它直接从`BaseException`派生的原因。 </b> `KeyboardInterrupt`异常在命令行程序中很常见。当用户按下与操作系统相关的组合键(通常为Ctrl + C),就会抛出这个异常。这是一种标准的用户故意中断正在运行程序的方法,就像`SystemExit`一样,它应该总是通过终止程序来响应。同样,像`SystemExit`一样,它应该在`finally`块中处理任何清理任务。 </b> 下面是一个类图,它充分说明了异常层次结构: ![](https://box.kancloud.cn/023498c9cc1b3168e5a7f1396fef4c86_338x184.png) 当我们使用`except:`子句而不指定任何类型的异常时,它将捕获`BaseException`的所有子类;也就是说,它将捕获所有异常,包括这两个特别的异常(中文书翻译有误)。因为我们几乎总是希望对这些进行特别处理,使用没有参数`except:`语句没有参数是不明智的。如果你愿意要捕获除`SystemExit`和`KeyboardInterrupt`之外的所有异常,请显式捕捉`Exception`。 </b> 此外,如果您确实想捕获所有异常,我建议使用语法`except BaseException:`,而不是`except:`。这有助于明确地告诉你代码的读者,你有意处理特殊情况下的异常。 ### 定义我们自己的异常 通常,当我们想要引发异常时,我们发现没有一个内置异常是合适的。幸运的是,定义我们自己的新异常是很容易的一件事。这个异常类的名字通常是为了说明出了什么问题,我们可以在初始化函数中包含附加信息的任意参数。 </b> 我们所要做的就是从`Exception`类继承。我们甚至不需要添加任何类内容!当然,我们可以直接扩展`BaseException`类,但是它将不会被`except Exception`子句捕获。 </b> 我们可以在银行应用程序中使用一个简单的异常: ``` class InvalidWithdrawal(Exception): pass raise InvalidWithdrawal("You don't have $50 in your account") ``` 最后一行说明了如何抛出新定义的异常。我们能够传递任意数量的参数进入这个异常。通常使用字符串消息,但在以后的异常处理程序中可能有用的任何对象都可以被存储。`Exception.__init__`方法旨在接受任何参数,并把它们作为属性存储在名为`args`的元组中。这使得异常更容易定义,且不需要重新定义`__init__`。 </b> 当然,如果我们确实想定制初始化函数,我们可以自由地这样做。这里的异常表示,其初始值函数接受当前余额和用户想提取的金额。此外,它还添加了一种计算透支程度的方法: ``` class InvalidWithdrawal(Exception): def __init__(self, balance, amount): super().__init__("account doesn't have ${}".format(amount)) self.amount = amount self.balance = balance def overage(self): return self.amount - self.balance raise InvalidWithdrawal(25, 50) ``` 最后的`raise`语句说明了如何构造这个异常。像你看到那样,我们可以对异常做任何事情,就像我们可以对其他对象做的事情一样。我们可以捕捉一个异常,并将其作为工作对象传递,尽管更常见的做法是将对工作对象的引用作为异常属性,并在内部进行传递。 </b> 以下是我们如何处理`InvalidWithdrawal`异常(如果出现的话): ``` try: raise InvalidWithdrawal(25, 50) except InvalidWithdrawal as e: print("I'm sorry, but your withdrawal is " "more than your balance by " "${}".format(e.overage())) ``` 这里我们看到`as`关键字的有效使用。按照惯例,大多数python程序员会将异常命名为变量`e`,尽管像往常一样,你也可以自由地将其称为`ex`、`exception`,或者`aunt_sally`,如果你喜欢的话。 </b> 有很多自定义异常的原因。在异常中添加信息或者以某种方式记录信息,通常很有用。但当创建一个框架、库或API供其他程序员访问时,自定义异常会很有用。在这种情况下,请小心确保你的代码抛出的异常,对客户端程序员时有意义。它们应该很容易处理,并清楚地描述发生了什么。客户端程序员应该很容易明白如何修复错误(如果异常反映了他们代码中的错误)或处理异常(如果这是他们需要被告知的情况)。 </b> 异常并不是例外。新手程序员倾向于将异常视为仅在特殊情况下有用。然而,例外环境的定义可能是模糊的,可能会有解释。考虑以下两种函数: ``` def divide_with_exception(number, divisor): try: print("{} / {} = {}".format( number, divisor, number / divisor * 1.0)) 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: print("{} / {} = {}".format( number, divisor, number / divisor * 1.0)) ``` 这两个函数行为相同。如果除数`divisor`为零,则错误消息将被印刷出来;否则,将显示打印除法结果的消息。我们可以通过使用`if`测试来避免引发`ZeroDivisionError`错误。同样,我们可以通过显式检查参数是否在列表的范围内来避免引发`IndexError`,通过检查`key`是否在字典里来避免引发`KeyError`。 </b> 但我们不应该这么做。我们可以写一个`if`语句来检查索引是否低于列表参数的数量,但忘记检查负值。 > 请记住,Python列表支持负索引;-1是指列表中的最后一个元素。 最终,我们会发现,我们必须找到我们曾经去过的所有地方检查代码。但是如果我们只是抓住了`IndexError`并处理了它,我们的代码可以正常工作。 </b> Python程序员倾向于*遵循请求原谅而不是许可*的模式,也就是说,他们执行代码,然后处理任何出错的地方。另一种选择是三思而后行,这通常是不可取的。有几个不可取的原因,但最主要的一个是,没有必要浪费CPU的能量在普通的代码中去寻找那些极其罕见的情况。因此,在特殊情况下使用异常处理是明智的,即使这些情况只是有点例外。沿着这个论点推进一步,我们实际上可以看到异常语法对于流程控制也是有效的。与`if`语句类似,异常可用于决策、分支和消息传递。 </b> 想象一个销售小部件和小工具的公司的库存应用程序。当顾客进行购买时,物品要么可以是可用的,在这种情况下,物品将从库存中被移除,并返回剩余的物品数量,要么可能库存没有货。现在,缺货是库存应用程序中非常正常的事情。这当然不是一个例外情况。但是,如果缺货的话,我们应该返回些什么?一串说缺货的话?负数?在这两种情况下,调用方法必须检查返回值是正整数还是其他什么。另一件事,就是确定是否缺货。这看起来有点乱。相反,我们可以抛出`OutOfStockException`异常,并使用`try`语句进行流程控制。有道理吗?此外,我们希望确保我们不会将相同的商品出售给两个不同的顾客,或者出售一个还没有在库的商品。解决这个问题的一种方法是锁定每种类型的商品,以确保一次只有一个人可以更新它。用户必须锁定物品,操作物品(购买,增加库存,清点剩余物品...),然后解锁该项目。下面是一个不完整的Inventory示例,其中包含文档字符串,用来描述一些方法应该做什么: ``` class Inventory: def lock(self, item_type): '''选择要操纵的项目类型。此方法将锁定项目, 所以没有其他人可以操纵库存,直到它被返回。 这防止了将同一商品卖给两个不同的人顾客。''' pass def unlock(self, item_type): '''释放给定类型,以便其他客户可以访问它。''' pass def purchase(self, item_type): '''如果项目未锁定,抛出异常。如果项目类型不存在,抛出异常。 如果项目当前是缺货,抛出异常。如果物品 可用,减去一个项目并返回剩余的项目数。''' pass ``` 我们可以将这个对象原型交给开发人员,让他们按照文档实现购买方法,同时我们在这些代码之上工作,以完成购买。我们将使用Python强大的异常处理,根据购买情况,来处置不同的分支: ``` item_type = 'widget' inv = Inventory() inv.lock(item_type) try: num_left = inv.purchase(item_type) except InvalidItemType: print("Sorry, we don't sell {}".format(item_type)) except OutOfStock: print("Sorry, that item is out of stock.") else: print("Purchase complete. There are " "{} {}s left".format(num_left, item_type)) finally: inv.unlock(item_type) ``` 注意所有可能被使用的异常处理子句,确保正确的行动发生在正确的时间。尽管`OutOfStock`不是一个非常特殊的情况,我们在这里使用异常来处理它是合适的。同样的代码可以用`if...elif...else`结构编制,但不是那么容易阅读或维护。 </b> 我们也可以使用异常在不同的方法之间传递消息。例如,如果我们想告知客户该商品再次有库存时的预期销售日期,我们需要确保我们构造`OutOfStock`对象时,应该有一个补货参数`back_in_stock`。然后,当我们处理异常时,我们可以检查这个参数值,并向客户提供附加信息。绑定在对象上的信息可以很容易地在程序的两个不同部分之间传递。该异常甚至可以提供一种方法指示库存对象重新订货或延期交货。 </b> 对流控制使用异常会有助于一些便利的程序设计。在我们的讨论中,有一点很重要:异常并不是一件我们应该尽量避免的坏事。发生异常并不意味着你应该阻止这种特殊情况的发生。相反,它只是一种在代码的两个部分之间传递信息的强大方式,而不用直接彼此相互调用。 </b> ## 个案研究 我们一直在相当低的水平(语法和定义)上研究异常的使用和处理细节。这个案例研究将有助于把这一切与我们前几章联系在一起,这样我们就可以看到异常是如何在对象、继承和模块中使用。 </b> 今天,我们将设计一个简单的中央认证和授权系统。整个系统将被放在一个模块中,其他代码将能够查询用于身份验证和授权目的的模块对象。我们得承认,从一开始,我们就不是安全专家,我们正在设计的系统可能布满了安全漏洞。我们的目的是研究异常,而不是确保系统安全。然而,对于一个基本的可以与其他代码交互的登录和许可系统,目前的设计已经足够了。稍后,如果需要使其他代码更加安全,我们可以让安全或密码学专家审查或重写我们的模块,最好不改变API。 </b> 身份验证是确保用户真的是他们所说的那个人的过程。今天,我们将跟随通用网络系统的主流做法,即使用用户名和私人密码的组合。其他认证方法包括语音识别、指纹或视网膜扫描仪以及识别卡。 </b> 另一方面,授权就是确定一个给定(经过身份验证的)用户允许执行特定行为的权限。我们将创建一个基本的许可列表系统,存储允许特定人员执行可能行为的列表。 </b> 此外,我们将添加一些管理功能,以允许向系统中添加新用户。为简洁起见,一旦新用户被添加,我们将省略对密码的编辑或对权限的改变,但是这些(非常必要的)功能将来肯定会增加上的。 </b> 这是一个简单的分析;现在让我们继续设计。我们显然会需要一个存储用户名和加密密码的用户`User`类。这个类还将通过检查提供的密码是否有效允许用户来登录。我们可能不需要权限`Permission`类,因为这些只是使用字典映射到用户列表的字符串。我们应该有一个中央验证`Authenticator`类,它处理用户管理和登录或注销。拼图的最后一块是`Authorizor`类,处理权限并检查用户是否可以进行一项活动。我们将在`auth`模块提供这些类的单例,以便其他模块可以为了认证和授权需求使用这个中央机制。当然,如果它们想实例化私有实例,用于非中央授权活动,它们可以自由地这样做。 </b> 我们还将定义几个异常。我们先创建一个`AuthException`基类,接受用户名`username`和可选用户`user`对象作为参数;我们大多数自定义的异常都将继承于这个基类。 </b> 让我们首先构建用户`User`类;这看起来很简单。可以初始化一个有用户名和密码的新用户。密码将被加密存储以减少它被盗的可能性。我们还需要一个检查密码`check_password`的方法来测试提供的密码是否正确。这是完整的类: ``` 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): '''对用户名和密码进行加密并返回sha摘要。''' hash_string = (self.username + password) hash_string = hash_string.encode("utf8") return hashlib.sha256(hash_string).hexdigest() def check_password(self, password): '''如果密码对该用户有效,则返回True,否则为False''' encrypted = self._encrypt_pw(password) return encrypted == self.password ``` 因为加密密码的代码在`__init__`和`check_password`中都是必需的,我们把它提取出来放到它自己的方法里。这样,如果有人意识到不安全,需要改进,只需要在一个地方改变。这个类可以很容易扩展,包括强制或可选的个人详细信息,例如姓名、联系信息和出生日期。 </b> 在我们编写代码添加用户之前(这将发生在尚未定义的`Authenticator`类),我们应该检查一些用例。如果一切顺利,我们可以添加具有用户名和密码的用户;创建并插入用户对象到字典里。但是有什么方面会不顺利呢?显然我们不想使用字典中已经存在的用户名添加用户。如果我们这样做了,我们会覆盖现有用户的数据,新用户可能有权访问该用户的数据特权。所以,我们需要一个用户名已经存在`UsernameAlreadyExists`的异常。另外,为了安全起见,如果密码太短,我们可能会抛出一个异常。这两个异常都是扩展自我们前面提到的`AuthException`。所以,在编写`Authenticator`类之前,让我们定义这三个异常类: ``` 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 ``` `AuthException`异常需要用户名和一个可选的用户参数。第二个参数应该是与用户名关联的用户`User`类的实例。我们正在定义的两个特殊异常只需在异常情况下通知调用类即可,所以我们不需要添加任何额外的方法。 </b> 现在让我们从`Authenticator`类开始。它仅仅是把用户名映射到用户对象上,所以我们将从初始化函数中的字典开始。添加用户的方法,在创建新的用户实例并将其添加到字典之前,需要检查两个条件(密码长度和以前已经存在的用户): ``` 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) ``` 当然,如果密码太容易以其他方式破解的话,我们可以扩展密码验证来引发密码异常。现在让我们准备登录`login`方法。如果我们刚才没有考虑异常,我们可能只是想方法根据登录成功与否返回真或假。但是我们正在考虑异常,这可能是一个使用它们的不太特殊的情况。例如,我们可能想抛出不同的异常,例如用户名不存在或密码不匹配。这将允许尝试登陆的任何人使用`try / except / else`子句优雅地处理这些情况。首先,我们添加这些新的异常: ``` class InvalidUsername(AuthException): pass class InvalidPassword(AuthException): pass ``` 然后,我们可以为我们的`Authenticator`类定义一个简单的登录`login`方法。如有必要,我们抛出这些异常。如果没有,它会用标记户已登录并返回: ``` 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 ``` 请注意`KeyError`是如何被处理的。也可以通过`if username not in self.users:`来处理,但我们选择了直接处理异常。我们已经消化了第一个异常,我们也可以提出一个全新的、自定义的、更适合面向用户API的异常。 </b> 我们还可以添加一个方法来检查特定用户名是否已登录。决定是否在这里使用异常是很棘手的事情。如果出现以下情况,我们是否应该提出用户名不存在的异常?如果用户没有登录,我们应该抛出异常吗? </b> 为了回答这些问题,我们需要思考这个方法是如何被访问的?大多数情况下,这种方法将用于回答是/否问题,“我应该允许他们访问*某些内容*吗?”答案要么是,“是的,用户名有效并且他们已登录”,或者“不,用户名无效或者他们未登录”。因此,布尔返回值是足够的,没必要只是为了使用异常而使用异常。 ``` def is_logged_in(self, username): if username in self.users: return self.users[username].is_logged_in return False ``` 最后,我们可以在模块中添加一个默认的验证器实例,这样客户端代码可以使用`auth.authenticator`轻松访问它: ``` authenticator = Authenticator() ``` 这行代码属于模块级别,在任何类定义之外,所以验证器变量可以作为`auth.authenticator`访问。现在我们可以开始了`Authorizor`类,它将权限映射到用户。如果用户未登录,`Authorizor`类应该不允许他们访问权限,因此他们需要对特定验证者的引用。我们还需要设置初始化时的许可字典: ``` 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) ``` 第一种方法允许我们在中创建新的权限,如果它已经存在,则抛出异常。第二个方法允许我们将用户名添加到一个权限里,除非权限或用户名尚不存在。 </b> 我们使用`set`而不是`list`作为用户名的容器(译注:见最后一句`perm_set.add(username)`,这里用的是`set`的`add`方法,`list`没有`add`方法),这样即使你授予用户多次权限,集合的性质意味着用户只在集合中出现一次。我们将在下一章进一步讨论集合。 </b> 这两种方法都会抛出`PermissionError`异常。这个新异常不需要用户名,所以我们可以直接扩展`Exception`获得`PermissionError`,而不是继承自我们的自定义的`AuthException`: ``` class PermissionError(Exception): pass ``` 最后,我们可以添加一个方法来检查用户是否有特定的权限。为了授予他们访问权限,他们必须同时满足通过登录验证和存在于被放授予该特权的人的集合中。如果不满足这些条件中的任何一个,则会引发异常: ``` 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 ``` 这里有两个新的异常;他们都带用户名,所以我们定义它们是`AuthException`的子类: ``` class NotLoggedInError(AuthException): pass class NotPermittedError(AuthException): pass ``` 最后,我们可以添加一个默认`authorizor`作为我们的默认验证器: ``` authorizor = Authorizor(authenticator) ``` 这就完成了一个基本的认证/授权系统。我们可以在Python提示符下测试这个系统,检查用户joe是否被允许在油漆部门工作: ``` >>> import auth >>> auth.authenticator.add_user("joe", "joepassword") >>> auth.authorizor.add_permission("paint") >>> auth.authorizor.check_permission("paint", "joe") Traceback (most recent call last): File "<stdin>", line 1, in <module> File "auth.py", line 109, in check_permission raise NotLoggedInError(username) auth.NotLoggedInError: joe >>> auth.authenticator.is_logged_in("joe") False >>> auth.authenticator.login("joe", "joepassword") True >>> auth.authorizor.check_permission("paint", "joe") Traceback (most recent call last): File "<stdin>", line 1, in <module> File "auth.py", line 116, in check_permission raise NotPermittedError(username) auth.NotPermittedError: joe >>> auth.authorizor.check_permission("mix", "joe") Traceback (most recent call last): File "auth.py", line 111, 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 "<stdin>", line 1, in <module> File "auth.py", line 113, in check_permission raise PermissionError("Permission does not exist") auth.PermissionError: Permission does not exist >>> auth.authorizor.permit_user("mix", "joe") Traceback (most recent call last): File "auth.py", line 99, in permit_user perm_set = self.permissions[perm_name] KeyError: 'mix' During handling of the above exception, another exception occurred: Traceback (most recent call last): File "<stdin>", line 1, in <module> File "auth.py", line 101, in permit_user raise PermissionError("Permission does not exist") auth.PermissionError: Permission does not exist >>> auth.authorizor.permit_user("paint", "joe") >>> auth.authorizor.check_permission("paint", "joe") True ``` 虽然冗长,但前面的输出显示了我们的所有代码和大部分在起作用的异常,但是为了真正理解我们定义的API,我们应该编写一些实际使用它的异常处理代码。这是一个基本菜单界面允许某些用户更改或测试程序: ``` import auth # Set up a test user and permission auth.authenticator.add_user("joe", "joepassword") auth.authorizor.add_permission("test program") auth.authorizor.add_permission("change program") auth.authorizor.permit_user("test program", "joe") class Editor: def __init__(self): self.username = None self.menu_map = { "login": self.login, "test": self.test, "change": self.change, "quit": self.quit } def login(self): logged_in = False while not logged_in: username = input("username: ") password = input("password: ") try: logged_in = auth.authenticator.login( username, password) except auth.InvalidUsername: print("Sorry, that username does not exist") except auth.InvalidPassword: print("Sorry, incorrect password") else: self.username = username def is_permitted(self, permission): try: auth.authorizor.check_permission( permission, self.username) except auth.NotLoggedInError as e: print("{} is not logged in".format(e.username)) return False except auth.NotPermittedError as e: print("{} cannot {}".format( e.username, permission)) return False else: return True def test(self): if self.is_permitted("test program"): print("Testing program now...") def change(self): if self.is_permitted("change program"): print("Changing program now...") def quit(self): raise SystemExit() def menu(self): try: answer = "" while True: print(""" Please enter a command: \tlogin\tLogin \ttest\tTest the program \tchange\tChange the program \tquit\tQuit """) answer = input("enter a command: ").lower() try: func = self.menu_map[answer] except KeyError: print("{} is not a valid option".format( answer)) else: func() finally: print("Thank you for testing the auth module") Editor().menu() ``` 这个相当长的例子在概念上非常简单。`is_permitted`方法可能是最有趣的;它是`test`和`change`都调用的主要内部方法,用于确保用户被允许访问。当然,这两种方法都是存根,但是我们这里没有编写编辑器;我们在这里主要通过测试身份验证和授权框架展示异常和异常处理程序! ## 摘要 在这一章中,我们讨论了抛出、处理、定义和操纵异常的细节。异常是一种强有力的、不需要调用函数显式检查返回值的方式。有许多内置的异常,抛出它们很简单。有很多不同的语法来处理不同的异常事件。 </b> 在下一章中,我们到目前为止所学的、所讨论的一切汇集在一起,看看如何最好地在Python应用程序中应用面向对象的编程原则和结构。