Python 入门指南(五)(3)

简介: Python 入门指南(五)

Python 入门指南(五)(2)https://developer.aliyun.com/article/1507420

抽象基类

虽然鸭子类型很有用,但事先很难判断一个类是否能够满足你所需的协议。因此,Python 引入了抽象基类ABC)的概念。抽象基类定义了一组类必须实现的方法和属性,以便被视为该类的鸭子类型实例。该类可以扩展抽象基类本身,以便用作该类的实例,但必须提供所有适当的方法。

实际上,很少需要创建新的抽象基类,但我们可能会发现需要实现现有 ABC 的实例的情况。我们将首先介绍实现 ABC,然后简要介绍如何创建自己的 ABC,如果你有需要的话。

使用抽象基类

Python 标准库中存在的大多数抽象基类都位于collections模块中。其中最简单的之一是Container类。让我们在 Python 解释器中检查一下这个类需要哪些方法:

>>> from collections import Container 
>>> Container.__abstractmethods__ 
frozenset(['__contains__']) 

因此,Container类确切地有一个需要被实现的抽象方法,__contains__。你可以发出help(Container.__contains__)来查看这个函数签名应该是什么样子的:

Help on method __contains__ in module _abcoll:
 __contains__(self, x) unbound _abcoll.Container method

我们可以看到__contains__需要接受一个参数。不幸的是,帮助文件并没有告诉我们这个参数应该是什么,但从 ABC 的名称和它实现的单个方法来看,很明显这个参数是用户要检查的容器是否包含的值。

这个方法由liststrdict实现,用于指示给定的值是否该数据结构中。然而,我们也可以定义一个愚蠢的容器,告诉我们给定的值是否在奇数集合中:

class OddContainer: 
    def __contains__(self, x): 
        if not isinstance(x, int) or not x % 2: 
            return False 
        return True 

有趣的是:我们可以实例化一个OddContainer对象,并确定,即使我们没有扩展Container,该类也是一个Container对象。

>>> from collections import Container 
>>> odd_container = OddContainer() 
>>> isinstance(odd_container, Container) 
True 
>>> issubclass(OddContainer, Container) 
True 

这就是为什么鸭子类型比经典多态更棒的原因。我们可以创建关系而不需要编写设置继承(或更糟的是多重继承)的代码的开销。

Container ABC 的一个很酷的地方是,任何实现它的类都可以免费使用in关键字。实际上,in只是语法糖,委托给__contains__方法。任何具有__contains__方法的类都是Container,因此可以通过in关键字查询,例如:

>>> 1 in odd_container 
True 
>>> 2 in odd_container 
False 
>>> 3 in odd_container 
True 
>>> "a string" in odd_container 
False 

创建一个抽象基类

正如我们之前看到的,要启用鸭子类型并不需要有一个抽象基类。然而,想象一下我们正在创建一个带有第三方插件的媒体播放器。在这种情况下,最好创建一个抽象基类来记录第三方插件应该提供的 API(文档是 ABC 的一个更强大的用例)。abc模块提供了你需要做到这一点的工具,但我提前警告你,这利用了 Python 中一些最深奥的概念,就像下面的代码块中所演示的那样:

import abc 
class MediaLoader(metaclass=abc.ABCMeta):
    @abc.abstractmethod
    def play(self):
        pass
    @abc.abstractproperty
    def ext(self):
        pass
    @classmethod
    def __subclasshook__(cls, C):
        if cls is MediaLoader:
            attrs = set(dir(C))
            if set(cls.__abstractmethods__) <= attrs:
                return True
        return NotImplemented

这是一个复杂的例子,包括了几个 Python 特性,这些特性在本书的后面才会被解释。它被包含在这里是为了完整性,但你不需要理解所有这些来了解如何创建你自己的 ABC。

第一件奇怪的事情是metaclass关键字参数被传递到类中,而在通常情况下你会看到父类列表。这是来自元类编程的神秘艺术中很少使用的构造。我们不会在本书中涵盖元类,所以你需要知道的是,通过分配ABCMeta元类,你为你的类赋予了超级英雄(或至少是超类)的能力。

接下来,我们看到了@abc.abstractmethod@abc.abstractproperty构造。这些是 Python 装饰器。我们将在第二十二章中讨论这些。现在,只需要知道通过将方法或属性标记为抽象,你声明了这个类的任何子类必须实现该方法或提供该属性,才能被视为该类的合格成员。

看看如果你实现了提供或不提供这些属性的子类会发生什么:

>>> class Wav(MediaLoader): 
...     pass 
... 
>>> x = Wav() 
Traceback (most recent call last): 
  File "<stdin>", line 1, in <module> 
TypeError: Can't instantiate abstract class Wav with abstract methods ext, play 
>>> class Ogg(MediaLoader): 
...     ext = '.ogg' 
...     def play(self): 
...         pass 
... 
>>> o = Ogg() 

由于Wav类未实现抽象属性,因此无法实例化该类。该类仍然是一个合法的抽象类,但你必须对其进行子类化才能实际执行任何操作。Ogg类提供了这两个属性,因此可以干净地实例化。

回到MediaLoader ABC,让我们解剖一下__subclasshook__方法。它基本上是说,任何提供了这个 ABC 所有抽象属性的具体实现的类都应该被认为是MediaLoader的子类,即使它实际上并没有继承自MediaLoader类。

更常见的面向对象语言在接口和类的实现之间有明确的分离。例如,一些语言提供了一个明确的interface关键字,允许我们定义一个类必须具有的方法,而不需要任何实现。在这样的环境中,抽象类是提供了接口和一些但不是所有方法的具体实现的类。任何类都可以明确声明它实现了给定的接口。

Python 的 ABCs 有助于提供接口的功能,而不会影响鸭子类型的好处。

解密魔术

如果你想要创建满足这个特定契约的抽象类,你可以复制并粘贴子类代码而不必理解它。我们将在本书中涵盖大部分不寻常的语法,但让我们逐行地概述一下:

@classmethod 

这个装饰器标记方法为类方法。它基本上表示该方法可以在类上调用,而不是在实例化的对象上调用:

def __subclasshook__(cls, C): 

这定义了__subclasshook__类方法。这个特殊的方法是由 Python 解释器调用来回答这个问题:类C是这个类的子类吗?

if cls is MediaLoader: 

我们检查方法是否是在这个类上专门调用的,而不是在这个类的子类上调用。例如,这可以防止Wav类被认为是Ogg类的父类:

attrs = set(dir(C)) 

这一行所做的只是获取类的方法和属性集,包括其类层次结构中的任何父类:

if set(cls.__abstractmethods__) <= attrs: 

这一行使用集合符号来查看候选类中是否提供了这个类中的抽象方法。请注意,它不检查方法是否已经被实现;只是检查它们是否存在。因此,一个类可能是一个子类,但仍然是一个抽象类本身。

return True 

如果所有的抽象方法都已经提供,那么候选类是这个类的子类,我们返回True。该方法可以合法地返回三个值之一:TrueFalseNotImplementedTrueFalse表示该类是否明确是这个类的子类:

return NotImplemented 

如果任何条件都没有被满足(也就是说,这个类不是MediaLoader,或者没有提供所有的抽象方法),那么返回NotImplemented。这告诉 Python 机制使用默认机制(候选类是否明确扩展了这个类?)来检测子类。

简而言之,我们现在可以将Ogg类定义为MediaLoader类的子类,而不实际扩展MediaLoader类:

>>> class Ogg(): ... ext = '.ogg' ... def play(self): ... print("this will play an ogg file") ... >>> issubclass(Ogg, MediaLoader) True >>> isinstance(Ogg(), MediaLoader) True

案例研究

让我们尝试用一个更大的例子把我们学到的东西联系起来。我们将为编程作业开发一个自动评分系统,类似于 Dataquest 或 Coursera 使用的系统。该系统需要为课程作者提供一个简单的基于类的接口,以便创建他们的作业,并且如果不满足该接口,应该提供有用的错误消息。作者需要能够提供他们的课程内容,并编写自定义答案检查代码,以确保他们的学生得到正确的答案。他们还可以访问学生的姓名,使内容看起来更友好一些。

评分系统本身需要跟踪学生当前正在进行的作业。学生可能在得到正确答案之前尝试几次作业。我们希望跟踪尝试次数,以便课程作者可以改进更难的课程内容。

让我们首先定义课程作者需要使用的接口。理想情况下,除了课程内容和答案检查代码之外,它将要求课程作者写入最少量的额外代码。以下是我能想到的最简单的类:

class IntroToPython:
    def lesson(self):
        return f"""
            Hello {self.student}. define two variables,
            an integer named a with value 1
            and a string named b with value 'hello'
        """
def check(self, code):
        return code == "a = 1\nb = 'hello'"

诚然,该课程作者可能对他们的答案检查方式有些天真。

我们可以从定义这个接口的抽象基类开始,如下所示:

class Assignment(metaclass=abc.ABCMeta):
    @abc.abstractmethod
    def lesson(self, student):
        pass
    @abc.abstractmethod
    def check(self, code):
        pass
    @classmethod
    def __subclasshook__(cls, C):
        if cls is Assignment:
            attrs = set(dir(C))
            if set(cls.__abstractmethods__) <= attrs:
                return True
        return NotImplemented

这个 ABC 定义了两个必需的抽象方法,并提供了魔术__subclasshook__方法,允许一个类被视为子类,而无需明确扩展它(我通常只是复制并粘贴这段代码。不值得记忆。)

我们可以使用issubclass(IntroToPython, Assignment)来确认IntroToPython类是否满足这个接口,这应该返回True。当然,如果愿意,我们也可以明确扩展Assignment类,就像在第二个作业中所看到的那样:

class Statistics(Assignment):
    def lesson(self):
        return (
            "Good work so far, "
            + self.student
            + ". Now calculate the average of the numbers "
            + " 1, 5, 18, -3 and assign to a variable named 'avg'"
        )
    def check(self, code):
        import statistics
        code = "import statistics\n" + code
        local_vars = {}
        global_vars = {}
        exec(code, global_vars, local_vars)
        return local_vars.get("avg") == statistics.mean([1, 5, 18, -3])

不幸的是,这位课程作者也相当天真。exec调用将在评分系统内部执行学生的代码,使他们可以访问整个系统。显然,他们将首先对系统进行黑客攻击,使他们的成绩达到 100%。他们可能认为这比正确完成作业更容易!

接下来,我们将创建一个类,用于管理学生在特定作业上尝试的次数:

class AssignmentGrader:
    def __init__(self, student, AssignmentClass):
        self.assignment = AssignmentClass()
        self.assignment.student = student
        self.attempts = 0
        self.correct_attempts = 0
    def check(self, code):
        self.attempts += 1
        result = self.assignment.check(code)
        if result:
            self.correct_attempts += 1
        return result
    def lesson(self):
        return self.assignment.lesson()

这个类使用组合而不是继承。乍一看,这些方法存在于Assignment超类似乎是有道理的。这将消除令人讨厌的lesson方法,它只是代理到作业对象上的相同方法。当然,可以直接在Assignment抽象基类上放置所有这些逻辑,甚至可以让 ABC 从这个AssignmentGrader类继承。事实上,我通常会推荐这样做,但在这种情况下,这将强制所有课程作者明确扩展该类,这违反了我们尽可能简单地请求内容创作的要求。

最后,我们可以开始组建Grader类,该类负责管理哪些作业是可用的,每个学生当前正在进行哪个作业。最有趣的部分是注册方法:

import uuid
class Grader:
    def __init__(self):
        self.student_graders = {}
        self.assignment_classes = {}
    def register(self, assignment_class):
        if not issubclass(assignment_class, Assignment):
            raise RuntimeError(
                "Your class does not have the right methods"
            )
        id = uuid.uuid4()
        self.assignment_classes[id] = assignment_class
        return id

这个代码块包括初始化器,其中包括我们将在一分钟内讨论的两个字典。register方法有点复杂,所以我们将彻底剖析它。

第一件奇怪的事是这个方法接受的参数:assignment_class。这个参数意味着是一个实际的类,而不是类的实例。记住,类也是对象,可以像其他类一样传递。鉴于我们之前定义的IntroToPython类,我们可以在不实例化的情况下注册它,如下所示:

from grader import Grader
from lessons import IntroToPython, Statistics
grader = Grader()
itp_id = grader.register(IntroToPython)

该方法首先检查该类是否是Assignment类的子类。当然,我们实现了一个自定义的__subclasshook__方法,因此这包括了不明确地作为Assignment子类的类。命名可能有点欺骗性!如果它没有这两个必需的方法,它会引发一个异常。异常是我们将在下一章详细讨论的一个主题;现在,只需假设它会使程序生气并退出。

然后,我们生成一个随机标识符来表示特定的作业。我们将assignment_class存储在一个由该 ID 索引的字典中,并返回该 ID,以便调用代码将来可以查找该作业。据推测,另一个对象将在某种课程大纲中放置该 ID,以便学生按顺序完成作业,但在项目的这一部分我们不会这样做。

uuid函数返回一个称为通用唯一标识符的特殊格式字符串,也称为全局唯一标识符。它基本上代表一个几乎不可能与另一个类似生成的标识符冲突的极大随机数。这是创建用于跟踪项目的任意 ID 的一种很好、快速和干净的方法。

接下来,我们有start_assignment函数,它允许学生开始做一项作业,给定该作业的 ID。它所做的就是构造我们之前定义的AssignmentGrader类的一个实例,并将其放入存储在Grader类上的字典中,如下所示:

def start_assignment(self, student, id):
        self.student_graders[student] = AssignmentGrader(
            student, self.assignment_classes[id]
        )

之后,我们编写了一些代理方法,用于获取学生当前正在进行的课程或检查作业的代码:

def get_lesson(self, student):
        assignment = self.student_graders[student]
        return assignment.lesson()
    def check_assignment(self, student, code):
        assignment = self.student_graders[student]
        return assignment.check(code)

最后,我们创建了一个方法,用于总结学生当前作业的进展情况。它查找作业对象,并创建一个格式化的字符串,其中包含我们对该学生的所有信息:

def assignment_summary(self, student):
        grader = self.student_graders[student]
        return f"""
        {student}'s attempts at {grader.assignment.__class__.__name__}:
        attempts: {grader.attempts}
        correct: {grader.correct_attempts}
        passed: {grader.correct_attempts > 0}
        """

就是这样。您会注意到,这个案例研究并没有使用大量的继承,这可能看起来有点奇怪,因为这一章的主题,但鸭子类型非常普遍。Python 程序通常被设计为使用继承,随着迭代的进行,它会简化为更多功能的构造。举个例子,我最初将AssignmentGrader定义为继承关系,但中途意识到最好使用组合,原因如前所述。

以下是一些测试代码,展示了所有这些对象是如何连接在一起的:

grader = Grader()
itp_id = grader.register(IntroToPython)
stat_id = grader.register(Statistics)
grader.start_assignment("Tammy", itp_id)
print("Tammy's Lesson:", grader.get_lesson("Tammy"))
print(
    "Tammy's check:",
    grader.check_assignment("Tammy", "a = 1 ; b = 'hello'"),
)
print(
    "Tammy's other check:",
    grader.check_assignment("Tammy", "a = 1\nb = 'hello'"),
)
print(grader.assignment_summary("Tammy"))
grader.start_assignment("Tammy", stat_id)
print("Tammy's Lesson:", grader.get_lesson("Tammy"))
print("Tammy's check:", grader.check_assignment("Tammy", "avg=5.25"))
print(
    "Tammy's other check:",
    grader.check_assignment(
        "Tammy", "avg = statistics.mean([1, 5, 18, -3])"
    ),
)
print(grader.assignment_summary("Tammy"))

练习

看看你的工作空间中的一些物理物体,看看你能否用继承层次结构描述它们。人类几个世纪以来一直在将世界划分为这样的分类法,所以这应该不难。在对象类之间是否存在一些非明显的继承关系?如果你要在计算机应用程序中对这些对象进行建模,它们会共享哪些属性和方法?哪些属性需要多态地重写?它们之间有哪些完全不同的属性?

现在写一些代码。不是为了物理层次结构;那很无聊。物理物品比方法更多。只是想想你过去一年想要解决的宠物编程项目。无论你想解决什么问题,都试着想出一些基本的继承关系,然后实现它们。确保你也注意到了实际上不需要使用继承的关系。有哪些地方你可能想要使用多重继承?你确定吗?你能看到任何你想使用混入的地方吗?试着拼凑一个快速的原型。它不必有用,甚至不必部分工作。你已经看到了如何使用python -i测试代码;只需编写一些代码并在交互式解释器中测试它。如果它有效,再写一些。如果不行,修复它!

现在,看看案例研究中的学生评分系统。它缺少很多东西,不仅仅是良好的课程内容!学生如何进入系统?是否有一个课程大纲规定他们应该按照什么顺序学习课程?如果你将AssignmentGrader更改为在Assignment对象上使用继承而不是组合,会发生什么?

最后,尝试想出一些使用混入的好用例,然后尝试使用混入,直到意识到可能有更好的设计使用组合!

总结

我们已经从简单的继承,这是面向对象程序员工具箱中最有用的工具之一,一直到多重继承——最复杂的之一。继承可以用来通过继承向现有类和内置类添加功能。将类似的代码抽象成父类可以帮助增加可维护性。父类上的方法可以使用super进行调用,并且在使用多重继承时,参数列表必须安全地格式化以使这些调用起作用。抽象基类允许您记录一个类必须具有哪些方法和属性才能满足特定接口,并且甚至允许您更改子类的定义。

在下一章中,我们将介绍处理特殊情况的微妙艺术。

第十八章:预料之外的情况

程序非常脆弱。如果代码总是返回有效的结果,那将是理想的,但有时无法计算出有效的结果。例如,不能除以零,或者访问五项列表中的第八项。

在过去,唯一的解决方法是严格检查每个函数的输入,以确保它们是有意义的。通常,函数有特殊的返回值来指示错误条件;例如,它们可以返回一个负数来表示无法计算出正值。不同的数字可能表示不同的错误。调用这个函数的任何代码都必须明确检查错误条件并相应地采取行动。许多开发人员不愿意这样做,程序就会崩溃。然而,在面向对象的世界中,情况并非如此。

在本章中,我们将学习异常,这是特殊的错误对象,只有在有意义处理它们时才需要处理。特别是,我们将涵盖以下内容:

  • 如何引发异常
  • 在异常发生时如何恢复
  • 如何以不同的方式处理不同类型的异常
  • 在异常发生时进行清理
  • 创建新类型的异常
  • 使用异常语法进行流程控制

引发异常

原则上,异常只是一个对象。有许多不同的异常类可用,我们也可以很容易地定义更多我们自己的异常。它们所有的共同之处是它们都继承自一个名为BaseException的内置类。当这些异常对象在程序的控制流中被处理时,它们就变得特殊起来。当异常发生时,除非在异常发生时应该发生,否则一切都不会发生。明白了吗?别担心,你会明白的!

引发异常的最简单方法是做一些愚蠢的事情。很有可能你已经这样做过,并看到了异常输出。例如,每当 Python 遇到无法理解的程序行时,它就会以SyntaxError退出,这是一种异常。这是一个常见的例子:

>>> print "hello world"
 File "<stdin>", line 1
 print "hello world"
 ^
SyntaxError: invalid syntax  

这个print语句在 Python 2 和更早的版本中是一个有效的命令,但在 Python 3 中,因为print是一个函数,我们必须用括号括起参数。因此,如果我们将前面的命令输入 Python 3 解释器,我们会得到SyntaxError

除了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错误并不总是意味着我们收到了无效的输入。它也可能意味着我们收到了不同的输入。用户可能误输入了零,或者故意输入了零,或者它可能代表一个合法的值,比如一个空的银行账户或者一个新生儿的年龄。

你可能已经注意到所有前面的内置异常都以Error结尾。在 Python 中,errorException这两个词几乎可以互换使用。错误有时被认为比异常更严重,但它们的处理方式完全相同。事实上,前面示例中的所有错误类都有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) 

这个类扩展了内置的list,就像我们在第十六章中讨论的那样,Python 中的对象,并覆盖了append方法以检查两个条件,以确保项目是偶数。我们首先检查输入是否是int类型的实例,然后使用模运算符确保它可以被 2 整除。如果两个条件中的任何一个不满足,raise关键字会引发异常。raise关键字后面跟着作为异常引发的对象。在前面的例子中,从内置的TypeErrorValueError类构造了两个对象。引发的对象也可以很容易地是我们自己创建的新Exception类的实例(我们很快就会看到),在其他地方定义的异常,甚至是先前引发和处理的Exception对象。

如果我们在 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  

我们很快就会看到,当解释器实际上没有采取捷径并立即退出时,我们可以在任一方法内部对异常做出反应并处理。事实上,异常可以在最初引发后的任何级别进行处理。

从下到上查看异常的输出(称为回溯),注意两种方法都被列出。在no_return内部,异常最初被引发。然后,在其上方,我们看到在call_exceptor内部,那个讨厌的no_return函数被调用,异常冒泡到调用方法。从那里,它再上升一级到主解释器,由于不知道该如何处理它,放弃并打印了一个回溯。

处理异常

现在让我们看一下异常硬币的反面。如果我们遇到异常情况,我们的代码应该如何对其做出反应或恢复?我们通过在try...except子句中包装可能引发异常的任何代码(无论是异常代码本身,还是调用可能在其中引发异常的任何函数或方法)来处理异常。最基本的语法如下:

try: 
    no_return() 
except: 
    print("I caught an exception") 
print("executed after the exception") 

如果我们使用现有的no_return函数运行这个简单的脚本——正如我们非常清楚的那样,它总是会引发异常——我们会得到这个输出:

I am about to raise an exception 
I caught an exception 
executed after the exception 

no_return函数愉快地通知我们它即将引发异常,但我们欺骗了它并捕获了异常。一旦捕获,我们就能够清理自己(在这种情况下,通过输出我们正在处理的情况),并继续前进,而不受那个冒犯性的函数的干扰。no_return函数中剩余的代码仍未执行,但调用函数的代码能够恢复并继续。

请注意tryexcept周围的缩进。try子句包装可能引发异常的任何代码。然后except子句回到与try行相同的缩进级别。处理异常的任何代码都在except子句之后缩进。然后正常代码在原始缩进级别上恢复。

上述代码的问题在于它会捕获任何类型的异常。如果我们编写的代码可能引发TypeErrorZeroDivisionError,我们可能希望捕获ZeroDivisionError,但让TypeError传播到控制台。你能猜到语法是什么吗?

这是一个相当愚蠢的函数,它就是这样做的:

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 / divider
TypeError: unsupported operand type(s) for /: 'int' and 'str'.  

输出的第一行显示,如果我们输入0,我们会得到适当的模拟。如果使用有效的数字(请注意,它不是整数,但仍然是有效的除数),它会正确运行。但是,如果我们输入一个字符串(你一定想知道如何得到TypeError,不是吗?),它会出现异常。如果我们使用了一个未指定ZeroDivisionError的空except子句,当我们发送一个字符串时,它会指责我们除以零,这根本不是正确的行为。

裸 except语法通常不受欢迎,即使你真的想捕获所有异常实例。使用except Exception:语法显式捕获所有异常类型。这告诉读者你的意思是捕获异常对象和所有Exception的子类。裸 except 语法实际上与使用except BaseException:相同,它实际上捕获了非常罕见的系统级异常,这些异常很少有意想要捕获,正如我们将在下一节中看到的。如果你真的想捕获它们,明确使用except BaseException:,这样任何阅读你的代码的人都知道你不只是忘记指定想要的异常类型。

我们甚至可以捕获两个或更多不同的异常,并用相同的代码处理它们。以下是一个引发三种不同类型异常的示例。它使用相同的异常处理程序处理TypeErrorZeroDivisionError,但如果您提供数字13,它也可能引发ValueError错误:

def funny_division2(divider):
    try:
        if divider == 13:
            raise ValueError("13 is an unlucky number")
        return 100 / divider
 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关键字,没有参数,将重新引发最后一个异常,如果我们已经在异常处理程序中。观察以下代码:

def funny_division3(divider):
    try:
        if divider == 13:
            raise ValueError("13 is an unlucky number")
        return 100 / divider
 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!之后,它将再次引发异常;我们仍然会在控制台上得到原始的堆栈跟踪。

如果我们像前面的例子中那样堆叠异常子句,只有第一个匹配的子句将被执行,即使有多个子句符合条件。为什么会有多个子句匹配?请记住,异常是对象,因此可以被子类化。正如我们将在下一节中看到的,大多数异常都扩展了Exception类(它本身是从BaseException派生的)。如果我们在捕获TypeError之前捕获Exception,那么只有Exception处理程序将被执行,因为TypeError是通过继承的Exception

这在一些情况下非常有用,比如我们想要专门处理一些异常,然后将所有剩余的异常作为更一般的情况处理。在捕获所有特定异常后,我们可以简单地捕获Exception并在那里处理一般情况。

通常,当我们捕获异常时,我们需要引用Exception对象本身。这最常发生在我们使用自定义参数定义自己的异常时,但也可能与标准异常相关。大多数异常类在其构造函数中接受一组参数,我们可能希望在异常处理程序中访问这些属性。如果我们定义自己的Exception类,甚至可以在捕获时调用自定义方法。捕获异常作为变量的语法使用as关键字:

try: 
    raise ValueError("This is an argument") 
except ValueError as e: 
    print("The exception arguments were", e.args) 

如果我们运行这个简单的片段,它会打印出我们传递给ValueError初始化的字符串参数。

我们已经看到了处理异常的语法的几种变体,但我们仍然不知道如何执行代码,无论是否发生异常。我们也无法指定仅在发生异常时执行的代码。另外两个关键字,finallyelse,可以提供缺失的部分。它们都不需要额外的参数。以下示例随机选择一个要抛出的异常并引发它。然后运行一些不那么复杂的异常处理代码,演示了新引入的语法:

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") 

如果我们运行这个例子——它几乎涵盖了每种可能的异常处理场景——几次,每次都会得到不同的输出,这取决于random选择的异常。以下是一些示例运行:

$ 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语句无论发生什么都会被执行。当我们需要在我们的代码运行结束后执行某些任务时(即使发生异常),这是非常有用的。一些常见的例子包括以下情况:

  • 清理打开的数据库连接
  • 关闭打开的文件
  • 通过网络发送关闭握手

finally子句在我们从try子句内部执行return语句时也非常重要。在返回值之前,finally处理程序将仍然被执行,而不会执行try...finally子句后面的任何代码。

此外,当没有引发异常时,请注意输出:elsefinally子句都会被执行。else子句可能看起来多余,因为应该在没有引发异常时执行的代码可以直接放在整个try...except块之后。不同之处在于,如果捕获并处理了异常,else块将不会被执行。当我们讨论后续使用异常作为流程控制时,我们将会更多地了解这一点。

try块之后可以省略任何exceptelsefinally子句(尽管单独的else是无效的)。如果包含多个子句,则必须先是except子句,然后是else子句,最后是finally子句。except子句的顺序通常从最具体到最一般。

Python 入门指南(五)(4)https://developer.aliyun.com/article/1507425

相关文章
|
3天前
|
Linux 开发工具 Python
初学者从无到有的Python语言如何入门,这份Python学习路线赶紧带走_python 从无到(1)
初学者从无到有的Python语言如何入门,这份Python学习路线赶紧带走_python 从无到(1)
初学者从无到有的Python语言如何入门,这份Python学习路线赶紧带走_python 从无到(1)
|
3天前
|
数据采集 算法 Python
2024年Python最全python基础入门:高阶函数,小米面试编程题
2024年Python最全python基础入门:高阶函数,小米面试编程题
|
3天前
|
存储 数据采集 数据挖掘
真正零基础Python入门:手把手教你从变量和赋值语句学起
真正零基础Python入门:手把手教你从变量和赋值语句学起
|
4天前
|
数据挖掘 数据处理 Python
【Python DataFrame 专栏】Python DataFrame 入门指南:从零开始构建数据表格
【5月更文挑战第19天】本文介绍了Python数据分析中的核心概念——DataFrame,通过导入`pandas`库创建并操作DataFrame。示例展示了如何构建数据字典并转换为DataFrame,以及进行数据选择、添加修改列、计算统计量、筛选和排序等操作。DataFrame适用于处理各种规模的表格数据,是数据分析的得力工具。掌握其基础和应用是数据分析之旅的重要起点。
【Python DataFrame 专栏】Python DataFrame 入门指南:从零开始构建数据表格
|
5天前
|
网络协议 网络架构 Python
Python 网络编程基础:套接字(Sockets)入门与实践
【5月更文挑战第18天】Python网络编程中的套接字是程序间通信的基础,分为TCP和UDP。TCP套接字涉及创建服务器套接字、绑定地址和端口、监听、接受连接及数据交换。UDP套接字则无连接状态。示例展示了TCP服务器和客户端如何使用套接字通信。注意选择唯一地址和端口,处理异常以确保健壮性。学习套接字可为构建网络应用打下基础。
20 7
|
6天前
|
Python
10个python入门小游戏,零基础打通关,就能掌握编程基础_python编写的入门简单小游戏
10个python入门小游戏,零基础打通关,就能掌握编程基础_python编写的入门简单小游戏
|
8天前
|
Python 索引 C语言
Python3从零基础到入门(2)—— 运算符-3
Python3从零基础到入门(2)—— 运算符
|
8天前
|
Python
Python3从零基础到入门(2)—— 运算符-2
Python3从零基础到入门(2)—— 运算符
Python3从零基础到入门(2)—— 运算符-2
|
8天前
|
Python C语言 存储
Python3从零基础到入门(2)—— 运算符-1
Python3从零基础到入门(2)—— 运算符
Python3从零基础到入门(2)—— 运算符-1
|
8天前
|
存储 C语言 Python