【问题标题】:Skipping execution of -with- block跳过 -with- 块的执行
【发布时间】:2012-09-17 15:09:23
【问题描述】:

我正在定义一个上下文管理器类,如果在实例化过程中满足某些条件,我希望能够跳过代码块而不引发异常。例如,

class My_Context(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 'Exiting...'
            CODE TO EXIT PREMATURELY
    def __exit__(self, type, value, traceback):
        print 'Exiting...'

with My_Context(mode=1):
    print 'Executing block of codes...'

【问题讨论】:

  • 我找到了这个,但我不太清楚如何理解它,也不知道如何实现它。 python.org/dev/peps/pep-0377还有其他更优雅的方式吗?
  • 它是 PEP 的事实(以及对语义变化的讨论)表明如果不改变解释器的行为就无法实现它。
  • 痴迷于整洁? :) with A(), B(): 其中 B 的 enter 可以提高一些东西对我来说似乎很好。
  • 我也需要这个功能。但唯一的建议是以非 hacky 的方式做到这一点,需要向 python 添加一个新的系统异常。该提案是 rejected,因为人们认为增加语言复杂性的成本不值得。

标签: python with-statement skip


【解决方案1】:

根据PEP-343with 语句翻译自:

with EXPR as VAR:
    BLOCK

到:

mgr = (EXPR)
exit = type(mgr).__exit__  # Not calling it yet
value = type(mgr).__enter__(mgr)
exc = True
try:
    try:
        VAR = value  # Only if "as VAR" is present
        BLOCK
    except:
        # The exceptional case is handled here
        exc = False
        if not exit(mgr, *sys.exc_info()):
            raise
        # The exception is swallowed if exit() returns true
finally:
    # The normal and non-local-goto cases are handled here
    if exc:
        exit(mgr, None, None, None)

如您所见,从调用上下文管理器的 __enter__() 方法可以跳过 with 语句的主体(“BLOCK”),没有什么明显可以做的。

人们已经完成了特定于 Python 实现的事情,例如在 withhacks 等项目中操作 __enter__() 内部的调用堆栈。我记得 Alex Martelli 一两年前在 stackoverflow 上发布了一个非常有趣的 with-hack(不记得足够多的帖子来搜索和找到它)。

但是对您的问题/问题的简单回答是,您不能按照您的要求做,跳过 with 语句的主体,而不诉诸所谓的“深层魔法”(这不一定在 python 实现之间可移植) )。使用深奥的魔法,你也许可以做到,但我建议只做一些练习来看看它是如何完成的,而不是在“生产代码”中。

【讨论】:

  • 好的,这解释了很多。我检查了withhacks。在这一点上认为它超出了我的范围......我不确定如何使用代码来执行跳过,但肯定有我可以使用的有趣的代码 sn-ps。 (更新:RUBY STYLE BLOCKS?我明白了,哈哈哈。这确实很疯狂)否则,我真的需要考虑另一种方法。谢谢!
【解决方案2】:

如果您想要一个使用来自withhacks(特别是来自AnonymousBlocksInPython)的想法的临时解决方案,这将起作用:

import sys
import inspect

class My_Context(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 'Met block-skipping criterion ...'
            # Do some magic
            sys.settrace(lambda *args, **keys: None)
            frame = inspect.currentframe(1)
            frame.f_trace = self.trace
    def trace(self, frame, event, arg):
        raise
    def __exit__(self, type, value, traceback):
        print 'Exiting context ...'
        return True

比较以下:

with My_Context(mode=1):
    print 'Executing block of code ...'

with My_Context(mode=0):
    print 'Executing block of code ... '

【讨论】:

  • 这就是我要找的。 tytytytytyty。
  • 我明白了,所以它以某种方式触发了一个 TypeError,它被 __exit__() 方法捕获和抑制。有趣的工作!
  • 我在方法 __exit__() 中添加了一个 if 循环来检查类型和值,以便仅抑制由 hack 引发的异常。
  • 方法trace()在进入一个新的本地范围时被调用,即当你的with块中的代码开始时。当此处引发异常时,它会被__exit__() 捕获。这就是这个黑客的工作原理。我应该补充一点,这在很大程度上是一种 hack,不应该依赖它。神奇的sys.settrace() 实际上并不是语言定义的一部分,它恰好出现在 CPython 中。此外,调试器依赖sys.settrace() 来完成他们的工作,所以自己使用它会干扰它。您不应该使用此代码的原因有很多。仅供参考。
  • 感谢乔的这个技巧。我正在尝试使用它来制作一个非常hacky的快速和肮脏的缓存系统。我得到了一个奇怪的结果。我在__enter__ 块的末尾添加了一个return 5,并有:for i in range(2): c=0 print('=== Iter {} ==='.format(i)) with My_Context(mode=1) as c: print('Executing block of code ...') print('c={}'.format(c)) 奇怪的是,我得到:` === Iter 0 === 满足块跳过标准...退出上下文...c =5 === 迭代 1 === 满足块跳过标准 ... 退出上下文 ... c=0 ` 为什么 0 迭代 1?
【解决方案3】:

python 3 更新其他答案中提到的 hack withhacks(具体来自AnonymousBlocksInPython):

class SkipWithBlock(Exception):
    pass


class SkipContextManager:
    def __init__(self, skip):
        self.skip = skip

    def __enter__(self):
        if self.skip:
            sys.settrace(lambda *args, **keys: None)
            frame = sys._getframe(1)
            frame.f_trace = self.trace

    def trace(self, frame, event, arg):
        raise SkipWithBlock()

    def __exit__(self, type, value, traceback):
        if type is None:
            return  # No exception
        if issubclass(type, SkipWithBlock):
            return True  # Suppress special SkipWithBlock exception


with SkipContextManager(skip=True):    
    print('In the with block')  # Won't be called
print('Out of the with block')

正如 joe 之前提到的,这是一个应该避免的 hack:

方法 trace() 在进入新的本地范围时被调用,即当你的 with 块中的代码开始时。当此处引发异常时,它会被 exit() 捕获。这就是这个黑客的工作原理。我应该补充一点,这在很大程度上是一种 hack,不应该依赖它。神奇的 sys.settrace() 实际上并不是语言定义的一部分,它恰好在 CPython 中。此外,调试器依赖 sys.settrace() 来完成他们的工作,所以自己使用它会干扰它。您不应该使用此代码的原因有很多。仅供参考。

【讨论】:

  • 这太可怕了!出于某种原因,with SkipContextManager(skip=True) as x: 有效(也就是说,您可以在上下文之后执行print(x))。但是with SkipContextManager(skip=True) \ as y:(带有明确的换行符)没有:NameError: name 'y' is not defined!这是由于frame.f_trace = self.trace...
【解决方案4】:

根据@Peter 的回答,这是一个不使用字符串操作但应该以相同方式工作的版本:

from contextlib import contextmanager

@contextmanager
def skippable_context(skip):
    skip_error = ValueError("Skipping Context Exception")
    prev_entered = getattr(skippable_context, "entered", False)
    skippable_context.entered = False

    def command():
        skippable_context.entered = True
        if skip:
            raise skip_error

    try:
        yield command
    except ValueError as err:
        if err != skip_error:
            raise
    finally:
        assert skippable_context.entered, "Need to call returned command at least once."
        skippable_context.entered = prev_entered


print("=== Running with skip disabled ===")
with skippable_context(skip=False) as command:
    command()
    print("Entering this block")
print("... Done")

print("=== Running with skip enabled ===")
with skippable_context(skip=True) as command:
    command()
    raise NotImplementedError("... But this will never be printed")
print("... Done")

【讨论】:

  • (我不确定skippable_context.entered 是否是必需的,如果是这样,是否确实有效,但我仍然保留了这些变量。)
  • 这是一个聪明的答案,不采用帧重写..非常好!
【解决方案5】:

不幸的是,您尝试做的事情是不可能的。如果__enter__ 引发异常,则在with 语句中引发该异常(不调用__exit__)。如果它没有引发异常,则将返回值提供给块并执行块。

我能想到的最接近的事情是块明确检查的标志:

class Break(Exception):
    pass

class MyContext(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 'Exiting...'
        return self.mode
    def __exit__(self, type, value, traceback):
        if type is None:
            print 'Normal exit...'
            return # no exception
        if issubclass(type, Break):
            return True # suppress exception
        print 'Exception exit...'

with MyContext(mode=1) as skip:
    if skip: raise Break()
    print 'Executing block of codes...'

这还允许您在 with 块的中间引发 Break() 以模拟正常的 break 语句。

【讨论】:

  • 该标志有效,但我想将所有检查保留在上下文管理器中并保持代码块清洁。如果不可能,我可能不得不找到除 with 之外的其他方法。非常感谢你!
【解决方案6】:

上下文管理器不是正确的构造。您要求执行主体 n 次,在本例中为零或一。如果你看一下一般情况,n where n >= 0,你最终会得到一个 for 循环:

def do_squares(n):
  for i in range(n):
    yield i ** 2

for x in do_squares(3):
  print('square: ', x)

for x in do_squares(0):
  print('this does not print')

在您的情况下,这是更特殊的用途,并且不需要绑定到循环变量:

def should_execute(mode=0):
  if mode == 0:
    yield

for _ in should_execute(0):
  print('this prints')

for _ in should_execute(1):
  print('this does not')

【讨论】:

    【解决方案7】:

    另一个稍微有点hacky的选项使用exec。这很方便,因为可以对其进行修改以执行任意操作(例如,上下文块的记忆):

    from contextlib import contextmanager
    
    
    @contextmanager
    def skippable_context_exec(skip):
        SKIP_STRING = 'Skipping Context Exception'
        old_value = skippable_context_exec.is_execed if hasattr(skippable_context_exec, 'is_execed') else False
        skippable_context_exec.is_execed=False
        command = "skippable_context_exec.is_execed=True; "+("raise ValueError('{}')".format(SKIP_STRING) if skip else '')
        try:
            yield command
        except ValueError as err:
            if SKIP_STRING not in str(err):
                raise
        finally:
            assert skippable_context_exec.is_execed, "You never called exec in your context block."
            skippable_context_exec.is_execed = old_value
    
    
    print('=== Running with skip disabled ===')
    with skippable_context_exec(skip=False) as command:
        exec(command)
        print('Entering this block')
    print('... Done')
    
    print('=== Running with skip enabled ===')
    with skippable_context_exec(skip=True) as command:
        exec(command)
        print('... But this will never be printed')
    print('... Done')
    

    如果有一些东西可以摆脱 exec 而不会产生奇怪的副作用,那就太好了,所以如果你能想到一种方法,我会全力以赴。这个问题的current lead answer 似乎可以做到这一点,但有some issues

    【讨论】:

    • 一方面,您可以在正文中运行yield lambda: exec(command)command()
    猜你喜欢
    • 1970-01-01
    • 2020-09-08
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多