【问题标题】:Context Manager Hackery上下文管理器黑客
【发布时间】:2019-08-22 22:29:42
【问题描述】:

我正在尝试为 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?是否有一些 hack 可以修复此 hack 中的错误?

【问题讨论】:

  • inspect.currentframe(1) 传递的1 是什么?我没有看到currentFrame 接受任何争论。
  • 那是“堆栈中的一步”-即。上下文管理器的调用者的框架。
  • 很奇怪。我找不到对带有参数的函数inspect.currentframe 的单个文档引用。这在 IntelliJ 中不适用于我。我只是想玩弄它。这个答案似乎在 Python 2 中,但即使是 2.7 文档也没有任何参数。
  • AFAIK, frame = sys._getframe(1) 会做正确的事——但这并不能解决/解释循环问题。
  • 为了让大家明白,Python 2中(甚至在.7.16中),inspect.currentframe被定义为@987654333 @ (尽管文档没有反映它)。此外,该行为可以概括:当增加循环步骤时,偶数(基于0)迭代将产生SET BY TRICKY CONTEXT MANAGER!!,而奇数迭代将产生not set

标签: python contextmanager


【解决方案1】:

您使用的这种可怕的 hack 会产生很多令人讨厌的、微妙的后果。我怀疑作者是否完全理解它(如果他们理解了,他们就不会使用裸露的raise,也不会尝试传递inspect.currentframe 一个它不接受的参数)。顺便说一句,inspect.currentframe 的错误使用会导致代码以 TypeError 失败,而不是按照您的描述进行,所以对于这个答案的其余部分,我假设调用被替换为 sys._getframe(1),这会产生描述的行为。


hack 依赖的一件事是使用frame.f_trace = self.trace 设置本地跟踪功能。这个本地跟踪函数将在 with 块内的第一行引发异常......或者至少,这是它通常所做的。

当某些跟踪事件发生时,Python 会调用跟踪函数。其中一个跟踪事件是新源代码行的开始。 Python 通过检查当前字节码指令索引是否对应于行的第一条指令或在执行最后一条指令之前的索引处的指令来确定新的源代码行已经开始。您可以在 maybe_call_line_trace 中看到 Python/ceval.c

Python 仅更新 instr_prev,当跟踪处于活动状态时,该变量用于确定执行的最后一条指令。但是,一旦本地跟踪函数引发异常,它就会自动停用,instr_prev 会停止接收更新。

当设置本地跟踪功能时,它可以激活的下两条指令是STORE_NAME 设置c(或STORE_FAST,如果您将代码放在函数中),LOAD_NAME 设置为下一行加载 print 函数(或 LOAD_GLOBAL 如果将代码放入函数中)。

第一次通过循环时,它在LOAD_NAME 上激活,并且instr_prev 被设置为该指令的索引。然后本地跟踪功能被禁用,因为它引发了异常。

第二次循环,instr_prev 仍然设置为LOAD_NAME 的索引,因此 Python 认为STORE_NAME 标志着新行的开始。本地跟踪功能在STORE_NAME 上激活,异常阻止分配给c

您可以通过检查trace 中的frame.f_lasti 并将结果与​​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对应STORE_FAST的索引。

【讨论】:

  • 这篇文章已有多年历史,来自 Python 2 “时代”,其中 inspect.currentframe(level)OK,即使没有记录(查看我的评论问题)。
  • 感谢您的详细解释。我想我现在明白了这个机制。这也让我注意到它实际上会在奇数迭代中跳过as c:,而不仅仅是在第一次之后。我会看看我是否可以使用它来找到解决方案,或者至少检测循环情况并引发异常。
猜你喜欢
  • 2016-09-22
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多