10.9 *创建异常

尽管标准异常集包含的内容已经相当广泛,你还是可以创建自己的异常。一种情况是你想在特定的标准异常和模块异常中添加额外的信息。我们将介绍两个例子,都与IOError有关。IOError是一个用于输入/输出地通用异常,可能在无效的文件访问或其他形式的通信中触发。假如我们想要更加明确地标明问题的来源,比如:对于文件错误,我们希望有行为类似IOError的一个FileError异常,但是名字表明是在执行文件操作。

我们将查看的另一个异常与套接字(socket)网络编程有关。socket模块生成的异常叫socket.error,不是内建的异常。它从通用Exception类派生。然而socket.error这个异常的宗旨和IOError很类似,所以我们打算定义一个新的从IOError派生的NetworkError的异常,但是其包含了socket.error提供的信息。

如同类和面向对象编程,我们暂时不会正式介绍网络编程,如果你需要的话可以跳到16章。

我们现在给出一个叫做myexc.py的模块和我们自定义的新异常FileError与NetworkError.代码如例10.2。

例10.2 创建异常(myexc.py)

此模块定义了两个新的异常,FileError和NetworkError,也重新实现了一个诊断版的openO[myopen()]和socket.connect()[myconnect()]。同时包含了一个测试函数[test()],当直接运行文件时执行。

10.9 *创建异常 - 图1

10.9 *创建异常 - 图2

10.9 *创建异常 - 图3

1 ~ 3行

模块的开始部分是Unix启动脚本和socket、os、errno、types和tempfile模块的导入。

5 ~ 9行

无论你是否相信,这5行代码定义了我们的新异常。不是仅仅一个,而是两个。除了将要介绍的一个新功能,创建一个新的异常仅需要从一个已经存在的异常类派生一个出子类。本例中,这个基类是IOError。我们也可以从IOError的基类EnvironmentError派生,但我们想明确表明我们的异常是I/O相关的。

我们选择IOError是因为它提供了两个参数,一个错误编号和一个错误字符串。文件相关[用open()]的IOError异常甚至支持大部分异常没有的第三个参数,那个可以是文件名。我们将对这个在主要元组之外的,名字叫“filename”的参数执行一些特定的操作。

11 ~ 21行

updArgs()函数的全部意图就是“更新”异常的参数。我们这里的意思是原来的异常提供给我们一个参数集。我们希望获取这些参数并让其成为我们新的异常的一部分,可能是嵌入或添加第三个参数(如果没有传入,什么也不添加——None是其默认值,我们下一章将会学习)。我们的目标是提供更多的细节信息给用户,这样当问题发生时能够尽快的捕捉到。

23 ~ 53行

函数fileArgs()仅在myopen()中使用(如下)。实际上,我们寻找表示”没有权限(permission denied.)”的错误EACCES。其他所有的IOError异常我们将不加修改(54〜55行)的传递。如果你对ENXIO、EACCES和其他的系统错误号感到好奇,你可以从Unix系统下/usr/include/sys/errno.h或Windows系统下Visula C++的C: \Msdev\include\Errno.h文件来对它们刨根究底。

在第27行,我们也确认了我们当前使用的机器支持os.access()函数,它用来检查对任意一个特定文件你所拥有的权限。除非我们收到权限错误同时也能够检查我们拥有的权限,否则我们什么不做。当一切完毕,我们设置一个字典来帮助构建表示我们对文件所拥有的权限的字符串。

Unix文件系统清晰标明用户(user)、组(group,可以有多个用户属于一个组)和其他(other,不是所有者,也不和所有者同组的用户)对文件的读、写、执行(‘r’, ‘w’, ‘x’)的权限。

Windows支持这些权限中的一部分。现在可以来构建权限字符串了。如果对文件有某种权限,字符串中就有相应的字母,否则用‘-’替代。比如,字符串‘rw-’标明你可以对其进行读/写访问。如果字符串是‘r-x’,你仅可¯以对其进行读和执行操作;‘—-’标示没有任何权限。

当权限字符串构建完成后,我们创建了一个临时的参数列表。我们随后更改了错误字符串使之包含权限字符串。(标准的IOError异常并没有提供权限字符串相关信息)。”Permission denied(没有权限)”这个错误似乎很愚蠢,而且系统并没有提供纠正它的方法。当然这是出于安全的考虑。当入侵者没有权限访问某个东西时,最好不要让他们看到文件的权限。不过,我们的例子仅仅是一个练习,所以我们可以暂时地”违背安全”。问题的关键在于确认调用os.chmod()函数修改后的文件权限是不是你的本意。

最后一件事情我们把文件名加入参数列表,并以元组形式返回参数。

55 ~ 65行

我们新的myconnect()函数仅仅是简单的对套接字的函数conect()进行包装当网络连接失败时提供一个IOError类型的异常。和一般的socket.error不一样,我们还提供给程序员主机名和端口号。

对于刚刚接触网络编程的,主机名和端口号可以想象为当你联系某人时的区号和电话号。在这个例子中,我们试着去连接一个在远程主机上运行的程序,可能是某种服务。因此我们需要知道主机名和服务器监听的端口。

当失败发生时,错误号和错误字符很有帮助,但是如果结合更精确的主机-端口会更有帮助,因为这一对可能是由某个数据库或名称服务动态生成或重新获得。这些值由我们版本的connect()加入。另一种情形是无法找到主机,socket.error异常没有直接提供的错误号,我们为了遵循IOError协议,提供了一个错误号-错误字符串对,我们查找最接近的错误号。我们选用ENXIO。

67 ~ 73行

类似同类myconnect(), myopen()也封装了已经存在的一些代码。这里,我们用的是open()函数。我们仅仅捕捉IOError异常。所有的其他都忽略并传给下一层(因为没有与他们相关的处理器)。一旦捕捉到IOError我们引发我们自己的异常并通过fileArgs()返回值来定制参数。

75 ~ 95行

我们首先测试文件,这里使用testfile()函数。开始之前,我们需要新建一个测试文件,以便我们可以手工修改其权限来造成权限错误。这个tempfile模块包含了创建临时文件文件名和临时文件的代码。当前我们仅仅需要文件名,然后用myopen()函数来创建一个空的文件。注意,如果此次产生了错误,我们不会捕获,我们的程序将致命的终止——测试程序当我们连文件都无法创建时不会继续。

我们的测试用了4种不同的权限配置。零表示没有任何权限,0100表示仅能执行,0400表示只读,0500表示只可读或执行(0400+0100)。在所有的情况下,我们试图用一种无效的方式打开文件。os.chmod()被用来改变文件的权限(注意:这些权限有前导的零,表明他们是八进制[基数8]数)。

如果发生错误,我们希望可以显示诊断的信息,类似Python解释器捕获异常时所做的那样。这就是给出异常名和紧跟其后的异常的参数。class属性表示实例化该实例的类对象。比在此显示完整的类名(myexc.FileError)更好的做法是通过类对象的name属性来显示类名(FileError),这也是异常未被捕获时你在解释器所见到的。随后是我们在封装函数中辛辛苦苦聚到一起的参数。

如果文件被打开成功,也就是权限由于某种原因被忽略。我们通过诊断信息指明并关闭文件。当所有的测试都完成时,我们对文件开启所有的权限然后用os.unlink()移除(os.remove()等价于os.unlink())。

97 ~ 106行

下一段代码(testnet())测试了我们的网络异常。套接字是一个用来与其他主机建立连接的通信端点。我们创建一个套接字,然后用它连接一个没有接受我们连接的服务器的主机和一个不存在于我们网络的主机。

108 ~ 110行

我们希望仅在直接调用我们脚本时执行test*()函数,此处的代码完成了该功能。大多数脚本用同样的格式给出了这段文本。

在Unix系的机器上运行这段脚本,我们得到了如下的输出:

10.9 *创建异常 - 图4

10.9 *创建异常 - 图5

在Win32的机器上有些不同:

10.9 *创建异常 - 图6

你可以看到Windows不支持文件的读权限,这就是前两次尝试文件打开成功的原因。在你的机器和操作系统上的结果可能会大相径庭。