是否可以在上下文管理器的 __exit__() 方法中访问上下文对象(代码块)?

问题描述 投票:0回答:2

如果引发异常(可能多次,可能有延迟),我想在 exit() 方法中再次调用代码对象。我知道使用装饰器很容易做到,但我的动机是有时我想重复一些我不想提取到单独的函数并装饰它的代码片段。我正在寻找类似的东西:

class again(object):
    def __enter__(self):
        pass

    def __exit__(self, exc_type, exc_val, exc_tb):
        if exc_type is not None:
            ????        # Invoke the code object again
            return True # eat exception

它会像这样使用:

x = 0
with again():
    print x
    x += 1
    if x == 1:
         raise Exception('I hate 1')

预期输出为:

0
1  

我可以找到一种方法来获取代码对象。上下文管理器属性似乎都没有引用它(我想它并不是真正需要的,因为它的工作只是在之前和之后做一些事情)。

可以吗?

python contextmanager
2个回答
10
投票

with
块不作为单独的代码对象存在,所以不存在。请参阅这个类似的问题。在这种情况下,提问者试图做相反的事情(从代码块内部访问上下文管理器),但正如thisanswer所解释的那样,
with
块不是一个单独的范围,所以它实际上没有任何单独的状态。

您可以通过示例来了解这一点:

import contextlib
import dis

@contextlib.contextmanager
def silly():
    yield

def foo():
    print "Hello"
    with silly():
        print "Inside"
    print "Goodbye"

然后

>>> dis.dis(foo.__code__)
  2           0 LOAD_CONST               1 (u'Hello')
              3 PRINT_ITEM          
              4 PRINT_NEWLINE       

  3           5 LOAD_GLOBAL              0 (silly)
              8 CALL_FUNCTION            0
             11 SETUP_WITH              10 (to 24)
             14 POP_TOP             

  4          15 LOAD_CONST               2 (u'Inside')
             18 PRINT_ITEM          
             19 PRINT_NEWLINE       
             20 POP_BLOCK           
             21 LOAD_CONST               0 (None)
        >>   24 WITH_CLEANUP        
             25 END_FINALLY         

  5          26 LOAD_CONST               3 (u'Goodbye')
             29 PRINT_ITEM          
             30 PRINT_NEWLINE       
             31 LOAD_CONST               0 (None)
             34 RETURN_VALUE  

您可以看到

with
块的代码与其他所有内容一起位于函数的代码对象内部。它不作为单独的代码对象存在,并且与函数的其余代码没有区别。你无法以任何理智的方式将其取出(我的意思是,在不破解字节码的情况下)。


0
投票

虽然

with
块不作为单独的作用域存在,但您仍然可以从调用者到上下文管理器的帧的行号获取
with
块。

块的主体始终从第一个

INDENT
标记开始,将缩进深度增加 1,在考虑了所有嵌套的
INDENT
DEDENT
标记后,以
DEDENT
标记结束,该标记减少了缩进深度深度回到 0。

确定了

with
语句的主体后,就可以简单地编译包含在虚拟块(例如
if 1:
)中的主体,并使用调用者框架的全局和局部变量按照指定的次数执行它:

import sys
from linecache import getline
from tokenize import tokenize, INDENT, DEDENT

class again:
    def __init__(self, times=1):
        self.times = times

    def __enter__(self):
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        if exc_type:
            def readline():
                lineno = caller.f_lineno
                while line := getline(filename, lineno):
                    if lineno == caller.f_lineno:
                        line = line.lstrip() # dedent the with statement itself
                    yield line.encode()
                    lineno += 1
                yield b''
            caller = sys._getframe(1)
            filename = caller.f_code.co_filename
            first = end = depth = 0
            for token, _, (start, _), (end, _), _ in tokenize(readline().__next__):
                if token == INDENT:
                    depth += 1
                    if not first:
                        first = start
                elif token == DEDENT:
                    if depth == 1:
                        break
                    depth -= 1
            body = ''.join(
                getline(filename, caller.f_lineno + lineno - 1)
                for lineno in range(first, end)
            )
            code = compile('if 1:\n' + body, '\n' + body, 'exec')
            while self.times:
                try:
                    exec(code, caller.f_globals, caller.f_locals)
                    break
                except:
                    self.times -= 1
            else:
                return
        return True

这样:

x = 0
with again():
    print(x)
    x += 1
    if x == 1:
         raise Exception('I hate 1')

输出:

0
1

演示这里

© www.soinside.com 2019 - 2024. All rights reserved.