0

If building and destroying context is heavy, how do I detect that I already have the context and do not need to build it again ? In other words I want the nested context 2 to do nothing.

I may need to need to call the context mutiple times inside a loop, can I put the whole loop inside a with clause, and then somehow the context inside the loop knows they are already inside a context, then skip building the context as if the inner with clause having no effect ? ( I want to completely skip context 2 in the following example )

from contextlib import contextmanager

@contextmanager
def myContext(i):

    print 'build context', i

    try:
        yield
    finally:
        print 'exiting context',i

def func():
    print 'call func'

with myContext(1):

    print 'outside'

    with myContext(2):
        func()

code output:

build context 1
outside
build context 2
call func
exiting context 2
exiting context 1
Shuman
  • 3,914
  • 8
  • 42
  • 65

3 Answers3

1

It might make sense to use a context manager class that is a singleton, rather than using the contextmanager decorator on a function. This isn't fundamentally different than kennytm's answer using a global, but my version hides the global state in a class variable rather than a regular global variable.

class MyContext(object):
    _instance = None

    def __new__(cls, i):
        if cls._instance is None:
            cls._instance = super(cls, MyContext).__new__(cls, i)
            cls._instance.depth = 0
        return cls._instance

    def __init__(self, i):
        self.i = i

    def __enter__(self):
        self.depth += 1
        if self.depth == 1:
            self.some_expensive_value = expensive_calculation(self.i)
        return self       # or maybe return self.some_expensive_value?

    def __exit__(self, exec_type, exec_value, traceback):
        self.depth -= 1
        if self.depth == 0:
            self.expensive_value.close()   # clean up value if necessary

This approach is not thread-safe, though you could probably make it so by adding a lock that you hold around the changes and checks of self.depth. The expensive calculation is only run for the outermost of any number of nested calls to the context manager.

Blckknght
  • 100,903
  • 11
  • 120
  • 169
1

I combined @Blckknght's answer with Method 3 from Creating a singleton in Python, using metaclass.

from abc import ABCMeta, abstractmethod
from functools import wraps
import time

class Singleton(ABCMeta):
    _instance = None

    def __call__(cls, *args, **kwargs):
        if not hasattr(cls, "_instance") or cls._instance is None:
            cls._instance = super(Singleton, cls).__call__(*args, **kwargs)
        return cls._instance


class ContextDecorator(object):

    __metaclass__ = Singleton

    def __init__(self, *args, **kwargs):
        self.args = args
        self.__dict__.update(kwargs)
        self._built = False
        self._contextExists = False

    @abstractmethod
    def _build(self):
        pass

    @abstractmethod
    def _destory(self):
        pass

    @classmethod
    def clear_singleton(cls):
        cls._instance = None

    def __enter__(self):
        if not self._built:
            self._build()
            self._built = True
            print 'call _build first time'
        else:
            print 'skip _build'
            self._contextExists = True

        return self

    def __exit__(self, typ, val, traceback):
        if not self._contextExists:
            self._destory()
            self.clear_singleton()
            # self._contextExists=False
            print 'call _destory first time'
        else:
            print 'skip _destory'
            self._contextExists = False

    def __call__(self, f):
        self.function = f

        @wraps(f)
        def wrapper(*args, **kw):
            with self:
                try:
                    return f(*args, **kw)
                except:
                    raise
        return wrapper


class CustomContext(ContextDecorator):

    def __init__(self, *args, **kwargs):
        super(CustomContext, self).__init__(*args, **kwargs)

    def _build(self):
        pass

    def _destory(self):
        pass




print 'context managere test'

with CustomContext():
    for i in range(3):
        with CustomContext():
            time.sleep(0.01)


print '-' * 10
print 'decorator test'


@CustomContext()
@CustomContext()
def test():
    print 'in side test func'


test()

And the output

context managere test
call _build first time
skip _build
skip _destory
skip _build
skip _destory
skip _build
skip _destory
call _destory first time
----------
decorator test
call _build first time
skip _build
in side test func
skip _destory
call _destory first time
Community
  • 1
  • 1
Shuman
  • 3,914
  • 8
  • 42
  • 65
0

You could create a global reference count:

_my_context_call_count = 0

@contextmanager
def my_context(i):
    global _my_context_call_count

    if _my_context_call_count == 0:
        print 'build context', i
    _my_context_call_count += 1

    try:
        yield

    finally:
        _my_context_call_count -= 1
        if _my_context_call_count == 0:
            print 'exiting context', i

(Note that this is not thread-safe, but it doesn't matter if your program is single-threaded.)

kennytm
  • 510,854
  • 105
  • 1,084
  • 1,005