简体   繁体   中英

Conditional context manager and decorator in Python

I have the following Python code littered throughout my code base which profiles a code block and sends the results to a monitoring solution if the SHOULD_PROFILE environment variable is set.

from contextlib import ExitStack

with ExitStack() as stack:
    if os.environ.get("SHOULD_PROFILE"):
        stack.enter_context(profile())
    ...

I would like to consolidate this snippet into a single context manager / decorator so it can be used as a context manager AND a decorator:

with profile_if_enabled():
    ...

# AND

@profile_if_enabled()
def func():
    ....

This is what I've come up with but it isn't working.

from contextlib import ContextDecorator, ExitStack

class profile_if_enabled(ContextDecorator):
    def __enter__(self):
        with ExitStack() as stack:
            if os.environ.get("SHOULD_PROFILE"):
                stack.enter_context(profile())
            return self

    def __exit__(self, *exc):
        return False

Any idea what I'm doing wrong?

Your current attempt fails because as soon as you return self , you leave the with ExitStack() as stack: block, and the ExitStack performs cleanup. You need to perform cleanup in __exit__ , not when __enter__ returns.

ContextDecorator requires a reusable, ideally reentrant context manager, which doesn't match your needs well. Even if you get something working, it will most likely break if you try to call a profiled function from another profiled function, or if you try to run two profiled functions in different threads at the same time.

It'd be cleaner to just write a decorator manually:

def profile_if_enabled(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        if os.environ.get('SHOULD_PROFILE'):
            with profile():
                return func(*args, **kwargs)
        else:
            return func(*args, **kwargs)
    return wrapper

If you really want something that works as both a decorator and a context manager, you can check for the presence of an argument:

def profile_if_enabled(func=None):
    if func is None:
        return profile()

    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        if os.environ.get('SHOULD_PROFILE'):
            with profile():
                return func(*args, **kwargs)
        else:
            return func(*args, **kwargs)
    return wrapper

This would then be used as either with profile_if_enabled(): or @profile_if_enabled . Note that unlike with ContextDecorator , the use as a decorator does not have parentheses.

If you don't need to support changing the SHOULD_PROFILE setting during execution, you could alternatively restructure the code to avoid repeating the check:

if os.environ.get('SHOULD_PROFILE'):
    def profile_if_enabled(func=None):
        if func is None:
            return profile()

        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            with profile():
                return func(*args, **kwargs)
        return wrapper
else:
    def profile_if_enabled(func=None):
        if func is None:
            return contextlib.nullcontext()
        return func

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM