Python 中的条件 with 语句

2025-03-18 08:55:00
admin
原创
41
摘要:问题描述:有没有办法用 with 语句有条件地开始一个代码块?类似于:if needs_with(): with get_stuff() as gs: # do nearly the same large block of stuff, # involving gs or not, dependin...

问题描述:

有没有办法用 with 语句有条件地开始一个代码块?

类似于:

if needs_with():
    with get_stuff() as gs:

# do nearly the same large block of stuff,
# involving gs or not, depending on needs_with()

需要澄清的是,一种情况是将一个块包含在 with 语句中,而另一种可能性是相同的块,但没有被包含在内(即,好像它没有缩进)

初步实验当然会出现压痕错误。


解决方案 1:

Python 3.7 及更高版本

下面描述的方法ExitStack也可以使用,但 Python 3.7 进一步引入了该contextlib.nullcontext方法(该答案最初发布几年后,从那时起在其他几个答案中提到过)。在评论中,@Kache 指出了此选项最优雅的用法:

from contextlib import nullcontext

with get_stuff() if needs_with() else nullcontext() as gs:
    # do nearly the same large block of stuff,
    # involving gs or not, depending on needs_with()

请注意,如果needs_with()False,那么gs它将None位于上下文块内。如果您希望gs处于something_else那种情况下,只需将其替换nullcontext()nullcontext(something_else)

这种简单的方法只提供了 a 和某些其他上下文之间的二元选择nullcontext。虽然这满足了 OP 的简单要求,但对于更复杂的场景,更复杂的方法也是可能的:在 Python 3.3 及更高版本中,ExitStack您可以根据需要添加任意数量的exiting 内容,并具有复杂的逻辑等等。


Python 3.3 及更高版本

Python 3.3contextlib.ExitStack就是为这种情况而引入的。它为您提供了一个“堆栈”,您可以根据需要向其中添加上下文管理器。对于您的情况,您可以这样做:

from contextlib import ExitStack

with ExitStack() as stack:
    if needs_with():
        gs = stack.enter_context(get_stuff())

    # do nearly the same large block of stuff,
    # involving gs or not, depending on needs_with()

像往常一样,输入的所有内容stack都会自动exit显示在语句末尾with。(如果没有输入任何内容,那不是问题。)在此示例中,返回的任何内容get_stuff()都会exit自动显示。

如果您必须使用较早版本的 Python,您可能能够使用该contextlib2模块,尽管这不是标准。它将此功能和其他功能反向移植到较早版本的 Python。如果您喜欢这种方法,您甚至可以进行条件导入。

解决方案 2:

从 Python 3.7 开始你可以使用contextlib.nullcontext

from contextlib import nullcontext

if needs_with():
    cm = get_stuff()
else:
    cm = nullcontext()

with cm as gs:
    # Do stuff

contextlib.nullcontext几乎只是一个无操作上下文管理器。你可以向它传递一个参数,如果你依赖于以下之后存在的某些东西,它将产生as

>>> with nullcontext(5) as value:
...     print(value)
...
5

否则它只会返回None

>>> with nullcontext() as value:
...     print(value)
...
None

它非常简洁,请查看此处的文档:https://docs.python.org/3/library/contextlib.html#contextlib.nullcontext

解决方案 3:

如果您想要避免重复代码并且正在使用 Python 3.7 版(contextlib.nullcontext推出时)甚至 3.3 版(contextlib.ExitStack推出时)之前的版本,您可以执行以下操作:

class dummy_context_mgr():
    def __enter__(self):
        return None
    def __exit__(self, exc_type, exc_value, traceback):
        return False

或者:

import contextlib

@contextlib.contextmanager
def dummy_context_mgr():
    yield None

然后使用它作为:

with get_stuff() if needs_with() else dummy_context_mgr() as gs:
   # do stuff involving gs or not

您也可以get_stuff()根据 返回不同的东西needs_with()

(有关您可以在后续版本中执行的操作,请参阅Mike 的回答或Daniel 的回答。)

解决方案 4:

实现此目的的第三方选项:

https://pypi.python.org/pypi/conditional

from conditional import conditional

with conditional(needs_with(), get_stuff()):
    # do stuff

解决方案 5:

import contextlib

my_context = None # your context
my_condition = False # your condition

# Option 1 (Recommended)
with my_context if my_condition else contextlib.nullcontext():
    print('hello 1')

# Option 2
with my_context if my_condition else contextlib.ExitStack():
    print('hello 2')

解决方案 6:

您可以contextlib.nested将 0 个或多个上下文管理器放入单个with语句中。

>>> import contextlib
>>> managers = []
>>> test_me = True
>>> if test_me:
...     managers.append(open('x.txt','w'))
... 
>>> with contextlib.nested(*managers):                                                       
...  pass                                                    
...                                                             
>>> # see if it closed
... managers[0].write('hello')                                                                                                                              
Traceback (most recent call last):                              
  File "<stdin>", line 2, in <module>                                   
ValueError: I/O operation on closed file

这个解决方案有其怪癖,我刚刚注意到从 2.7 开始它已被弃用。我编写了自己的上下文管理器来处理多个上下文管理器。到目前为止,它对我来说是有效的,但我还没有真正考虑过边缘条件

class ContextGroup(object):
    """A group of context managers that all exit when the group exits."""

    def __init__(self):
        """Create a context group"""
        self._exits = []

    def add(self, ctx_obj, name=None):
        """Open a context manager on ctx_obj and add to this group. If
        name, the context manager will be available as self.name. name
        will still reference the context object after this context
        closes.
        """
        if name and hasattr(self, name):
            raise AttributeError("ContextGroup already has context %s" % name)
        self._exits.append(ctx_obj.__exit__)
        var = ctx_obj.__enter__()
        if name:
            self.__dict__[name] = var

    def exit_early(self, name):
        """Call __exit__ on named context manager and remove from group"""
        ctx_obj = getattr(self, name)
        delattr(self, name)
        del self._exits[self._exits.index(ctx_obj)]
        ctx_obj.__exit__(None, None, None)

    def __enter__(self):
        return self

    def __exit__(self, _type, value, tb):
        inner_exeptions = []
        for _exit in self._exits:
            try:
                _exit(_type, value, tb )
            except Exception, e:
                inner_exceptions.append(e)
        if inner_exceptions:
            r = RuntimeError("Errors while exiting context: %s" 
                % (','.join(str(e)) for e in inner_exceptions))

    def __setattr__(self, name, val):
        if hasattr(val, '__exit__'):
            self.add(val, name)
        else:
            self.__dict__[name] = val

解决方案 7:

很难找到@farsil 的漂亮的 Python 3.3 单行代码,因此这里是它自己的答案:

with ExitStack() if not needs_with() else get_stuff() as gs:
     # do stuff

请注意,ExitStack 应该首先出现,否则get_stuff()将被评估。

解决方案 8:

将 with 语句的主体封装在函数中

您可以按照其他答案中的建议使用各种复杂的技巧,但这些技巧很难阅读,并且会增加不必要的复杂性。如果您将可选块的主体封装with在其自己的函数中,您的程序结构将更容易理解。例如:

def do_stuff(a, *args, **kwargs):
    ...
    return a

a = 1
gs = "already_present_stuff"
if needs_with():
    with get_stuff() as gs:
        a = do_stuff(a, gs=gs)
else:
    a = do_stuff(a, gs=gs)

传递哪些参数和关键字参数do_stuff以及返回什么完全由您决定。您应该传递其中访问的所有内容do_stuff,并返回您更改的每个名称和您创建的每个新名称。

以这种方式做事是一种很好的方法,可以避免意大利面条式代码,从而避免 pylint 或人工代码审阅者的投诉(如果适用)。

是的,有些情况下contextlib是需要的,但是大多数时候只需使用函数就可以了。

解决方案 9:

所以我编写了这个代码;它的调用方式如下:

with c_with(needs_with(), lambda: get_stuff()) as gs:
    ##DOESN't call get_stuff() unless needs_with is called.
    # do nearly the same large block of stuff,
    # involving gs or not, depending on needs_with()

特性:

  1. get_stuff()除非条件为真,否则不会调用

  2. 如果条件为假,它会提供一个虚拟的上下文管理器。(可能可以用contextlib.nullcontextPython >= 3.7 替换)

  3. 如果条件为假,您可以选择发送另一个上下文管理器:

with c_with(needs_with(), lambda: get_stuff(), lambda: dont_get_stuff()) as gs:

希望这对某人有帮助!

-- 代码如下:

def call_if_lambda(f):
    """
    Calls f if f is a lambda function.
    From https://stackoverflow.com/a/3655857/997253
    """
    LMBD = lambda:0
    islambda=isinstance(f, type(LMBD)) and f.__name__ == LMBD.__name__
    return f() if islambda else f
import types
class _DummyClass(object):
    """
    A class that doesn't do anything when methods are called, items are set and get etc.
    I suspect this does not cover _all_ cases, but many.
    """
    def _returnself(self, *args, **kwargs):
        return self
    __getattr__=__enter__=__exit__=__call__=__getitem__=_returnself
    def __str__(self):
        return ""
    __repr__=__str__
    def __setitem__(*args,**kwargs):
        pass
    def __setattr__(*args,**kwargs):
        pass

class c_with(object):
    """
    Wrap another context manager and enter it only if condition is true.
    Parameters
    ----------
    condition:  bool
        Condition to enter contextmanager or possibly else_contextmanager
    contextmanager: contextmanager, lambda or None
        Contextmanager for entering if condition is true. A lambda function
        can be given, which will not be called unless entering the contextmanager.
    else_contextmanager: contextmanager, lambda or None
        Contextmanager for entering if condition is true. A lambda function
        can be given, which will not be called unless entering the contextmanager.
        If None is given, then a dummy contextmanager is returned.
    """
    def __init__(self, condition, contextmanager, else_contextmanager=None):
        self.condition = condition
        self.contextmanager = contextmanager
        self.else_contextmanager = _DummyClass() if else_contextmanager is None else else_contextmanager
    def __enter__(self):
        if self.condition:
            self.contextmanager=call_if_lambda(self.contextmanager)
            return self.contextmanager.__enter__()
        elif self.else_contextmanager is not None:
            self.else_contextmanager=call_if_lambda(self.else_contextmanager)
            return self.else_contextmanager.__enter__()
    def __exit__(self, *args):
        if self.condition:
            return self.contextmanager.__exit__(*args)
        elif self.else_contextmanager is not None:
            self.else_contextmanager.__exit__(*args)

#### EXAMPLE BELOW ####

from contextlib import contextmanager

def needs_with():
    return False

@contextmanager
def get_stuff():
    yield {"hello":"world"}

with c_with(needs_with(), lambda: get_stuff()) as gs:
    ## DOESN't call get_stuff() unless needs_with() returns True.
    # do nearly the same large block of stuff,
    # involving gs or not, depending on needs_with()
    print("Hello",gs['hello'])

解决方案 10:

我发现@Anentropic 的回答不完整。

from conditional import conditional

a = 1 # can be None

if not a is None:
  b = 1

class WithNone:
  def __enter__(self):
    return self
  def __exit__(self, type, value, tb):
    pass

def foo(x):
  print(x)
  return WithNone()

with conditional(not a is None, foo(b) if not a is None else None):
  print(123)

完整conditional使用需要 3 个条件而不是 1 个,因为:

  1. NameError: name 'b' is not defined如果没有定义a

  2. 该函数foo仍然必须返回可输入的对象,否则:AttributeError: 'NoneType' object has no attribute '__enter__'

相关推荐
  政府信创国产化的10大政策解读一、信创国产化的背景与意义信创国产化,即信息技术应用创新国产化,是当前中国信息技术领域的一个重要发展方向。其核心在于通过自主研发和创新,实现信息技术应用的自主可控,减少对外部技术的依赖,并规避潜在的技术制裁和风险。随着全球信息技术竞争的加剧,以及某些国家对中国在科技领域的打压,信创国产化显...
工程项目管理   2482  
  为什么项目管理通常仍然耗时且低效?您是否还在反复更新电子表格、淹没在便利贴中并参加每周更新会议?这确实是耗费时间和精力。借助软件工具的帮助,您可以一目了然地全面了解您的项目。如今,国内外有足够多优秀的项目管理软件可以帮助您掌控每个项目。什么是项目管理软件?项目管理软件是广泛行业用于项目规划、资源分配和调度的软件。它使项...
项目管理软件   1533  
  PLM(产品生命周期管理)项目对于企业优化产品研发流程、提升产品质量以及增强市场竞争力具有至关重要的意义。然而,在项目推进过程中,范围蔓延是一个常见且棘手的问题,它可能导致项目进度延迟、成本超支以及质量下降等一系列不良后果。因此,有效避免PLM项目范围蔓延成为项目成功的关键因素之一。以下将详细阐述三大管控策略,助力企业...
plm系统   0  
  PLM(产品生命周期管理)项目管理在企业产品研发与管理过程中扮演着至关重要的角色。随着市场竞争的加剧和产品复杂度的提升,PLM项目面临着诸多风险。准确量化风险优先级并采取有效措施应对,是确保项目成功的关键。五维评估矩阵作为一种有效的风险评估工具,能帮助项目管理者全面、系统地评估风险,为决策提供有力支持。五维评估矩阵概述...
免费plm软件   0  
  引言PLM(产品生命周期管理)开发流程对于企业产品的全生命周期管控至关重要。它涵盖了从产品概念设计到退役的各个阶段,直接影响着产品质量、开发周期以及企业的市场竞争力。在当今快速发展的科技环境下,客户对产品质量的要求日益提高,市场竞争也愈发激烈,这就使得优化PLM开发流程成为企业的必然选择。缺陷管理工具和六西格玛方法作为...
plm产品全生命周期管理   0  
热门文章
项目管理软件有哪些?
曾咪二维码

扫码咨询,免费领取项目管理大礼包!

云禅道AD
禅道项目管理软件

云端的项目管理软件

尊享禅道项目软件收费版功能

无需维护,随时随地协同办公

内置subversion和git源码管理

每天备份,随时转为私有部署

免费试用