Python中的带条件语句


72

有没有一种方法可以用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语句中包含一个块,而另一种可能性将是相同的块,但未包含(即,好像没有缩进)

当然,最初的实验会产生压痕错误。


2
为主体编写一个函数?
瑞恩·海宁

Answers:


58

如果要避免重复代码,并使用3.7(contextlib.nullcontext引入时)甚至3.3(引入时)之前的Python版本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的答案。)


5
此上下文管理器应位于标准python库imho中。谢谢你
jjmontes

如何使用名称should _...dont。然后像这样的陈述会读成with get_stuff() if should_get_stuff() else dont() as gs:
Riaz Rizvi

@RiazRizvi我个人不会这样命名;我正在使用问题中的名称。
jamesdlin

1
@jjmontes contextlib.ExitStack(新蟒3.3)可以用作虚设上下文管理器。
布赖斯·几塔

68

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

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的早期版本。如果您喜欢这种方法,甚至可以执行条件导入。


15
+1,这应该是选定的答案。如此处所指出的它旨在解决此类问题。此外,它还可以用作漂亮的单线:with get_stuff() if needs_with() else ExitStack() as gs
farsil

33

从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


4
但是,这引发了一个问题,输入with语句之前调用get_stuff()始终安全吗?是with open(file) as fh相当于f = open(file)其次是with f as fh
6005年

2
这取决于您的上下文管理器做什么。大多数上下文管理器不应在其__init__方法中执行操作,而应仅在其__enter__(或__aenter__)方法中执行操作,该方法在with语句中使用时会被调用。因此,不幸的是答案是“取决于”。如果您担心它,可以改为分配功能cm而不调用它们(functools.partial如果需要),然后执行with cm() as gs
Daniel Porteous

10

第三方选项可实现此目的:https :
//pypi.python.org/pypi/conditional

from conditional import conditional

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

它是否支持as ...语句末尾的子句with
Craig McQueen

1
看源头...是的。with conditional(needs_with(), get_stuff()) as stuff: 将为您提供get_stuff()上下文管理器的引用(如果且仅当满足条件时,否则您会收到None
Anentropic

我发现您的答案不完整:stackoverflow.com/questions/27803059/…–
Andry

4

您可以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

1
正如我在回答中提到的那样,python 3.3已添加了contextlib.ExitStack,它似乎可以完成您的ContextGroup工作。我会说我还没有被移植,对此我感到有些惊讶,但是如果您愿意使用python> = 3.3,那对您来说可能是一个很好的健壮选择。
迈克(Mike)

1
contextlib2是一个pypi程序包,已反向移植ExitStack到python 2
Anthony Sottile

3

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

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

请注意,ExitStack应该排在第一位,否则get_stuff()将被评估。


Note that ExitStack should come first, otherwise get_stuff() will be evaluated-不,不会
-khelwood

0

因此,我编写了这段代码;它的调用方式如下:

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. (可选)您可以在条件为假的情况下发送替代的contextmanager:
    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'])

-1

我发现@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__'

1
您仍然只传递了两个参数conditional,这并没有给我的原始答案添加任何内容……看来您的问题只是在正确准备所需的两个参数
Anentropic

1
@Anentropic我的观点是仅仅投入conditional工作是不够的,另外还需要变通方法以使其正常工作,这不像您的回答中那么简单,并且使整个事情变得不太适用。
安德里(Andry)
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.