首页 > 技术文章 > python中 with 用法及原理(上下文管理器) || python中 from contextlib import closing 的使用

hls-code 2021-09-13 14:42 原文

python中 with 用法及原理(上下文管理器)

前言

with 语句适用于对资源进行访问的场合,确保不管使用过程中是否发生异常都会执行必要的“清理”操作,释放资源,比如文件使用后自动关闭/线程中锁的自动获取和释放等。

问题引出

如下代码:

file = open("1.txt")
data = file.read()
file.close()

上面代码存在2个问题:

①文件读取发生异常,但没有进行任何处理;
②可能忘记关闭文件句柄;

改进

try:
    f = open('xxx')
except:
    print('fail to open')
    exit(-1)
try:
    do something
except:
    do something
finally:
    f.close()

虽然这段代码运行良好,但比较冗长。

而使用 with 语句的话,能够减少冗长,还能自动处理上下文环境产生的异常。如下面代码:

with open("1.txt") as file:
    data = file.read()

with 工作原理

①紧跟with后面的语句被求值后,返回对象的 __enter__ 方法被调用,返回值将被赋值给 as 后面的变量;

②当 with 语句体全部被执行完之后,将调用前面返回对象的 __exit__ 方法。

with工作原理代码示例:

class Sample:
    def __enter__(self):
        print("in __enter__")
        return "Foo"

    def __exit__(self, exc_type, exc_val, exc_tb):
        print("in __exit__")


def get_sample():
    return Sample()


with get_sample() as sample:
    print("Sample: ", sample)

运行结果:

整个运行过程如下:

(1) __enter__ 方法被执行;

(2) __enter__ 方法的返回值,在这个例子中是“Foo”,赋值给变量sample;

(3)执行代码块,打印sample变量的值为“Foo”;

(4) __exit__ 方法被调用;

【注意】 __exit__ 方法中有3个参数,  exc_type , exc_val ,exc_tb ,这些参数在异常处理中相当有用。

参数解释:

 exc_type :错误的类型

 exc_val :错误类型对应的值

 exc_tb :代码中错误发生的位置

示例代码:

class Sample:
    def __enter__(self):
        print('in enter')
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        print("type: ", exc_type)
        print("val: ", exc_val)
        print("tb: ", exc_tb)

    def do_something(self):
        bar = 1 / 0
        return bar + 10


with Sample() as sample:
    sample.do_something()

运行结果:

总结

实际上,在 with 后面的代码块抛出异常时, __exit__ 方法被执行。开发库时,清理资源,关闭文件等操作,都可以放在 __exit__ 方法中。

总之, with-as 表达式极大的简化了每次写 finally 的工作,这对代码的优雅性是有极大帮助的。

只要实现了 __enter__()  和  __exit__() 这两个方法的类都可以轻松创建上下文管理器,就能使用 with 。

如果有多项,可以这样写:

With open('1.txt') as f1, open('2.txt') as  f2:
    do something

with语句的原理

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

with语句的常用表达式:

with EXPR as VAR:  # EXPR可以是任意表达式
    BLOCK

其一般的执行过程是这样的:

1、执行EXPR,生成上下文管理器context_manager;

2、获取上下文管理器的 __exit()__ 方法,并保存起来用于之后的调用;

3、调用上下文管理器的 __enter__() 方法;如果使用了as子句,则将 __enter__() 方法的返回值赋值给as子句中的VAR;

4、执行BLOCK中的表达式;

5、不管是否执行过程中是否发生了异常,执行上下文管理器的 __exit__() 方法, __exit__() 方法负责执行“清理”工作,如释放资源等。

如果执行过程中没有出现异常,或者with语句体中执行了语句 break/continue/return ,则以None作为参数调用 __exit__(None, None, None) ;如果执行过程中出现异常,则使用 sys.exc_info 得到的异常信息为参数调用 __exit__(exc_type, exc_value, exc_traceback) ;

6、出现异常时,如果 __exit__(type, value, traceback) 返回False,则会重新抛出异常,让with之外的语句逻辑来处理异常,这也是通用做法;如果返回True,则忽略异常,不再对异常进行处理。

自定义上下文管理器

python的 with 语句是提供一个有效的机制,让代码更简练,同时在异常产生时,清理工作更简单。

示例1:

class DBManager(object):
    def __init__(self):
        pass

    def __enter__(self):
        print('__enter__')
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        print('__exit__')
        return True


def getInstance():
    return DBManager()


with getInstance() as dbManagerIns:
    print('with demo')

【注意】with后面必须跟一个上下文管理器,如果使用了as,则是把上下文管理器的  __enter__ 方法的返回值赋值给 target,target 可以是单个变量,或者由“()”括起来的元组【不能是仅仅由“,”分隔的变量列表,必须加“()”】

运行结果:

结果分析:当我们使用with语句的时候, __enter__ 方法被调用,并且将 __enter__ 方法返回值赋值给as后面的变量,并且在退出with的时候自动执行 __exit__ 方法。

示例2:

class With_work(object):
    def __enter__(self):
        """进入with语句的时候被调用"""
        print('①enter called')
        return "②打印对象f的值"

    def __exit__(self, exc_type, exc_val, exc_tb):
        """离开with的时候被with调用"""
        print('④exit called')


with With_work() as f:
    print(f)
    print('③打印with代码块中的输出')
print('⑤with代码块执行完毕之后的打印')

运行结果:

【注意】没有实现 __enter__()  和  __exit__() 这两个方法的类都不能创建上下文管理器,不能使用 with 语句。

例如:

class Door(object):
    def open(self):
        print('Door is opened')

    def close(self):
        print('Door is closed')


with Door() as d:
    d.open()

运行结果:

python中 from contextlib import closing 的使用

官方:https://docs.python.org/dev/library/contextlib.html

1、python中有些类没有实现 __enter__()  和  __exit__() 这两个方法也是可以使用 with 语句。但是前提是实现了 close() 语句。

例如:

import contextlib


class Door(object):
    def open(self):
        print('Door is opened')

    def close(self):
        print('Door is closed')


with contextlib.closing(Door()) as door:
    door.open()

运行结果:

2、 contextlib.closing(xxx) 原理如下:

class closing(object):
    """Context to automatically close something at the end of a block.
    Code like this:
        with closing(<module>.open(<arguments>)) as f:
            <block>
    is equivalent to this:
        f = <module>.open(<arguments>)
        try:
            <block>
        finally:
            f.close()
    """

    def __init__(self, thing):
        self.thing = thing

    def __enter__(self):
        return self.thing

    def __exit__(self, *exc_info):
        self.thing.close()

 contextlib.closing() 会自动帮某些类加上 __enter__() 和 __exit__() 这两个方法,使其满足上下文管理器的条件。

3、并不是只有类才能满足上下文管理器的条件,其实方法也可以实现一个上下文管理器【contextlib.contextmanager】

可以通过 @contextlib.contextmanager 装饰器的方式实现,但是其装饰的方法必须是一个生成器。 yield 关键字前半段用来表示 __enter__() 方法, yield 关键字后半段用来表示 __exit__()  方法。

例如:

import contextlib


@contextlib.contextmanager
def tag(name):
    print("<%s>" % name)
    yield
    print("</%s>" % name)


with tag(name="h1"):
    print('hello world!')

运行结果:

4、使用 contextlib.contextmanager 实现装饰器才能做的事情

例如:比如给一段代码加时间花费计算。

普通装饰器版本:【此方法解决此类需求要更加方便美观】

import time


def wrapper(func):
    def new_func(*args, **kwargs):
        t1 = time.time()
        ret = func(*args, **kwargs)
        t2 = time.time()
        print('cost time=', (t2 - t1))
        return ret

    return new_func


@wrapper
def hello(a, b):
    time.sleep(1)
    print('a + b = ', a + b)


if __name__ == '__main__':
    hello(100, 200)

运行结果:

contextlib.contextmanger版本:

import time
import contextlib


@contextlib.contextmanager
def cost_time():
    t1 = time.time()
    yield
    t2 = time.time()
    print('cost time=', t2 - t1)


with cost_time():
    time.sleep(1)
    a = 100
    b = 200
    print('a + b = ', a + b)

原理:
1、因为cost_time()方法是个生成器,所以运行__enter__()的时候,contextmanager调用 self.gen.next() 会跑到cost_time()方法的yield处,停住挂起,这个时候已经有了t1=time.time();
2、然后运行with语句体里面的语句,也就是a+b=300;
3、跑完后运行 __exit__() 的时候,contextmanager调用  self.gen.next() 会从cost_time()的yield的下一句开始一直到结束。这个时候有了t2=time.time(),t2-t1从而实现了统计cost_time的效果。

5、 contextlib.contextmanager 源码如下:

import sys

class GeneratorContextManager(object):
    """Helper for @contextmanager decorator."""

    def __init__(self, gen):
        self.gen = gen

    def __enter__(self):
        try:
            return self.gen.next()
        except StopIteration:
            raise RuntimeError("generator didn't yield")

    def __exit__(self, type, value, traceback):
        if type is None:
            try:
                self.gen.next()
            except StopIteration:
                return
            else:
                raise RuntimeError("generator didn't stop")
        else:
            if value is None:
                # Need to force instantiation so we can reliably
                # tell if we get the same exception back
                value = type()
            try:
                self.gen.throw(type, value, traceback)
                raise RuntimeError("generator didn't stop after throw()")
            except StopIteration as exc:
                return exc is not value
            except:
                if sys.exc_info()[1] is not value:
                    raise


def contextmanager(func):
    @wraps(func)
    def helper(*args, **kwds):
        return GeneratorContextManager(func(*args, **kwds))

    return helper

 

推荐阅读