Python 中的条件语句

有没有一种方法可以用 with 语句开始一段代码,但是有条件的?

Something like:

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 语句中包含一个块,而另一种可能性是相同的块,但是没有包含(例如,好像它没有缩进)

最初的实验给出了压痕误差。

28040 次浏览

可以使用 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

如果您想避免代码重复,并且正在使用3.7(当 contextlib.nullcontext被引入时)甚至3.3(当 contextlib.ExitStack被引入时)之前的 Python 版本,您可以这样做:

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's answer了解您在以后的版本中可以做什么。)

Python 3.3及以上版本

Python 3.3为这种情况引入了 contextlib.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的内容都会像往常一样在 with语句的末尾自动被 exited。(如果没有输入任何内容,这不是问题。)在这个示例中,get_stuff()返回的任何内容都是 exited 自动返回的。

如果必须使用 Python 的早期版本,那么可以使用 contextlib2模块,尽管这不是标准版本。它将这个特性和其他特性支持到 Python 的早期版本。如果您喜欢这种方法,您甚至可以执行条件导入。


Python 3.7及以上版本

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。在这种情况下,如果您希望 gssomething_else,只需将 nullcontext()替换为 nullcontext(something_else)即可。

这种方法显然不像 ExitStack那样灵活,因为这只是一种二进制选择,而 ExitStack允许您使用复杂的逻辑等添加任意多的 exiting 内容。但是这肯定回答了 OP 的简单要求。

实现这一目标的第三方选择:
Https://pypi.python.org/pypi/conditional

from conditional import conditional


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

在 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 is pretty much just a no-op context manager. You can pass it an argument that it will yield, if you depend on something existing after the 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

很难找到@farsil 的漂亮的 Python 3.3俏皮话,所以下面是它自己的回答:

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

Note that ExitStack should come first, otherwise get_stuff() will be evaluated.

所以我做了这个代码 它是这样被援引的:

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. 除非条件为 true,否则它不调用 get_stuff()
  2. 如果条件为 false,它将提供一个虚拟的上下文管理器(对于 python > = 3.7,可以用 contextlib.nullcontext替换)
  3. 如果条件为 false,您可以选择发送另一个上下文管理器:
    with c_with(needs_with(), lambda: get_stuff(), lambda: dont_get_stuff()) as gs:

希望这对谁有帮助!

-- Here is the code:

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'])

我发现这个“天文学 回答”是不完整的。

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. 如果没有定义 a,则为 NameError: name 'b' is not defined
  2. 函数 foo仍然必须返回可输入的对象,否则: AttributeError: 'NoneType' object has no attribute '__enter__'
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')