前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Python中的with语句解析和实践

Python中的with语句解析和实践

作者头像
py3study
发布2020-01-10 14:41:25
8440
发布2020-01-10 14:41:25
举报
文章被收录于专栏:python3python3

Python中的with

with语句在我们的日常Python代码编写中时常会用到,我们通常知道可以用with语句来代替try…except…finally这样的写法,但是为什么它能够替代,如果在with中发生了异常怎么处理,这背后的原理却是并不是很明了。

最权威的说法肯定是来自官方文档的说法。

官方文档

先放出自己的小总结,然后翻译一下官方文档的with语句章节和with语句的上下文管理器章节。

小总结

  1. 上下文管理器提供了 __enter__()方法和__exit__()方法,在with语句中,如果用as指定了一个目标,会将__enter__()方法的返回值赋予这个目标。
  2. 运行中如果发生了异常,那么将会把异常的类型,值和追踪传递给__exit__()方法。如果__exit__()方法返回值为true,那么这个异常将会被抑制,否则这个异常将会被重新抛出。
  3. 如果没有发生异常,也会调用__exit__()方法,但是传入的参数为None, None, None。通常也是在这里放入代码进行如文件流/会话的关闭等操作。

with语句

地址在此

with是在2.5版本中引入的,with用于包装一个方法由上下文管理器(context manager)定义的代码块。with允许通常的 try…except…finally的使用模式被封装来方便使用。

代码语言:javascript
复制
with_stmt ::=  "with" with_item ("," with_item)* ":" suite
with_item ::=  expression ["as" target]

具有一个“item”的with语句的运行如下:

  1. 上下文表达式(在上面的with_item中给出的表达式)被执行来获取一个上下文管理器。
  2. 上下文管理器的__exit__()方法被加载以供后续使用。
  3. 上下文管理器的__enter__()方法被调用。
  4. 如果在with语句中指定了一个目标,则来自__enter__()的返回值被赋值给该目标。 注意,with语句保证如下:如果__enter__()方法返回没有发生错误,则__exit__()方法将始终会被调用。然而,如果在赋值给目标列表中发生了错误,其处理的方式和处理在嵌套的代码中发生的错误一样。参见步骤6。
  5. 运行嵌套的代码。
  6. 上下文管理器的__exit__()方法被调用。如果一个异常导致嵌套的代码退出,异常的类型,值和追踪将会被作为参数传递给__exit__()。否则,传递三个None参数。 如果嵌套的代码由于异常退出,并且从__exit__()方法返回的值为false,这个异常被重新抛出,如果返回的值为true,这个异常被抑制,程序会继续运行在with语句之后的语句。 如果嵌套的代码因为除了异常之外的任何理由退出,来自__exit__()方法的返回值将会被忽略,运行将会在这种类型离开执行之后的正常位置继续运行。

对于超过一个项这样的情况,上下文管理器被处理得就像多个with语句被嵌套一样:

代码语言:javascript
复制
with A() as a, B() as b:
    suite

和如下等价

代码语言:javascript
复制
with A() as a:
    with B() as b:
        suite

With语句的上下文管理器

地址在此

一个上下文管理器(context manager)是一个对象,其定义了运行一个with语句时候要建立的运行时上下文(runtime context)。上下文管理器掌控了何处进入,何处退出以及一个代码块运行所需的运行时上下文。上下文管理器通常在使用with语句的时候调用,但是也可以通过直接调用它们的方法来使用。

上下文管理器的典型使用包括存储和恢复各种全局状态,锁和解锁资源,关闭打开的文件等。

要获得更多上下文管理器相关信息,参考上下文管理器类型

代码语言:javascript
复制
object.__enter__(self)
进入和这个对象相关的运行时上下文,with语句会将这个方法的返回值绑定到用as语句指定的特定目标(如果有的话)。

object.__exit__(self, exc_type, exc_value, traceback)
离开和这个对象相关的运行时上下文,参数描述了导致离开上下文的异常。如果不需要异常离开上下文,所有的参数将会是None。

如果引入了异常,并且该方法希望抑制异常(例如,阻止异常的传播),则它应该返回true。否则在退出这个方法的时候,异常将会被正常处理。

注意,__exit__()方法不应该重新抛出传入的异常,这是调用者的职责。

实例

我们最常用的with语句莫过于

代码语言:javascript
复制
with open(file) as f

了吧,那么这个背后的原理是什么呢?

先看看__builtin__.py中的open

代码语言:javascript
复制
def open(name, mode=None, buffering=None): # real signature unknown; restored from __doc__
    """
    open(name[, mode[, buffering]]) -> file object

    Open a file using the file() type, returns a file object.  This is the
    preferred way to open a file.  See file.__doc__ for further information.
    """
    return file('/dev/null')

本质上就是返回一个file对象,再看看file对象(Python源代码中的Objects/fileobject.c),

代码语言:javascript
复制
  {"__enter__", (PyCFunction)file_self,     METH_NOARGS,  enter_doc},

__enter__()指向的是file_self。

代码语言:javascript
复制
static PyObject *
file_self(PyFileObject *f)
{
    if (f->f_fp == NULL)
        return err_closed();
    Py_INCREF(f);
    return (PyObject *)f;
}

所以__enter__()本质上就是返回一个file的对象。再看看__exit__()

代码语言:javascript
复制
{"__exit__",  (PyCFunction)file_exit,     METH_VARARGS, exit_doc}

然后其指向的方法

代码语言:javascript
复制
static PyObject *
file_exit(PyObject *f, PyObject *args)
{
    PyObject *ret = PyObject_CallMethod(f, "close", NULL);
    if (!ret)
        /* If error occurred, pass through */
        return NULL;
    Py_DECREF(ret);
    /* We cannot return the result of close since a true
     * value will be interpreted as "yes, swallow the
     * exception if one was raised inside the with block". */
    Py_RETURN_NONE;
}

可以看到,在__exit__()中间执行了f.close(),所以就不用我们自己再去手动执行了。同时返回值并不为true,所以任何的错误都会抛出。

自己玩玩

是时候自己玩一下了

代码语言:javascript
复制
# 玩with语句
class TestWith(object):

    def __init__(self):
        print "this is init of TestWith"

    def if_exist(self):
        print "I'm here"

    def __enter__(self):
        print "This is TestWith __enter__"
        return self

    def __exit__(self, *args):
        for i in args:
            print i
        print "Now __exit__"

with TestWith() as t:
    t.if_exist()

按照设想,首先得到TestWith的对象,然后t会得到__enter__()的结果,就是这个对象自身,然后调用一下if_exist函数,最后退出。运行的结果为

代码语言:javascript
复制
this is init of TestWith
This is TestWith __enter__
I'm here
None
None
None
Now __exit__

可见符合我们预期,并且在没有异常的时候传入__exit__()的为三个None。如果抛出了异常呢?

代码语言:javascript
复制
with TestWith() as t:
    1/0
    t.if_exist()

结果为

代码语言:javascript
复制
this is init of TestWith
This is TestWith __enter__
<type 'exceptions.ZeroDivisionError'>
integer division or modulo by zero
<traceback object at 0x1124558c0>
Now __exit__
---------------------------------------------------------------------------
ZeroDivisionError                         Traceback (most recent call last)
<ipython-input-9-53bd54b7b92a> in <module>()
     18 
     19 with TestWith() as t:
---> 20     1/0
     21     t.if_exist()

ZeroDivisionError: integer division or modulo by zero

由于1/0抛出异常,所以在这里退出,然后将异常传入到__exit__()中,由于__exit__()没有返回true,所以会重新抛出异常。

那如果我们改一下__exit__()函数,让它返回True呢?

代码语言:javascript
复制
class TestWith(object):

    def __init__(self):
        print "this is init of TestWith"

    def if_exist(self):
        print "I'm here"

    def __enter__(self):
        print "This is TestWith __enter__"
        return self

    def __exit__(self, *args):
        for i in args:
            print i
        print "Now __exit__"
        return True

with TestWith() as t:
    1/0
    t.if_exist()

我们得到输出如下:

代码语言:javascript
复制
this is init of TestWith
This is TestWith __enter__
<type 'exceptions.ZeroDivisionError'>
integer division or modulo by zero
<traceback object at 0x1124553b0>
Now __exit__

可见,确实是不会抛出异常。

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2019-07-29 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • Python中的with
    • 官方文档
      • 小总结
      • with语句
      • With语句的上下文管理器
    • 实例
      • 自己玩玩
      领券
      问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档