杂记(十一) 解读Python中的异常处理

try-except-else-finally语句

目录

一、绪论 (introduction)

二、异常捕获 (exception catching)

2.1 try-except 语句

2.1.1 基本用法

2.1.2 指定异常类型

2.1.3 小结

2.2 try-except-else 语句

2.3 try-except-else-finally 语句

2.4 小结

三、异常抛出 (exception raising)

四、异常自定义 (exception customizing)

五、预定义的清理行为 (with statement)

六、断言 (asserting)

七、小结 (summary)

八、彩蛋 —— finally 和 return 谁的优先级更高?(选读)



一、绪论 (introduction)

不同于语法错法错误 (解析错误),调试 Python 程序时,即便语句或表达式的语法正确,也可能在执行时引发错误。在 执行时检测到的错误 称为 **异常。**Python 使用被称为 异常特殊对象 来管理程序执行期间发生的错误。每当发生让 Python 不知所措的错误时,它都会创建一个异常对象。

异常虽不一定会导致严重后果,但大多数异常并不会被程序处理。当 Python 脚本发生异常时,程序将终止执行,并显示各种 回溯 (Traceback) 信息。Traceback 是 Python 错误信息的报告,类似于其他编程语言中的 stack trace、stack traceback、backtrac 等。Traceback 的前一部分以堆栈回溯的形式显示发生异常时的上下文,并由语法分析器指示出错的行,而最后一行则声明程序的错误类型信息。

尤其是在读写文件时,很多地方都可能导致错误发生。例如,试图读取一个不存在的文件或目录时,将得到一个找不到文件的错误 (FileNotFoundError):

>>> fin = open('test.py')

Traceback (most recent call last):
  File "<pyshell#1>", line 1, in <module>
    fin = open('test.py')
FileNotFoundError: [Errno 2] No such file or directory: 'test.py'

关于异常的原因,一方面,可能源自个人疏忽与考虑不周,此时需要根据异常 Traceback 到出错位置并分析改正;另一方面,有些异常 无法预料或不可避免,此时可选择 捕获异常并处理,从而 避免程序的意外终止或崩溃


二、异常捕获 (exception catching)

img

使用 try 语句 代码块是最基本的 异常捕获和处理 方法,其常见的搭配形式 (关键词组合) 有:

  • try-except
  • try-except-else
  • try-except-else-finally

当然,对于关键词的选用搭配,实质还是取决于个人需求,以下将依次进行说明:


2.1 try-except 语句

2.1.1 基本用法

try-except 语句是 最基础而重要 的部分,其基本语法规则为:

try:
    # 执行要尝试 (try) 的代码

except# 执行应对异常发生时的代码

try-except 语句用于检测 try 子句(块) 中的错误,从而令 except 语句(块) 捕获异常信息并作出应对和处理。具体而言,Python 从 try 子句开始执行,若一切正常,则跳过 except 子句;若发生异常,则跳出 try 子句,执行 except 子句

延续上节例子 —— 捕获读取一个不存在的文件/目录的异常:

>>> try:
	fin = open('test.py')  # 不存在的文件
	print('Everything went well!')  # 打印顺利运行提示信息

except:
	print('Something went wrong!')  # 处理异常方式:打印错误提示信息

Something went wrong!

可见异常被捕获了,IDLE 并未打印 Traceback 信息,而是打印了我们自定义的 except 子句中的错误提示信息。然而,本例中的 except 子句仅仅是简单地提示了错误。实际上,可以根据需求设计更多具有实用修正/弥补功能的 except 子句。

另一方面,若文件/目录存在,则将顺利执行完 try 子句并跳过 except 子句:

>>> try:
	fin = open('train.py')  # 实际存在的文件
	print('Everything went well!')  # 打印顺利运行提示信息

except:
	print('Something went wrong!')  # 处理异常方式:打印错误提示信息

Everything went well!
2.1.2 指定异常类型

因为 except 子句默认捕获的异常类型是 Exception,所以 except 子句总是捕获所有异常。

>>> try:
	fin = open('test.py')  
	print('Everything went well!')  

except Exception:  # 不指定 Exception 也一样
	print('Something went wrong!')  

Something went wrong!

但若有特殊需要,也可 指定 except 子句捕获的异常类型,例如:

>>> try:
	fin = open('test.py')
	print('Everything went well!')

except FileNotFoundError:
	print('Something went wrong!')

Something went wrong!

关于异常类型指定,既可以后知后觉 —— 根据 Trackback 指出的错误,也可以先知先觉 —— 查文档选定以防不测。但注意,倘若发生了未指定到的异常类型 (通常源于误指定或漏指定导致异常类型不匹配),则异常仍会发生:

>>> try:
	fin = open('test.py')
	print('Everything went well!')

except KeyError:  # 虽然可以 catch KeyError, 但发生了 FileNotFoundError
	print('Something went wrong!')

Traceback (most recent call last):
  File "<pyshell#19>", line 2, in <module>
    fin = open('test.py')

FileNotFoundError: [Errno 2] No such file or directory: 'test.py'

因此,若仅需要捕获异常,不指定 except 语句捕获的异常类型将更为保险和省事 (毕竟异常类型辣么多…)。

与此同时,若要 捕获处理指定类型异常,一方面,可以 将需要捕获的异常类型全都放在同一个 tuple 中

>>> try:
	fin = open('eval.py')
	print('Everything goes well!')

except (FileExistsError, FileNotFoundError):  # 异常类型 tuple
	print('There is a FileExistsError or FileNotFoundError!')

There is a FileExistsError or FileNotFoundError!

这样做的优点是简洁明了,统一捕获异常处理;缺点是不能够“特事特办” —— except 子句的异常处理将缺乏针对性

为实现对多种不同的特定类型异常的 分别捕获处理,可以 令一个 try 语句对应多个 except 语句,例如:

>>> try:
	fin = open('eval.py')
	print('Everything goes well!')

except FileExistsError:  # 捕获特定类型异常
	print('There is a FileExistsError!')

except FileNotFoundError:  # 捕获特定类型异常
	print('There is a FileNotFoundError!')

There is a FileNotFoundError!

多个 except 子句串行执行,对于异常 “有则捕获,无则通过”。注意,若发生的异常和 except 子句指定的异常类是同一个类或者是其基类,则可以 兼容并照常捕获处理 (比如异常指定为 Exception 时可捕获大部分的异常,因为所有内置的非系统退出类异常/用户自定义异常都派生自此类),但 反之不成立 (except 子句指定的异常是实际发生异常的子类/派生类时则无法捕获)。

此外,还可以使用 as 关键字指定 except 语句所捕获异常的别名,

>>> try:
	fin = open('eval.py')
	print('Everything goes well!')

except FileNotFoundError as error:  # as 关键字指定异常别名 
	print("Error information:{0}".format(error))

Error information:[Errno 2] No such file or directory: 'eval.py'
2.1.3 小结

总而言之,常见的用法仍是:令前面的 except 子句指定特定类型异常,令最后一个 except 子句忽略异常名以用作通配符,然后打印一个未知错误信息,并用 raise 关键字抛出异常。如下所示:

>>> import sys

>>> try:
	fin = open('eval.py')
	print('Everything goes well!')

except FileExistsError as error:
	print("Error information:{0}".format(error))

except:
	print("Unexpected error:", sys.exc_info()[0])
	raise  # 抛出异常

Unexpected error: <class 'FileNotFoundError'>
Traceback (most recent call last):
  File "<pyshell#36>", line 2, in <module>
    fin = open('eval.py')

FileNotFoundError: [Errno 2] No such file or directory: 'eval.py'

以上即为篇幅最大、最为基本而详实的用法说明。


2.2 try-except-else 语句

try 语句除了可以后跟一至多个 except 子句,还可以选用 else 子句。若使用 else 子句,则必须将其后接于 except 子句后,且只能有一个 else 子句。else 子句将在 try 子句未发生任何异常时执行

>>> try:
	fin = open('oneline.txt')
	print('Everything goes well!')

except FileExistsError:
	print('There is a FileExistsError!')

except FileNotFoundError:
	print('There is a FileNotFoundError!')

else:
	print(fin.readlines())  # 读取一行
	fin.close()  # 关闭/释放文件对象 fin

Everything goes well!

['I Love Python!']

上例顺利执行 try 语句,读取了一个只有一行的 txt 文件,打印出成功读取信息,并因此跳过各个 except 子句。然后,执行 else 子句,读取 txt 文件的一行内容并打印之,最后关闭 fin 文件对象。

通常,使用 else 子句比将所有语句都放在 try 语句中灵活性更强,效果更好,因为如此 可避免一些难以预料且 except 无法捕获的异常。异常处理并不仅仅处理那些直接发生在 try 语句中的异常,而且还 能处理子句中调用的函数 (甚至间接调用的函数) 里抛出的异常。例如:

>>> def wrong():
        num = 6 / 0

>>> try:
        wrong()

except ZeroDivisionError as error:
        print('Handling run-time error:', error)

Handling run-time error: division by zero

总之,对于在 try子句不引发异常时必须执行的代码而言,else 子句很有用。


2.3 try-except-else-finally 语句

除了 else 子句,还有另一个常用可选子句 —— finally 子句。若使用 finally 子句,则必须将其后 接于最后,且 只能有一个 finally 子句。无论异常有无发生,finally 子句都将执行。因此,finally 子句常用于存放一些必定要执行的内容或操作,例如:

>>> try:
	fin = open('oneline.txt')
	print('Everything goes well!')

except FileExistsError:
	print('There is a FileExistsError!')

except FileNotFoundError:
	print('There is a FileNotFoundError!')

else:
	print(fin.readlines())
	fin.close()

finally:
	print("Operations are Finished!")

Everything goes well!
['I Love Python!']
Operations are Finished!

finally 子句常用于定义 无论在任何情况下都会执行的清理行为。若一个异常在 try 子句里 (或在 except 子句和 else 子句里) 被抛出,而又没有任何的 except 子句将其捕获,那么该异常 将会在 finally 子句执行后被抛出。例如:

>>> def divide(x, y):
        try:
            result = x / y

        except ZeroDivisionError:
            print("division by zero!")

        else:
            print("the result is", result)

        finally:
            print("executing finally clause")

>>> divide('6', '3')
executing finally clause

Traceback (most recent call last):
  File "<pyshell#18>", line 1, in <module>
    divide('6', '3')

  File "<pyshell#17>", line 3, in divide
    result = x / y

TypeError: unsupported operand type(s) for /: 'str' and 'str'

2.4 小结

try-except-else-finally 语句简图:

img


三、异常抛出 (exception raising)

Python 通过 raise 语句强制抛出一个指定异常,其语法格式为:

raise [Exception [, args [, traceback]]]

raise 的唯一参数即 要抛出的指定异常,该参数必须是一个异常实例或异常类 (即派生自 Exception 的类)。若传递的是一个异常类,它将通过调用无参数的构造函数实现隐式实例化。

若只想确定是否抛出了异常而并不想去处理它,那么一个 无参数的 raise 语句 便可 将当前在处理的异常****再次抛出。例如:

>>> try:
	raise NameError('HiThere')  # 指定抛出异常名及其 Trackback 提示语

except NameError:
	print('An exception flew by!')

An exception flew by!

# ------------------------------------------------------------------------------
>>> try:
	raise NameError('Hello')  # 指定抛出异常名及其 Trackback 提示语

except NameError:
	print('An exception flew by!')
	raise  # 再次抛出, 对比上例

An exception flew by!
Traceback (most recent call last):
  File "<pyshell#1>", line 2, in <module>
    raise NameError('Hello')

NameError: Hello

如果令最后一个 raise 语句指定另一个类型的异常,则 Traceback 将按发生顺序显示这些 串联 的异常信息:

>>> try:
	raise NameError('Hello')

except NameError:
	print('An exception flew by!')
	raise KeyError

An exception flew by!
Traceback (most recent call last):
  File "<pyshell#8>", line 2, in <module>
    raise NameError('Hello')
    
NameError: Hello

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "<pyshell#8>", line 5, in <module>
    raise KeyError

KeyError

总之,使用 raise 语句可引发内置异常,通常用于测试异常处理程序或报告错误条件


四、异常自定义 (exception customizing)

在 Python 中,所有异常必须为一个派生自 BaseException 的类的实例 (BaseException 是所有内置异常的基类)。在带有指定一个特定类的 except 子句的 try 语句中,该子句将处理派生自 BaseException 类的异常类 (但也有例外)。通过子类化创建的两个不相关异常类永远不等效的,即便二者名称相同。

除了 Python 内置异常类,还可以将内置异常类子类化以定义新的异常。因为 BaseException 类不应被用户自定义类直接继承,所以 鼓励从 Exception 类或其子类来派生新的异常

例如,可以直接或间接继承 Exception 类实现一个自定义的异常类:

>>> class MyError(Exception):
        ''' 自定义异常类需要继承自 Exception 类 '''
        def __init__(self, value):  # 重写父类 Exception 的构造方法 __init__() 以覆盖之
            self.value = value
            
        def __str__(self):
            return repr(self.value)

>>> try:
        raise MyError(6)

except MyError as error:
        print('My exception occurred, value:', error.value)

My exception occurred, value: 6

自定义异常类可执行任何其他类能执行的任何操作,但实现时通常只提供许多属性和少量方法,以允许处理程序为异常提取有关错误的信息的同时确保简洁性。

此外,在创建可能引发多个不同异常的模块时,通常的做法是为该模块定义的各种异常创建一个基类 (作为基础的异常类),然后基于该基类为不同的错误条件创建不同的子类。例如:

class Error(Exception):
    """Base class for exceptions in this module."""
    pass

class InputError(Error):
    """Exception raised for errors in the input.
    Attributes:
        expression -- input expression in which the error occurred
        message -- explanation of the error
    """

    def __init__(self, expression, message):
        self.expression = expression
        self.message = message

class TransitionError(Error):
    """Raised when an operation attempts a state transition that's not
    allowed.
    Attributes:
        previous -- state at beginning of transition
        next -- attempted new state
        message -- explanation of why the specific transition is not allowed
    """

    def __init__(self, previous, next, message):
        self.previous = previous
        self.next = next
        self.message = message

大多数异常的名称都定义为以 Error 结尾,类似于标准异常的命名。


五、预定义的清理行为 (with statement)

在 Python 中,一些对象定义了在不再需要该对象时要执行的标准清理行为,无论使用该对象的操作成败与否。例如,下面的示例它尝试打开一个文件并打印其内容:

for line in open("test.txt"):
    print(line, end="")

这段代码的问题在于,当执行完毕后,文件会在一段不确定的时间内保持打开状态,而未被显式地关闭!这在简单脚本中无所谓,但对较大的应用程序而言可能是个问题。

因为 with 语句可以实现资源的精确分配与释放,所有在本例场景下,with 语句能够保证诸如文件之类的对象在使用完后,一定会正确地执行其清理方法,例如:

>>> with open("train.txt") as f:
	for line in f:
		print(line, end="")

从而,执行完语句后,既便上述代码在处理过程中出现问题,也能够确保文件 f 总是被关闭


六、断言 (asserting)

除了上述引发异常的方式,Python 中还有一个 assert 断言语句能够触发异常。 assert 语句常用于 判断表达式,并 在表达式条件为 False 时触发异常 (准确地说是表达式的 bool 逻辑值为 False 时)。其语法格式为:

assert expression

实质等价于:

if not expression:
    raise AssertionError

例如:

>>> assert True

>>> assert False  # 表达式的 bool 逻辑值为 False 将引发异常

Traceback (most recent call last):
  File "<pyshell#22>", line 1, in <module>
    assert False

AssertionError

>>> assert 1 > 0

>>> assert 1 < 0  # 表达式的 bool 逻辑值为 False 将引发异常

Traceback (most recent call last):
  File "<pyshell#25>", line 1, in <module>
    assert 1 < 0

AssertionError

与此同时,assert 后也可指定参数:

assert expression [, arguments]

实质等价于:

if not expression:
    raise AssertionError(arguments)

例如:

>>> assert 1 == 0, '1 is not equal to 0'

Traceback (most recent call last):
  File "<pyshell#27>", line 1, in <module>
    assert 1 == 0, '1 is not equal to 0'

AssertionError: 1 is not equal to 0

总之,assert 语句能够在条件不满足程序运行的情况下直接返回错误,而不必等待程序运行后出现崩溃的情况。例如,某代码只能在 Linux 下运行,于是可以先判断当前系统是否符合条件:

import sys

assert ('linux' in sys.platform), "该代码只能在 Linux 下执行"

# 接下来要执行的代码

七、小结 (summary)

本文中主要说明的 Python 异常相关常见关键字 (还有 assert 用于断言):

img


八、彩蛋 —— finally 和 return 谁的优先级更高?(选读)

已知 try-finally 语句中,无论 try 子句正常执行还是引发异常,finally 子句最终都能被执行 (用于收尾)。又知, return 作为函数的出口,每逢 return 语句,函数都将结束运行。那么问题来了,如果 同时存在 finally 和 return,谁的优先级更高,Python 解释器将如何抉择?测试一下:

>>> def test():  # 在 try 子句与 finally 子句均书写 return 语句

	try:
		return "try"

	finally:
		return "finally"
    
>>> test()

'finally'

可见,Python 解释器忽略了 try 子句中的 return 语句 (此处的 return 并非函数终点),以确保 finally 子句的执行。

但其实,try 子句中的 return 语句并非被忽视。已知函数未显式定义 return 语句时,将隐式地返回 None (返回值为 None)。那么,若 finally 子句中未显式定义 return 语句时,是否应返回 None 呢?验证一下:

>>> def val():  # 只在 try 子句中书写 return 语句

	try:
		return "try"

	finally:
		...                # ... 等同于 pass

>>> val()

'try'

# ------------------------------------------------------------------------------

>>> def val():  # 只在 try 子句中书写 return 语句
	try:
		return "try"

	finally:
		print("finally")

>>> val()

finally
'try'

# ------------------------------------------------------------------------------

>>> def val():  # 在 try 子句与 finally 子句中均不书写 return 语句
	try:
		print("try")

	finally:
		print("finally")

>>> val()

try
finally

可见,未在 finally 子句中显式定义 return 语句时,try 子句中的 return 语句还是有效的。总而言之,在包含 try-finally 语句的函数中:

若 finally 子句中显式定义了 return 语句,那么该 return 语句会直接覆盖 try 子句中的 return 语句 (如果有);

若 finally 子句中未显式定义 return 语句,那么 try 子句中的 return 语句 (如果有) 将生效


详解with语句 (上下文管理器)

目录

一、绪论

二、基本概念

三、简单示例

四、原理阐述

五、基于类实现上下文管理器 —— 自定义实例与说明

5.1 定义前提

5.2 简单实现

5.3 异常处理

六、基于生成器实现上下文管理器



一、绪论

详细说明另一种处理方式,即用于 上下文管理器 (Context manager)with 语句。


二、基本概念

首先,开门见山、先入为主地给出上下文管理器与 with 语句的相关基本概念:

  1. **上下文管理协议 (Context management protocol):**包含 enter() 和 exit() 方法,支持该协议的对象要求必须实现这两个方法,以成为上下文管理器。
  2. **上下文管理器 (Context manager):**支持上下文管理协议 —— 实现了 enter() 和 exit() 方法的对象。上下文管理器用于 定义执行 with 语句时要建立的运行时上下文,并负责执行 with 语句块 上下文的进入与退出 操作。通常使用 with 语句调用上下文管理器,也可通过直接调用其方法来使用。
  3. **运行时上下文 (Runtime context):**由上下文管理器创建、通过 enter() 和 exit() 方法实现进入和退出。其中,enter() 方法在语句体执行开始前进入运行时上下文,exit() 在语句体执行结束后退出运行时上下文。with 语句支持“运行时上下文”这一概念。
  4. **上下文表达式 (Context expression):**with 语句中跟在关键字 with 后的表达式。该表达式返回一个上下文管理器对象。
  5. **语句体 (with-body):**即 with 语句中的代码块。执行语句体前,会调用上下文管理器的 enter() 方法;执行语句体后,会调用上下文管理器 exit() 方法。

上下文管理器常用于资源的精确分配和释放,且常使用 with 语句调用 (说明还有其他方式)。

上述概念虽然较严谨而晦涩,但尚不必畏难。后续将先展示一个简单例子,再给出原理定义,后作出实例分析,且重要内容会强调多次,从而促进理解和领悟。(上述概念需要反复回头看理解)


三、简单示例

假如现在要打开一个文件,并写入一条数据,然后关闭之。那么,常见的 try- finally 语句实现如下:

>>> file = open('test.txt', 'w')  # 文件对象

>>> try:
	file.write('Hello!')  # 写入

finally:
	file.close()  # 关闭

这样写固然没错,但其实存在另一种等价写法 —— with 语句:

>>> with open('test.txt', 'w') as file:
	file.write('Hello!')

可见,使用 with 语句,不但简化了许多样板代码 (boilerplate code),而且确保⽂件会被关闭,而无须关心如何退出众多子句构成的代码块。

以上便是上下文管理器实现资源加锁/解锁关闭已打开文件的一个常见用例,是异常处理的“强化版本”。接下来,将说明这背后的原理。


四、原理阐述

Python 对一些内建对象进行改进,加入了对上下文管理器的支持。上下文管理器的典型用途包括:保存和恢复各种全局状态、锁定和解锁资源、关闭已打开的文件 等。

with 语句则用于包装带有使用上下文管理器定义方法的代码块的执行,从而允许对普通的 try-except-finally 语句使用一种模式封装以方便使用。

with 语句的语法格式为:

with context_expression [as target(s)]:
    with-body

with 语句的执行过程由代码实现说明如下:

context_manager = context_expression  # 由上下文表达式返回一个上下文管理器对象
exit = type(context_manager).__exit__  # 载入上下文管理器的 __exit__() 方法
value = type(context_manager).__enter__(context_manager)  # 发起调用上下文管理器的 __enter__() 方法
exc = True   # True 表示正常执行, 即便有异常也忽略;False 表示重新抛出异常, 需对异常处理

try:
    try:
        target = value  # 若指定 as 子句 (赋值目标变量 traget(x))
        with-body  # 执行 with-body (with 语句体/代码块)

    except:
        exc = False  # 执行过程中有异常发生
        # 若 __exit__ 返回 True, 则异常被忽略;若 __exit__ 返回 False, 则重新抛出异常
        # 由外层代码对异常进行处理
        if not exit(context_manager, *sys.exc_info()):
            raise
            
finally:
    # 正常执行完毕退出
    # 或通过 statement-body 中的 break/continue/return 语句退出
    # 或忽略异常退出
    if exc:
        exit(context_manager, None, None, None) 

    # 缺省返回 None, None 在 bool 上下文中看做是 False (bool(None)=False)

with 语句的执行过程文字说明如下:

  1. 上下文表达式 (context_expression) 求值以生成上下文管理器 (context_manager)
  2. 载入(保存)上下文管理器的 exit() 方法以便后续使用。
  3. 调用上下文管理器的 enter() 方法,进入关联到此对象的运行时上下文 (runtime context)
  4. 若 with 语句指定了 as 子句,则上下文管理器 enter() 方法的返回值将赋给目标变量 target(s)。target(s) 既可为单变量,也可为包含多元素的 tuple (必须由圆括号 () 包裹)。注意,with 语句会保证若 enter() 方法返回时未发生错误的情况下,exit() 方法总能被调用。 因此,若在对目标变量(或 tuple) 赋值期间发生错误,则会将其视为在 with 语句体内部发生的错误,总之详见第 6 步。
  5. 执行 with 语句体/代码块。
  6. 不论如何,总会调用上下文管理器的 exit() 方法,退出关联到此对象的运行时上下文 (runtime context),而__exit__() 方法通常负责执行“清理”工作,如释放资源等。
  • 注意,若由异常导致 with 语句体的退出,则将使用 sys.exc_info 获取异常信息 —— 类型 (exc_type)、值 (exc_value) 及回溯信息 (traceback) 作为参数,调用 exit(exc_type, exc_value, exc_traceback) 方法;
  • 若由异常导致 with 语句体的退出,且 exit() 方法的返回值为 False,则该异常会被重新引发;若 exit() 方法的返回值为 True,则该异常会被抑制/屏蔽,并将继续执行 with 语句后的语句;
  • 若执行过程中未出现异常,或 with 语句体中执行了 break / continue / return 语句,则将以 3 个 None 作为参数调用 exit(None, None, None) 方法;
  • 若由异常之外的任何原因导致 with 语句体的退出,则 exit() 方法的返回值将被忽略,并在该类退出正常的发生位置继续执行。

以上是较为严谨但却晦涩的原理概述,接下来将结合实例进行说明。


五、基于类实现上下文管理器 —— 自定义实例与说明

5.1 定义前提

自定义上下文管理器类 (即自定义支持上下文管理协议的类),至少要实现 enter() 和 exit() 方法。因为 with 语句总会先检查是否提供 exit() 方法,后检查是否定义 enter() 方法,二者任缺其一都会导致 AttributeError。

5.2 简单实现

接下来将构造一个上下文管理器类 File,例如:

>>> class File(object):

	def __init__(self, file_name, method):
		self.file_obj = open(file_name, method)

	def __enter__(self):
		return self.file_obj

	def __exit__(self, type, value, traceback):
		self.file_obj.close()

由于定义了 enter() 和 exit() 方法支持上下文管理协议,File 类成为了一个上下文管理器类,因此可在 with 语句中调用它:

>>> with File('test.txt', 'w') as file:
	file.write('World!')

表面上看,with 语句完成的操作是:读取 test.txt 文件并写入 World! 。

实质上,内部进行的步骤是:with 语句先暂存 File 类的 exit 方法,然后调用 File 类的 enter 方法 (进入上下文管理器的运行时上下文)。 enter 方法打开文件 test.txt 并返回给 with 语句,而打开的文件句柄被传递给变量 file (将文件对象赋值给变量 file)。接着,执行 with 语句体/代码块,令 file 调用 write() 方法在文件 test.txt 中写入 World! 。最后,with 语句调用先前暂存的 exit 方法 (退出上下文管理器的运行时上下文),正常关闭文件 test.txt 。

5.3 异常处理

其实,enter 方法含义简单且无参数,其原型为:

  • **context_manager.enter():**进入上下文管理器的运行时上下文,在语句体执行前调用。若指定了 as 子句,with 语句将该方法的返回值赋值给 as 子句中的 target(s)。

eixt 方法则复杂些,有三个形参 exc_type, exc_value, exc_traceback,其原型为:

  • **context_manager.exit(exc_type, exc_value, exc_traceback):**退出与上下文管理器相关的运行时上下文,返回一个bool 值表示是否对发生的异常进行处理。参数为引起退出操作的异常信息,如果退出时未发生异常,则3个参数均为None。若发生异常,返回 True 表示无需处理异常,返回 False 则会在退出该方法后重新抛出异常以由 with 语句之外的代码逻辑进行处理。若该方法内部产生异常,则会取代由 statement-body 中语句产生的异常。要处理异常时,不应显示重新抛出异常 (即不能重新抛出通过参数传递进来的异常),只需将返回值设为 False 即可。之后,上下文管理代码会检测是否 exit() 失败来处理异常。

在 5.2 节内部步骤中,“打开的文件句柄传递”与“with 调用 exit 方法”之间,若发生异常,Python 将使用 sys.exc_info 获取异常信息,将异常类型 (exc_type)、值 (exc_value) 及回溯信息 (traceback) 作为实参,调用 exit(exc_type, exc_value, exc_traceback) 方法,从而让 exit 方法决定如何关闭文件以及是否需要其他步骤

事实上,在访问文件对象时,很多情况都可能导致异常发生。例如,调用一个不支持 (因为不存在) 的文件对象方法:

>>> with File('test.txt', 'w') as file:
	file.undefinded_function('Bye!')  # 未定义 undefinded_function() 

Traceback (most recent call last):
  File "<pyshell#25>", line 2, in <module>
    file.undefinded_function('Bye!')

AttributeError: '_io.TextIOWrapper' object has no attribute 'undefinded_function'

当异常发生时,with 语句会采取如下措施:

  1. 将发生异常的类型 (exc_type)、值 (exc_value) 及回溯信息 (traceback) 作为实参传递给 exit 方法;
  2. exit 方法处理异常;
  3. exit 方法返回 True,则说明该异常已被“处理”,无需再处理;
  4. exit 方法返回 True 之外的任何内容,则该异常将被 with 语句抛出;

在 5.2 节的例子 —— File 类中,exit 方法返回 None (若无显式定义的 return 语句,方法默认返回 None)。因此,5.3 节的上例在调用不存在的 undefinded_function 方法时,抛出了异常。

为此,可以尝试完善 exit 方法以处理异常:

>>> class File(object):

	def __init__(self, file_name, method):
		self.file_obj = open(file_name, method)

	def __enter__(self):
		return self.file_obj

	def __exit__(self, type, value, traceback):
		print("Exception has been handled")  # 提示语

		self.file_obj.close()  # 关闭文件对象
		print("File has been closed")  # 提示语

		return True  # 返回 True 说明异常已被处理

这时,再试图触发异常:

>>> with File('test.txt', 'w') as file:
	file.undefinded_function('Bye!')

Exception has been handled
File has been closed

完全按照预期发生!因为自定义的 exit 方法返回了 True,所有没有异常会被 with 语句抛出。

同时,提示语 “File has been closed” 的输出也说明了发生异常时,with 语句体/代码块中并未执行完,但保证会将文件关闭 (确保资源被释放掉),从而体现了一定的安全性。因此,可以自定义上下文管理器来对软件系统中的资源进行管理,比如数据库连接、共享资源的访问控制等

此外,如果有多个上下文管理器,则会视为存在多个语句嵌套处理。即:

>>> with CM1 as cm1, CM2 as cm2:
	suite

# --------------------------------- 等价于 ----------------------------------
>>> with CM1 as cm1:
	with CM2 as cm2:

		suite

不过通常不会这样复杂地嵌套使用,因此仅作展示不作赘述。


六、基于生成器实现上下文管理器

事实上,除了自定义类,还可以基于装饰器(decorators) 和生成器(generators) 实现上下文管理器。

Python 中的 contextlib 模块正用于此。contextlib 模块提供了3个对象:装饰器 contextmanager、函数 nested 和上下文管理器 closing。使用这些对象,可包装已有生成器函数或对象,从而加入对上下文管理协议的支持,避免了专门编写上下文管理器来支持 with 语句。

然而,contextlib 模块平时似乎用得并不多,因此点到为止,不再赘述。有兴趣可以看一下 contextlib 模块的文档


  • 21
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
Python,冒号有多种用途。首先,冒号可以用来定义代码块,例如在if语句、循环语句和函数定义。冒号后面的缩进代码将被视为该代码块的一部分。其次,冒号还可以用来声明函数的参数和返回值的类型。这是在Python 3引入的新特性,可以使用函数注释来标注参数和返回值的类型。最后,冒号还可以用来切片操作,用于提取列表、数组等数据结构的一部分元素。在切片操作,冒号前面的数字表示起始位置,冒号后面的数字表示结束位置。如果冒号前面或后面的数字为空,则表示从开头或到结尾。\[1\]\[2\]\[3\] #### 引用[.reference_title] - *1* *2* [python杂记——箭头(->)和冒号(:)说明](https://blog.csdn.net/itlilyer/article/details/120633337)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^control_2,239^v3^insert_chatgpt"}} ] [.reference_item] - *3* [python冒号(:)的作用](https://blog.csdn.net/weixin_46813313/article/details/113696218)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^control_2,239^v3^insert_chatgpt"}} ] [.reference_item] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小酒馆燃着灯

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值