基本语法

with是用来操作上下文管理器的。

上下文管理器(Context Manager):支持上下文管理协议的对象,这种对象实现了__enter__()__exit__() 方法。上下文管理器定义执行 with 语句时要建立的运行时上下文,负责执行 with 语句块上下文中的进入与退出操作。通常使用 with 语句调用上下文管理器,也可以通过直接调用其方法来使用。

换句话说,实现了__enter__()__exit__()的就是上下文管理器,就可以使用with语句。

Python

1

2

with context_expression [as target(s)]:

    with-body

使用with语句,会先执行上下文管理器context_expression的__enter__()方法,如果该方法有返回值,将返回值赋值给as的变量,然后执行with语句块的内容,最后执行with-body

上下文管理器的基本语法:

Python

1

2

3

4

5

6

7

8

9

@contextlib.contextmanager

def some_generator(<arguments>):

    <setup>

    try:

        yield <value>

    finally:

        <cleanup>

with some_generator(<arguments>) as <variable>:

    <body>

将一个上下文管理器用try-catch的方式实现:

Python

1

2

3

4

5

6

    <setup>

    try:

        <variable> = <value>

        <body>

    finally:

        <cleanup>

原理

with的执行过程如下:

Python

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

    context_manager = context_expression

    exit = type(context_manager).__exit__  

    value = type(context_manager).__enter__(context_manager)

    exc = True   # True 表示正常执行,即便有异常也忽略;False 表示重新抛出异常,需要对异常进行处理

    try:

        try:

            target = value  # 如果使用了 as 子句

            with-body     # 执行 with-body

        except:

            # 执行过程中有异常发生

            exc = False

            # 如果 __exit__ 返回 True,则异常被忽略;如果返回 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 在布尔上下文中看做是 False

首先,执行__enter__(),返回值赋值给value

然后执行withwith-body代码块,如果上一步有value,则使用。

如果with-body执行过程中发生异常,执行__exit__()方法。没有发生异常也会执行__exit__()方法,区别就是exc变量。

如果发生异常退出的,__exit__()如果返回True,则异常被忽略;否则异常被重新抛出,交给with外面的代码处理。如果__exit__()发生了异常,则会取代with-body中的异常抛出。

要处理异常时,不要显示重新抛出异常,即不能重新抛出通过参数传递进来的异常,只需要将返回值设置为False就可以了。之后,上下文管理代码会检测是否 __exit__() 失败来处理异常。

自定义上下文管理器

定义一个实现了__enter__()__exit__()的对象即可。

  • context_manager.__enter__() :进入上下文管理器的运行时上下文,在语句体执行前调用。with 语句将该方法的返回值赋值给 as 子句中的 target,如果指定了 as 子句的话
  • context_manager.__exit__(exc_type, exc_value, exc_traceback) :退出与上下文管理器相关的运行时上下文,返回一个布尔值表示是否对with-body过程中发生的异常进行处理。参数表示引起退出操作的异常,如果退出时没有发生异常,则3个参数都为None。如果发生异常,返回True 表示不处理with-body的异常,否则会在退出该方法后重新抛出异常以由 with 语句之外的代码逻辑进行处理。如果该方法内部产生异常,则会取代由 statement-body中语句产生的异常。要处理异常时,不要显式重新抛出异常,即不能重新抛出通过参数传递进来的异常,只需要将返回值设置为 False就可以了。之后,上下文管理代码会检测是否 __exit__() 失败来处理异常。

contextlib模块

这个模块提供了很多使用的装饰器和函数。

装饰器 contextmanager

装饰一个生成器函数,函数被装饰过后,返回一个上下文管理器对象。

Python

1

2

3

4

5

6

7

8

9

10

11

12

13

14

    from contextlib import contextmanager

    @contextmanager

    def demo():

        print '[Allocate resources]'

        print 'Code before yield-statement executes in __enter__'

        yield '*** contextmanager demo ***'

        print 'Code after yield-statement executes in __exit__'

        print '[Free resources]'

    with demo() as value:

        # 这里,demo()返回了一个上下文管理器对象

        # 相当于 context_obj = demo(); with context_obj as value:...

        print 'Assigned Value: %s' % value

yield之前的语句,作为__enter__()执行,之后的语句作为__exit__()执行。yield作为__enter__()的返回值。

函数 nested

nested可以将多个上下文管理器组织在一起,避免使用嵌套 with 语句。

Python

1

2

    with nested(A(), B(), C()) as (X, Y, Z):

         # with-body code here

类似于:

Python

1

2

3

4

    with A() as X:

        with B() as Y:

            with C() as Z:

                 # with-body code here

上下文管理器 closing

closing会将包装的对象返回给as,并在with代码块结束的时候调用对象的close()方法:

Python

1

2

3

4

5

6

7

8

9

10

11

12

    class ClosingDemo(object):

        def __init__(self):

            self.acquire()

        def acquire(self):

            print 'Acquire resources.'

        def free(self):

            print 'Clean up any resources acquired.'

        def close(self):

            self.free()

    with closing(ClosingDemo()):

        print 'Using resources'

一个小问题

之前有点疑惑,为什么python内置的open()既没有被@contextmanager装饰器装饰,又不是一个对象,为什么可以使用with语句呢?

还在staskoverflow提问了这个问题。

原来,open()返回的是一个继承自IObase的对象,这个对象也是一个上下文管理器,所以就支持with语句了。

参考资料

  1. Expert-Python
  2. [翻译] 理解Python的With语句
  3. 浅谈 Python 的 with 语句(非常好的资料)
  4. 如何正确理解关键字”with”与上下文管理器