`
luozhaoyu
  • 浏览: 346690 次
  • 性别: Icon_minigender_1
  • 来自: 北京
社区版块
存档分类
最新评论

从with关键字到编写自己简单的ContextManager(二)

阅读更多
上文
contextlib.contextmanager的用法是怎样的?我摘抄一下模块源代码
引用
    Typical usage:

        @contextmanager
        def some_generator(<arguments>):
            <setup>
            try:
                yield <value>
            finally:
                <cleanup>

    This makes this:

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

    equivalent to this:

        <setup>
        try:
            <variable> = <value>
            <body>
        finally:
            <cleanup>

大致就是可以把程序中的主体,<body>从中抽取出来,放到with块之中。而之后处理的<finally>都可以放到some_generator函数里面。
some_generator函数的编写很好办。只是contextmanager这个装饰器应该作为一个函数好呢还是一个类好呢?这是个很有意思的问题,我也想了好一会,等下一起讨论下。先给出我的解法:
class MyGeneratorContextManager(object):
    def __init__(self, gen):
        print("__init__ called")
        self.gen = gen
    
    
    def __enter__(self):
        print("__enter__ called")
        return self.gen.next()
    
    
    def __exit__(self, exc_type, exc_val, exc_tb):
        print("__exit__called exc_type = %s, exc_val = %s, exc_tb = %s"\
              % (exc_type, exc_val, exc_tb))
        # 这里没有做异常处理,需要处理StopIteration异常
        # 不是用return也可以
        # 下面这句话将输出yield [1, 2, 3]后面的的打印语句end foo
        return self.gen.next()
        
    
def MyContextManager(func):
    def tmpf(*args):
        print("func info:", func)
        return MyGeneratorContextManager(func(*args))
    return tmpf


@MyContextManager
def foo(val):
    # 尝试用老方法捕捉错误
    try:
        print("start foo", val)
        yield [1, 2, 3]
        # 下面一行需要调用self.gen.next()才能输出
        print("end foo")
    except (Exception, AssertionError):
        # 但是实际上并没有捕捉到yield中的错误
        # except的功能完全被__exit__取代
        print("EXCEPTION ENCOUNTERED!")
    finally:
        print("FINALLY")


print("foo is ", foo)
print("foo() is ", foo("bbbb"))
print("\nWITH INFO BELOW:")
with foo("aaaa") as tmp:
    print("START WITH")
    #: tmp实际上就是yield穿过来的值
    print(tmp)
    for i in tmp:
        print(i)
    assert 1>2
    # 出错之后直接从with中跳出去,下面不可能被执行
    print("END WITH")

输出结果是:
# 首先可以看到foo的值是闭包中的tmpf函数
('foo is ', <function tmpf at 0x7fb78b15f140>)
('func info:', <function foo at 0x7fb78b15f0c8>)
__init__ called
('foo() is ', <__main__.MyGeneratorContextManager object at 0x7fb78b1591d0>)

WITH INFO BELOW:
# 请看,两次调用foo(),发现他们最终都是同一个foo函数
('func info:', <function foo at 0x7fb78b15f0c8>)
# 但是奇怪的是,函数被初始化了两次?这是因为这是个工厂模式,每次调用的函数虽然一样,但是会生成不同的类
__init__ called
__enter__ called
('start foo', 'aaaa')
START WITH
[1, 2, 3]
1
2
3
# assert触发的错误没有被except捕捉到!被__exit__函数捕捉到了
__exit__called exc_type = <type 'exceptions.AssertionError'>, exc_val = , exc_tb = <traceback object at 0x7fb78b15c2d8>
end foo
FINALLY
# 为什么跳出StopIteration异常?这是因为gen.next()已经走到头了,我们没有处理这异常
Traceback (most recent call last):
  File "/home/leonardo/Aptana Studio 3 Workspace/PythonStudy/src/thinking/mycontext_manager.py", line 68, in <module>
    print("END WITH")
  File "/home/leonardo/Aptana Studio 3 Workspace/PythonStudy/src/thinking/mycontext_manager.py", line 31, in __exit__
    return self.gen.next()
StopIteration

首先创建了一个工厂模式的MyContextManager,它实际上又是个闭包函数,也可以作为装饰器方便以后使用。

其次我定义了一个类MyGeneratorContextManager,这个函数在初始化的时候,就接收一个generator。请注意,接收的是generator而不是function
请看函数
def tmpf(*args):
        print("func info:", func)
        return MyGeneratorContextManager(func(*args))

func在这里虽然是一个函数,但是func(*args)是一个generator,忘记传参数就糟了

在__enter__中最紧要的是要
return self.gen.next()

因为我们在with中面对的是一个generator,如果不对其进行next(),这个函数是不会动的。

进入到with块之后,foo函数里yield的值会直接传给tmp,这个值无关紧要。with块中所有语句就好像全部被填到yield那个地方去了一样。

程序于是执行with块中的语句,当其中出现异常的时候,我们外围的except语句块应该迅速捕捉到这一点,并输出才对。实际上不是,实际上当foo函数出现异常的时候,__exit__函数是第一时间捕捉到这个异常的。通过它的打印信息我们可以看出。

当处理完这个异常之后,我们调用了一下self.gen.next(),这个语句保证field语句后面的语句会被执行。最后执行了finally中的内容。你看except语句块被完全架空了。

好,我们回过头来再看,我们到底实现了什么东西?我们想执行的是
引用
<setup>
        try:
            <variable> = <value>
            <body>
        finally:
            <cleanup>

现在我们把body单独抽出来了,放到with当中,把variable value替换成了yield value。这样做到掐头去尾,把前后不变的东西拿了出来,把内部的东西抽了出来。
咦?这不就是个装饰器么?错。装饰器是保持真身不懂,把前后改了,这是两个不同的方面。


最后我们回到我一开始提出的问题,为什么要设计一个mycontextmanager函数?可不可以不用工厂模式,直接用MyGeneratorContextManager函数一步到位?
这有两个前提条件要注意。第一,如果用class作为装饰器,这个类必须是可调用的(callable),如果将来我们要使用它,只可能它的调用__call__函数,因为它又要实现能在call之后调用__exit__,所以它只能返回self, type(self)之类的东西。第二,因为foo()是用在with语句中的,所以它必须是一个generator,也就是说foo.__call__()的返回结果是一个generator。那么可以找到有这么一个返回值,它既是self, type(self)这样类相关的类型,而且还是一个包含yield的generator么?:-)


要是对yield还不熟悉的朋友可能现在还不是很清楚,尤其是generator函数和with块中代码的关系,很可能还有一些把这代码再折腾折腾的想法。嗯,这里我就不写了,有问题给我留言吧!
分享到:
评论

相关推荐

    Python with关键字,上下文管理器,@contextmanager文件操作示例

    在Python编程语言中,`with`关键字和上下文管理器是高效、安全处理资源的关键工具,特别是涉及到文件操作。它们确保了资源(如文件)在使用完毕后会被正确关闭,即使在执行过程中出现了异常。本篇文章将深入探讨`...

    Python库 | scope_injected_contextmanager-0.0.2.tar.gz

    在使用这个库时,开发人员可以将需要管理资源的代码封装到`with`语句中,确保资源的生命周期得到正确的控制。这有助于防止资源泄露,提高代码的可靠性和可维护性。 在实际项目中,"scope_injected_contextmanager...

    Python中的上下文管理器和with语句的使用

    此外,Python 的 `contextlib` 模块提供了一个 `contextmanager` 装饰器,可以简化上下文管理器的编写,通过 `yield` 语句划分 `__enter__` 和 `__exit__` 的逻辑。 ```python from contextlib import ...

    Baekjoon_Online_Judge_Python:使用Python的알고리with제with이

    在Python中,"with"关键字用于创建一个上下文管理器,它会自动处理对象的获取和释放。例如,当我们需要读取或写入文件时,可以使用"with open() as f:"的形式来操作。这样,无论代码是否抛出异常,文件都会在离开...

    python_context_management_sandbox:研究Python上下文管理器,以便编写代码可能更优雅

    在这个例子中,`open('file.txt', 'r')`返回一个实现了上下文管理器的对象,我们在`with`关键字后将其赋值给变量`f`。当`with`块结束时,`__exit__`方法会被自动调用,确保文件被正确关闭,即使在处理文件时发生异常...

    python面试合集资源包

    以下是一份详尽的Python面试知识点概述,涵盖从基础到高级的主题。 1. **Python基础知识** - Python的哲学:简洁、可读性强的语法,"There should be one-- and preferably only one --obvious way to do it." - ...

    Professional-Python.pdf.pdf

    通过以上内容,读者可以了解到文档涵盖了Python编程的高级概念和实践,从函数的高级用法,到面向对象编程的深入探讨,再到数据处理的策略,以及对Python版本差异的比较、测试、命令行工具的使用,以及异步编程和编程...

    Python contextlib模块使用示例

    `contextlib.nested`函数用于同时管理多个上下文管理器,但请注意,自Python 3.7起,它已经被弃用,取而代之的是使用`async with`和`await`关键字的嵌套上下文管理器。然而,对于旧版本的Python,`nested`可以这样...

    Python进阶

    考虑到Python 2和Python 3之间的一些差异,在编写兼容两者的代码时需要注意版本之间的兼容性问题。 **示例代码**: ```python from __future__ import print_function print("Hello, Python 2 and 3!") ``` #### ...

    python 中的9个实用技巧,助你提高开发效率

    这样可以有效地从迭代器中提取特定范围内的元素,而无需先将其转换为列表。 ### 三、跳过可迭代对象的开头 处理文本文件时,经常需要跳过头部的注释或其他非数据行。使用`itertools.dropwhile`可以轻松地过滤掉不...

    holbertonschool-higher_level_programming

    6. **迭代器和生成器**:迭代器允许遍历任何可迭代对象,而生成器则是一种惰性计算的迭代器,可以通过 yield 关键字创建。使用生成器可以节省内存,尤其在处理大量数据时。 7. **装饰器**:装饰器是 Python 的一种...

Global site tag (gtag.js) - Google Analytics