前言

with 语句会设置一个临时的上下文,交给上下文管理器对象控制,并且负责清理上下文。这么做能避免错误并减少样板代码,因此 API 更安全,而且更易于使用。除了自动关闭文件之外,with 块还有很多用途。

else

for/else

仅当for循环运行完毕时(即for循环没有被break语句中止)才运行else块。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
for x in range(3):
    print(x)
    if x > 100:
        break
else:
    print('x never greater than 100')
>>> 0
>>> 1
>>> 2
>>> x never greater than 100

for x in range(5):
    print(x)
    if x > 2:
        break
else:
    print('I am a dog')
>>> 0
>>> 1
>>> 2
>>> 3

while/else

仅当while循环因为条件为假值而退出时(即while循环没有被break语句中止)才运行else块。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
i = 0
while i < 10:
    i += 1
    print(i)
    if i > 2:
        break
else:
    print('I am a dog')
>>> 1
>>> 2
>>> 3

try/else

仅当try 块中没有异常抛出时才运行 else 块。官方文档还指出else子句抛出的异常不会由前面的except子句处理。

1
2
3
4
5
6
7
8
try:
    x = 1 / 0
except ZeroDivisionError:
    print('ZeroDivisionError')
else:
    print('I am a dog')

>>> ZeroDivisionError:

在所有情况下,如果异常或者returnbreakcontinue语句导致控制权跳到了复合语句的主块之外, else子句也会被跳过。

ps:本书作者认为then能替代这样语境下的else, 但是我们的Guido极其不喜欢加入新关键字

EAFP

取得原谅比获得许可容易(easier to ask for forgiveness than permission)。这是一种常见的 Python 编程风格,先假定存在有效的键或属性,如果假定不成立,那么捕获异常。这种风格简单明快,特点是代码中有很多 try 和 except 语句。与其他很多语言一样(如 C 语言),这种风格的对立面是LBYL风格。

LBYL

三思而后行(look before you leap)。这种编程风格在调用函数或查找属性或键之前显式测试前提条件。与 EAFP风格相反,这种风格的特点是代码中有很多if语句。在多线程环境中, LBYL风格可能会在“检查”和“行事”的空当引入条件竞争。

上下文管理器和with块

上下文管理器对象存在的目的是管理with语句,就像迭代器的存在是为了管理for句一样

with语句的目的是简化try/finally模式。这种模式用于保证一段代码运行完毕后执行某项操作,即便那段代码由于异常、return语句或sys.exit()调用而中止,也会执行指定的操作。finally子句中的代码通常用于释放重要的资源,或者还原临时变更的状态。

上下文管理器协议包含__enter____exit__两个方法。with语句开始运行时,会在上下文管理器对象上调用__enter__方法。with语句运行结束后,会在上下文管理器对象上调用__exit__方法,以此扮演finally子句的角色。

1
2
with open('filename.txt') as fp:
    pass

执行with后面的表达式得到的结果是上下文管理器对象, 而把值绑定到目标变量上(as子句)是在上下文管理器对象上调用__enter__方法的结果(即先生成一个上下文管理对象,然后调用__enter__()生成一个目标对象(在上面那个例子是TextIOWrapper), 返回绑定到fp上)

open()数返回TextIOWrapper类的实例,而该实例的__enter__方法返回self。不过,__enter__方法除了返回上下文管理器之外,还可能返回其他对象。

不管控制流程以哪种方式退出with块,都会在上下文管理器对象上调用__exit__方法,而不是__enter__方法返回的对象上调用。

with语句的as子句是可选的, 有些上下文管理器会返回None

一个例子

 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
class LookingGlass:
    def __enter__(self):
        import sys
        self.original_write = sys.stdout.write
        sys.stdout.write = self.reverse_write
        return 'I LOVE YOU'
    
    def reverse_write(self, text):
        self.original_write(text[::-1])
    
    def __exit__(self, exc_type, exc_value, traceback):
        import sys
        sys.stdout.write = self.original_write
        if exc_type is ZeroDivisionError:
            print('Please DO NOT divide by zero!')
            return True

with LookingGlass() as recv:
    print(recv)
    print('test output')
>>> UOY EVOL I
>>> tuptuo tset

print('test output')
>>> test output
  • 上下文管理器是LookingGlass类的实例;Python 在上下文管理器上调用__enter__方法,把返回结果绑定到变量recv上。
  • 由于更改了标准输出sys.stdout.write, 导致输出为反的
  • 执行完毕后退出作用域调用__exit__将之前的标准输出重新绑定回来
  • 如果__exit__方法返回None, 或者True之外的值, with块中的任何异常都会向上冒泡。

__exit__的三个参数

  • exc_type: 异常类, 如ZeroDivisionError
  • exc_value: 异常类实例,有时候会有额外的信息保存在异常类实例里
  • traceback: traceback对象

try/finally语句的finally块中调用sys.exc_info()(https://docs.python.org/3/library/sys.html#sys.exc_info), 得到的就是__exit__接收的这三个参数。鉴于with语句是为了取代大多数try/finally语句,而且通常需要调用sys.exc_info()来判断做什么清理操作,这种行为是合理的。

使用上下文管理器的场景

  • sqlite3模块用于管理事务
  • threading模块中用于维护锁、条件和信号
  • Decimal对象的算术运算设置环境
  • 为了测试临时给对象打补丁,参见 unittest.mock.patch 函数的文档

contextlib模块

自己定义上下文管理器类之前,先看一下 Python 标准库文档中的29.6 contextlib — Utilities for with-statement contexts

closing

如果对象提供了close()方法,但没有实现__enter____exit__协议,那么可以使用这个函数构建上下文管理器

suppress()

构建临时忽略指定异常的上下文管理器

@contextmanager

This function is a decorator that can be used to define a factory function for with statement context managers, without needing to create a class or separate enter() and exit() methods. The function being decorated must return a generator-iterator when called. This iterator must yield exactly one value, which will be bound to the targets in the with statement’s as clause, if any. 这个装饰器把简单的生成器函数变成上下文管理器,这样就不用创建类去实现管理器协议了

来自文档的一个抽象的例子

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
from contextlib import contextmanager

@contextmanager
def managed_resource(*args, **kwds):
    # Code to acquire resource, e.g.:
    resource = acquire_resource(*args, **kwds)
    try:
        yield resource
    finally:
        # Code to release resource, e.g.:
        release_resource(resource)

>>> with managed_resource(timeout=3600) as resource:
...     # Resource is released at the end of this block,
...     # even if code in the block raises an exception

在使用@contextmanager装饰的生成器中, yield语句的作用是把函数的定义体分成两部分: yield语句前面的所有代码在with块开始时(即解释器调用__enter__方法时)执行, yield语句后面的代码在with块结束时(即调用__exit__方法时)执行, yield产生的结果返回绑定给as后面的变量

使用@contextmanager重新实现之前的LookingGlass

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
from contextlib import contextmanager

@contextmanager
def looking_glass():
    import sys
    stdout = sys.stdout.write

    def reverse_write(text):
        stdout(text[::-1])
    
    sys.stdout.write = reverse_write
    yield 'I LOVE YOU'
    sys.stdout.write = stdout

with looking_glass() as recv:
    print(recv)
    print('test output')
>>> UOY EVOL I
>>> tuptuo tset

print('test output')
>>> test output

注意上面这个例子,如果在with块中抛出了异常, Python解释器会将其捕获,然后在looking_glass函数的yield表达式里再次抛出。但是, 那里没有处理错误的代码, 因此looking_glass函数会中止, 永远无法恢复成原来的sys.stdout.write方法,导致系统处于无效状态

修改后:

 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
from contextlib import contextmanager

@contextmanager
def looking_glass():
    import sys
    stdout = sys.stdout.write

    def reverse_write(text):
        stdout(text[::-1])
    
    sys.stdout.write = reverse_write
    try:
        yield 'I LOVE YOU'
    except ZeroDivisionError:
        msg = 'error'
    finally:
        sys.stdout.write = stdout
        if msg:
            print(error)

with looking_glass() as recv:
    print(recv)
    print('test output')
>>> UOY EVOL I
>>> tuptuo tset

print('test output')
>>> test output

ps: 这么写我还不如直接try/except/finally

前面说过,为了告诉解释器异常已经处理了, __exit__方法会返回 True,此时解释器会压制异常。如果__exit__方法没有显式返回一个值,那么解释器得到的是None, 然后向上冒泡异常。使用@contextmanager装饰器时,默认的行为是相反的:装饰器提供的__exit__方法假定发给生成器的所有异常都得到处理了, 因此应该压制异常. 如果 不想让@contextmanager压制异常, 必须在被装饰的函数中显式重新抛出异常。

使用@contextmanager装饰器时,要把yield语句放在try/finally语句中(或者放在with语句中),这是无法避免的,因为我们永远不知道上下文管理器的用户会在with块中做什么

ContextDecorator

这是个基类,用于定义基于类的上下文管理器。这样实现的上下文管理器可以当成装饰器来使用

 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
from contextlib import ContextDecorator

class mycontext(ContextDecorator):
    def __enter__(self):
        print('Starting')
        return self

    def __exit__(self, *exc):
        print('Finishing')
        return False

@mycontext()
def function():
    print('The bit in the middle')

function()
>>> Starting
>>> The bit in the middle
>>> Finishing

with mycontext():
    print('The bit in the middle')

>>> Starting
>>> The bit in the middle
>>> Finishing

ExitStack

这个上下文管理器能进入多个上下文管理器。with块结束时, ExitStack按照后进先出的顺序调用栈中各个上下文管理器的__exit__方法。如果事先不知道 with 块要进入多少个上下文管理器,可以使用这个类。例如,同时打开任意一个文件列表中的所有文件。

使用一个上下文管理器实现文件的同时原地读与写

作者Martijn Pieters, 博客地址
使用场景

1
2
3
4
5
6
7
8
9
import csv

with inplace(csvfilename, 'r', newline='') as (infh, outfh):
    reader = csv.reader(infh)
    writer = csv.writer(outfh)

    for row in reader:
        row += ['new', 'coloumn']
        writer.writerow(row)

inplace()的实现

 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
from contextlib import contextmanager
import io
import os


@contextmanager
def inplace(filename, mode='r', buffering=-1, encoding=None, errors=None,
            newline=None, backup_extension=None):
    """Allow for a file to be replaced with new content.

    yields a tuple of (readable, writable) file objects, where writable
    replaces readable.

    If an exception occurs, the old file is restored, removing the
    written data.

    mode should *not* use 'w', 'a' or '+'; only read-only-modes are supported.

    """

    # move existing file to backup, create new file with same permissions
    # borrowed extensively from the fileinput module
    if set(mode).intersection('wa+'):
        raise ValueError('Only read-only file modes can be used')

    backupfilename = filename + (backup_extension or os.extsep + 'bak')
    try:
        os.unlink(backupfilename)
    except os.error:
        pass
    os.rename(filename, backupfilename)
    readable = io.open(backupfilename, mode, buffering=buffering,
                       encoding=encoding, errors=errors, newline=newline)
    try:
        perm = os.fstat(readable.fileno()).st_mode
    except OSError:
        writable = open(filename, 'w' + mode.replace('r', ''),
                        buffering=buffering, encoding=encoding, errors=errors,
                        newline=newline)
    else:
        os_mode = os.O_CREAT | os.O_WRONLY | os.O_TRUNC
        if hasattr(os, 'O_BINARY'):
            os_mode |= os.O_BINARY
        fd = os.open(filename, os_mode, perm)
        writable = io.open(fd, "w" + mode.replace('r', ''), buffering=buffering,
                           encoding=encoding, errors=errors, newline=newline)
        try:
            if hasattr(os, 'chmod'):
                os.chmod(filename, perm)
        except OSError:
            pass
    try:
        yield readable, writable
    except Exception:
        # move backup back
        try:
            os.unlink(filename)
        except os.error:
            pass
        os.rename(backupfilename, filename)
        raise
    finally:
        readable.close()
        writable.close()
        try:
            os.unlink(backupfilename)
        except os.error:
            pass