Existe-t-il un moyen de commencer un bloc de code avec une instruction with, mais de manière conditionnelle?
Quelque chose comme:
if needs_with():
with get_stuff() as gs:
# do nearly the same large block of stuff,
# involving gs or not, depending on needs_with()
Pour clarifier, un scénario aurait un bloc enfermé dans l'instruction with, tandis qu'une autre possibilité serait le même bloc, mais pas enfermé (c'est-à-dire, comme s'il n'était pas en retrait)
Les premières expériences donnent bien sûr des erreurs d'indentation.
Si vous voulez éviter la duplication de code et utilisez une version de Python antérieure à 3.7 (lorsque contextlib.nullcontext
A été introduit) ou même 3.3 (lorsque contextlib.ExitStack
A été introduit ), vous pourriez faire quelque chose comme:
class dummy_context_mgr():
def __enter__(self):
return None
def __exit__(self, exc_type, exc_value, traceback):
return False
ou:
import contextlib
@contextlib.contextmanager
def dummy_context_mgr():
yield None
puis l'utiliser comme:
with get_stuff() if needs_with() else dummy_context_mgr() as gs:
# do stuff involving gs or not
Vous pouvez également faire en sorte que get_stuff()
renvoie différentes choses en fonction de needs_with()
.
(Voir réponse de Mike ou réponse de Daniel pour ce que vous pouvez faire dans les versions ultérieures.)
Python 3.3 a été introduit contextlib.ExitStack
pour ce genre de situation. Il vous donne une "pile", à laquelle vous ajoutez des gestionnaires de contexte si nécessaire. Dans votre cas, vous feriez ceci:
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()
Tout ce qui est entré dans stack
est automatiquement exit
ed à la fin de l'instruction with
comme d'habitude. (Si rien n'est entré, ce n'est pas un problème.) Dans cet exemple, tout ce qui est retourné par get_stuff()
est exit
ed automatiquement.
Si vous devez utiliser une version antérieure de python, vous pourrez peut-être utiliser le module contextlib2
, bien que ce ne soit pas standard. Il rétroporte cela et d'autres fonctionnalités vers les versions antérieures de python. Vous pouvez même faire une importation conditionnelle, si vous aimez cette approche.
Depuis Python 3.7 vous pouvez utiliser contextlib.nullcontext
:
from contextlib import nullcontext
if needs_with():
cm = get_stuff()
else:
cm = nullcontext()
with cm as gs:
# Do stuff
contextlib.nullcontext
est à peu près juste un gestionnaire de contexte sans opération. Vous pouvez lui passer un argument qu'il produira, si vous dépendez de quelque chose qui existe après le as
:
>>> with nullcontext(5) as value:
... print(value)
...
5
Sinon, il retournera simplement None
:
>>> with nullcontext() as value:
... print(value)
...
None
C'est super soigné, consultez les documents pour cela ici: https://docs.python.org/3/library/contextlib.html#contextlib.nullcontext
Une option tierce pour atteindre exactement cela:
https://pypi.python.org/pypi/conditional
from conditional import conditional
with conditional(needs_with(), get_stuff()):
# do stuff
Vous pouvez utiliser contextlib.nested
pour mettre 0 gestionnaire de contexte ou plus dans une seule instruction 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
Cette solution a ses caprices et je viens de remarquer qu'à partir de 2.7, elle est obsolète. J'ai écrit mon propre gestionnaire de contexte pour gérer le jonglage de plusieurs gestionnaires de contexte. Cela a fonctionné pour moi jusqu'à présent, mais je n'ai pas vraiment considéré les conditions Edge
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
Il était difficile de trouver le astucieux de @ farsil Python 3.3 one-liner, alors le voici dans sa propre réponse:
with ExitStack() if not needs_with() else get_stuff() as gs:
# do stuff
Notez que ExitStack doit venir en premier, sinon get_stuff()
sera évalué.
J'ai donc fait ce code; Il est invoqué comme ceci:
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()
Propriétés:
get_stuff()
sauf si la condition est vraiecontextlib.nullcontext
pour python> = 3.7)with c_with(needs_with(), lambda: get_stuff(), lambda: dont_get_stuff()) as gs:
J'espère que cela aidera quelqu'un!
- Voici le 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.__== 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'])