我正在尝试使用一种技巧,使上下文管理器有条件地跳过其上下文中的代码,从而为Python创建一个快速而肮脏的缓存系统-参见Skipping execution of -with- block。我偶然发现了一个奇怪的失败案例,我想知道是否有人可以帮助理解和解决这个问题。
在有人这么说之前,我知道我在做什么很糟糕,我不应该这样做,等等,等等。
无论如何,这是棘手的上下文管理器的代码:
import sys
import inspect
class SkippableContext(object):
def __init__(self,mode=0):
"""
if mode = 0, proceed as normal
if mode = 1, do not execute block
"""
self.mode=mode
def __enter__(self):
if self.mode==1:
print(' ... Skipping Context')
# Do some magic
sys.settrace(lambda *args, **keys: None)
frame = inspect.currentframe(1)
frame.f_trace = self.trace
return 'SET BY TRICKY CONTEXT MANAGER!!'
def trace(self, frame, event, arg):
raise
def __exit__(self, type, value, traceback):
return True
这是测试代码:
print('==== First Pass with skipping disabled ====')
c='not set'
with SkippableContext(mode=0) as c:
print('Should Get into here')
c = 'set in context'
print('c: {}'.format(c))
print('==== Second Pass with skipping enabled ====')
c='not set'
with SkippableContext(mode=1) as c:
print('This code is not printed')
c = 'set in context'
print('c: {}'.format(c))
c='not set'
with SkippableContext(mode=1) as c:
print('This code is not printed')
c = 'set in context'
print('c: {}'.format(c))
print('==== Third Pass: Same as second pass but in a loop ====')
for i in range(2):
c='not set'
with SkippableContext(mode=1) as c: # For some reason, assinging c fails on the second iteration!
print('This code is not printed')
c = 'set in context'
print('c: {}'.format(c))
测试代码生成的输出与预期的一样,除了最后一行未设置c
的情况:
==== First Pass with skipping disabled ====
Should Get into here
c: set in context
==== Second Pass with skipping enabled ====
... Skipping Context
c: SET BY TRICKY CONTEXT MANAGER!!
... Skipping Context
c: SET BY TRICKY CONTEXT MANAGER!!
==== Third Pass: Same as second pass but in a loop ====
... Skipping Context
c: SET BY TRICKY CONTEXT MANAGER!!
... Skipping Context
c: not set
为什么在循环的第二次运行中未设置c
?是否有一些黑客可以修复此黑客中的错误?
答案 0 :(得分:3)
您正在使用的骇客骇客做很多事情,带来令人讨厌,微妙的后果。我怀疑作者是否完全理解它(如果这样做的话,他们将不会使用裸露的raise
,也不会尝试传递inspect.currentframe
不需要的参数)。顺便说一下,inspect.currentframe
的不正确使用会导致代码以TypeError
失败,而不是执行您所描述的操作,因此对于本答案的其余部分,我将假定调用已替换为{{1 }},从而产生所描述的行为。
黑客依赖的一件事是使用sys._getframe(1)
设置本地跟踪功能。此局部跟踪功能将在frame.f_trace = self.trace
块内的第一行上引发异常……至少,这通常是正常的。
当某些 trace事件发生时,Python会调用跟踪函数。这些跟踪事件之一是新源代码行的开始。 Python通过检查当前字节码指令索引是对应于行的第一条指令,还是对应于最后一条执行指令之前的索引处的指令,来确定新的源代码行已开始。您可以在with
的{{3}}中看到它。
当跟踪处于活动状态时,Python仅更新Python/ceval.c
,该变量用于确定最后执行的指令。但是,一旦本地跟踪功能引发异常,它将自动停用,并且instr_prev
停止接收更新。
设置了本地跟踪功能后,接下来可以激活的两条指令是instr_prev
来设置STORE_NAME
(如果将代码放入函数中,则设置为c
),和STORE_FAST
为下一行加载LOAD_NAME
函数(如果将代码放入函数,则为print
)。
第一次通过循环,它在LOAD_GLOBAL
上激活,并且LOAD_NAME
被设置为该指令的索引。然后会禁用本地跟踪功能,因为它引发了异常。
第二遍循环,instr_prev
仍设置为instr_prev
的索引,因此Python认为LOAD_NAME
标志着新行的开始。本地跟踪功能在STORE_NAME
上激活,并且异常阻止分配给STORE_NAME
。
通过检查c
中的frame.f_lasti
,并将结果与trace
的输出中的指令索引进行比较,可以查看激活本地跟踪功能的指令。例如,您的代码的以下变体:
dis.dis
产生以下输出:
import sys
import inspect
import dis
class SkippableContext(object):
def __enter__(self):
print(' ... Skipping Context')
sys.settrace(lambda *args, **keys: None)
frame = sys._getframe(1)
frame.f_trace = self.trace
return 'SET BY TRICKY CONTEXT MANAGER!!'
def trace(self, frame, event, arg):
print(frame.f_lasti)
raise Exception
def __exit__(self, type, value, traceback):
return True
def f():
for i in range(2):
c='not set'
with SkippableContext() as c:
print('This code is not printed')
c = 'set in context'
print('c: {}'.format(c))
f()
dis.dis(f)
第一次打印的 ... Skipping Context
26
c: SET BY TRICKY CONTEXT MANAGER!!
... Skipping Context
24
c: not set
21 0 SETUP_LOOP 64 (to 66)
2 LOAD_GLOBAL 0 (range)
4 LOAD_CONST 1 (2)
6 CALL_FUNCTION 1
8 GET_ITER
>> 10 FOR_ITER 52 (to 64)
12 STORE_FAST 0 (i)
22 14 LOAD_CONST 2 ('not set')
16 STORE_FAST 1 (c)
23 18 LOAD_GLOBAL 1 (SkippableContext)
20 CALL_FUNCTION 0
22 SETUP_WITH 18 (to 42)
24 STORE_FAST 1 (c)
24 26 LOAD_GLOBAL 2 (print)
28 LOAD_CONST 3 ('This code is not printed')
30 CALL_FUNCTION 1
32 POP_TOP
25 34 LOAD_CONST 4 ('set in context')
36 STORE_FAST 1 (c)
38 POP_BLOCK
40 LOAD_CONST 0 (None)
>> 42 WITH_CLEANUP_START
44 WITH_CLEANUP_FINISH
46 END_FINALLY
26 48 LOAD_GLOBAL 2 (print)
50 LOAD_CONST 5 ('c: {}')
52 LOAD_METHOD 3 (format)
54 LOAD_FAST 1 (c)
56 CALL_METHOD 1
58 CALL_FUNCTION 1
60 POP_TOP
62 JUMP_ABSOLUTE 10
>> 64 POP_BLOCK
>> 66 LOAD_CONST 0 (None)
68 RETURN_VALUE
对应于26
的索引,第二次打印的LOAD_GLOBAL
对应于24
的索引。