web-dev-qa-db-fra.com

Conditionnel avec instruction dans Python

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.

53
nicole

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.)

44
jamesdlin

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 exited à 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 exited 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.

51
Mike

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

9
Daniel Porteous

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
9
Anentropic

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
4
tdelaney

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é.

2
skeller88

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:

  1. il n'appelle pas get_stuff() sauf si la condition est vraie
  2. si la condition est fausse, elle fournit un gestionnaire de contexte factice. (pourrait probablement être remplacé par contextlib.nullcontext pour python> = 3.7)
  3. Vous pouvez également envoyer un gestionnaire de contexte alternatif au cas où la condition est fausse:
    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'])