2013-04-18 16 views
18

Mamy kod, który wywołuje zmienną liczbę menedżerów kontekstowych w zależności od parametrów środowiska wykonawczego:Alternatywa contextlib.nested ze zmienną liczbą menedżerów kontekstowych

from contextlib import nested, contextmanager 

@contextmanager 
def my_context(arg): 
    print("entering", arg) 
    try: 
     yield arg 
    finally: 
     print("exiting", arg) 

def my_fn(items): 
    with nested(*(my_context(arg) for arg in items)) as managers: 
     print("processing under", managers) 

my_fn(range(3)) 

Jednak contextlib.nested is deprecated since Python 2.7:

DeprecationWarning: With-statements now directly support multiple context managers 

odpowiedź do Multiple variables in Python 'with' statement wskazuje, że contextlib.nested ma pewne "mylące błędy podatne na błędy", ale sugerowana alternatywa korzystania z instrukcji wielu menedżerów with nie będzie działać dla zmiennej liczby menedżerów kontekstów (a także łamie kompatybilność wsteczną).

Czy są jakieś alternatywy dla contextlib.nested, które nie są przestarzałe i (najlepiej) nie mają takich samych błędów? Czy mogę nadal używać contextlib.nested i ignorować ostrzeżenie? Jeśli tak, czy powinienem w przyszłości planować usunięcie contextlib.nested?

+1

Podbijasz dobry punkt o dowolnej liczby menedżerów ... to wydaje mi się, że ten punkt powinien zostać podniesiony na liście dyskusyjnej gdzieś ... – mgilson

+0

Dlatego właśnie Python 3 wprowadził ['contextlib.ExitStack'] (http://docs.python.org/3/library/contextlib.html# contextlib.ExitStack). Zobacz http://bugs.python.org/issue13585 –

Odpowiedz

19

Nowy Python 3 contextlib.ExitStack class został dodany jako zamiennik dla contextlib.nested() (patrz issue 13585).

jest kodowane w taki sposób można go używać w Pythonie 2 bezpośrednio:

import sys 
from collections import deque 


class ExitStack(object): 
    """Context manager for dynamic management of a stack of exit callbacks 

    For example: 

     with ExitStack() as stack: 
      files = [stack.enter_context(open(fname)) for fname in filenames] 
      # All opened files will automatically be closed at the end of 
      # the with statement, even if attempts to open files later 
      # in the list raise an exception 

    """ 
    def __init__(self): 
     self._exit_callbacks = deque() 

    def pop_all(self): 
     """Preserve the context stack by transferring it to a new instance""" 
     new_stack = type(self)() 
     new_stack._exit_callbacks = self._exit_callbacks 
     self._exit_callbacks = deque() 
     return new_stack 

    def _push_cm_exit(self, cm, cm_exit): 
     """Helper to correctly register callbacks to __exit__ methods""" 
     def _exit_wrapper(*exc_details): 
      return cm_exit(cm, *exc_details) 
     _exit_wrapper.__self__ = cm 
     self.push(_exit_wrapper) 

    def push(self, exit): 
     """Registers a callback with the standard __exit__ method signature 

     Can suppress exceptions the same way __exit__ methods can. 

     Also accepts any object with an __exit__ method (registering a call 
     to the method instead of the object itself) 
     """ 
     # We use an unbound method rather than a bound method to follow 
     # the standard lookup behaviour for special methods 
     _cb_type = type(exit) 
     try: 
      exit_method = _cb_type.__exit__ 
     except AttributeError: 
      # Not a context manager, so assume its a callable 
      self._exit_callbacks.append(exit) 
     else: 
      self._push_cm_exit(exit, exit_method) 
     return exit # Allow use as a decorator 

    def callback(self, callback, *args, **kwds): 
     """Registers an arbitrary callback and arguments. 

     Cannot suppress exceptions. 
     """ 
     def _exit_wrapper(exc_type, exc, tb): 
      callback(*args, **kwds) 
     # We changed the signature, so using @wraps is not appropriate, but 
     # setting __wrapped__ may still help with introspection 
     _exit_wrapper.__wrapped__ = callback 
     self.push(_exit_wrapper) 
     return callback # Allow use as a decorator 

    def enter_context(self, cm): 
     """Enters the supplied context manager 

     If successful, also pushes its __exit__ method as a callback and 
     returns the result of the __enter__ method. 
     """ 
     # We look up the special methods on the type to match the with statement 
     _cm_type = type(cm) 
     _exit = _cm_type.__exit__ 
     result = _cm_type.__enter__(cm) 
     self._push_cm_exit(cm, _exit) 
     return result 

    def close(self): 
     """Immediately unwind the context stack""" 
     self.__exit__(None, None, None) 

    def __enter__(self): 
     return self 

    def __exit__(self, *exc_details): 
     # We manipulate the exception state so it behaves as though 
     # we were actually nesting multiple with statements 
     frame_exc = sys.exc_info()[1] 
     def _fix_exception_context(new_exc, old_exc): 
      while 1: 
       exc_context = new_exc.__context__ 
       if exc_context in (None, frame_exc): 
        break 
       new_exc = exc_context 
      new_exc.__context__ = old_exc 

     # Callbacks are invoked in LIFO order to match the behaviour of 
     # nested context managers 
     suppressed_exc = False 
     while self._exit_callbacks: 
      cb = self._exit_callbacks.pop() 
      try: 
       if cb(*exc_details): 
        suppressed_exc = True 
        exc_details = (None, None, None) 
      except: 
       new_exc_details = sys.exc_info() 
       # simulate the stack of exceptions by setting the context 
       _fix_exception_context(new_exc_details[1], exc_details[1]) 
       if not self._exit_callbacks: 
        raise 
       exc_details = new_exc_details 
     return suppressed_exc 

używać tego jako swojego menedżera kontekstowego, następnie dodać zagnieżdżone menedżerów kontekstowych do woli:

with ExitStack() as stack: 
    managers = [stack.enter_context(my_context(arg)) for arg in items] 
    print("processing under", managers) 

Dla przykładowego menedżera kontekstów wydrukuje on:

>>> my_fn(range(3)) 
('entering', 0) 
('entering', 1) 
('entering', 2) 
('processing under', [0, 1, 2]) 
('exiting', 2) 
('exiting', 1) 
('exiting', 0) 

Można również zainstalować contextlib2 module; obejmuje on ExitStack jako backport.

+0

To rozwiązanie działa tylko w sytuacji, gdy używane menedżery kontekstów są "wprowadzane" w tym samym kontekście, w którym zostały połączone. W [tym kodzie] (https://github.com/fabric/fabric/blob/master/fabric/context_managers.py#L243), na przykład, wynik 'zagnieżdżonych' jest zwracany, aby dzwoniący mógł wprowadzić później. Nie widzę sposobu na zaimplementowanie tego w Pythonie 3. –

+0

Masz na myśli, że chcesz skonstruować skumulowany CM w funkcji lub podobnej, a następnie * później * użyć tego stosu w instrukcji 'with'? Możesz to zrobić za pomocą 'ExitStack()'; po prostu utwórz instancję w zmiennej, użyj '.enter_context()', aby przepchnąć menedżery kontekstu, a następnie * później * użyj instancji w instrukcji 'with'. Możesz przekazać obiekt dookoła, tak jak inne obiekty Pythona. –

+1

Jeśli chcesz odłożyć * wprowadzanie * również, podklasa, która pobiera kolejkę i wywołuje '.enter_context()' w '__enter __()' (i obsługuje wychodzenie poprawnie, gdy wystąpi wyjątek), nie jest zbyt trudne do napisania. –

0

to trochę dokuczliwy, że opiekunowie python3 zdecydował się złamać kompatybilność wsteczną, ponieważ wdrożenia nested pod względem ExitStack jest dość prosta:

try: 
    from contextlib import nested # Python 2 
except ImportError: 
    from contextlib import ExitStack, contextmanager 

    @contextmanager 
    def nested(*contexts): 
     """ 
     Reimplementation of nested in python 3. 
     """ 
     with ExitStack() as stack: 
      for ctx in contexts: 
       stack.enter_context(ctx) 
      yield contexts 
+0

Ta implementacja ma ten sam problem, co oryginalny "nested". Menedżerowie kontekstu są już stworzeni do czasu wywołania funkcji 'zagnieżdżonej'. Jeśli konstruktor dla menedżera kontekstu o indeksie> 0 zgłasza wyjątek, poprzednie menedżery kontekstu nigdy nie zostaną zakończone. –

+0

Ma to "problem", tak. To jest tylko problem dla kontekstów, które pobierają zasoby, gdy są konstruowane (tak jak "niestety" działa). Istnieje wiele kontekstów, które nie działają w ten sposób, a "zagnieżdżony" pozwala zwrócić pojedynczą wartość zwracaną, która może być użyta jako kontekst dla wszystkich pod-kontekstów. – RecursivelyIronic

Powiązane problemy