上下文管理器黑客

时间:2019-08-22 22:29:42

标签: python contextmanager

我正在尝试使用一种技巧,使上下文管理器有条件地跳过其上下文中的代码,从而为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?是否有一些黑客可以修复此黑客中的错误?

1 个答案:

答案 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的索引。