如果引发异常(可能多次,可能有延迟),我想在 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
我可以找到一种方法来获取代码对象。上下文管理器属性似乎都没有引用它(我想它并不是真正需要的,因为它的工作只是在之前和之后做一些事情)。
可以吗?
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
块的代码与其他所有内容一起位于函数的代码对象内部。它不作为单独的代码对象存在,并且与函数的其余代码没有区别。你无法以任何理智的方式将其取出(我的意思是,在不破解字节码的情况下)。
虽然
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
演示这里