Условные или необязательные менеджеры контекста в операторе with

Предположим, у меня есть какой-то контекстный менеджер (из сторонней библиотеки), который я использую так:

with freeze_time(test_dt):
    lines_of_code_1
    lines_of_code_2
    lines_of_code_3

Но предположим, что если для test_dt нет значения, менеджер контекста не должен запускаться, но весь оставшийся код должен выполняться, например:

if test_dt:
    with freeze_time(test_dt):
        lines_of_code_1
        lines_of_code_2
        lines_of_code_3
else:
    lines_of_code_1
    lines_of_code_2
    lines_of_code_3

Предположим, что lines_of_code здесь 2-3 строки кода, которые абсолютно идентичны, есть ли более чистый способ написать это? Я знаю, что мог бы написать что-то вроде этого:

def do_thing():
    lines_of_code_1
    lines_of_code_2
    lines_of_code_3

if test_dt:
    with freeze_time(test_dt):
        do_thing()
else:
    do_thing()

Но я не без ума от этого форматирования. Кроме того, я не хочу засорять этот шаблон по всему коду.

Есть еще одна последняя возможность, но я не уверен, что она сработает: создание подкласса менеджера контекста и пропуск функций __enter__ и __exit__, если заданный test_dt пуст, например:

class optional_freeze_time(object):
    def __init__(self, test_dt=None):
        if test_dt:
            self.ctx_manager = freeze_time(test_dt)
        else:
            self.ctx_manager = None
    def __enter__(self, *args, **kwargs):
        if self.ctx_manager:
            self.ctx_manager.__enter__(*args, **kwargs)
    def __exit__(self, *args, **kwargs):
        if self.ctx_manager:
            self.ctx_manager.__exit__(*args, **kwargs)

Я протестировал его с пустым классом менеджера контекста, и он, похоже, работал корректно. Однако меня беспокоит, правильно ли будет вести себя настоящий менеджер контекста, если я это сделаю (я не очень хорошо знаком с внутренностями того, как он работает).


person Jordan Reiter    schedule 20.12.2016    source источник
comment
Не могли бы вы привести менее абстрактный пример? Да, вы можете изменить диспетчер контекста, чтобы он не делал того, что он делает при входе и выходе if input is None:. Вы проверяли то, что написали?   -  person jonrsharpe    schedule 21.12.2016
comment
возможно, сделать объект blank_context_manager, который ничего не делает, и переопределить context_manager.__new__, чтобы вернуть пустой, если нет ввода, уменьшит количество условных выражений до 1.   -  person Tadhg McDonald-Jensen    schedule 21.12.2016
comment
Возможный дубликат условного оператора с оператором в Python   -  person Tadhg McDonald-Jensen    schedule 21.12.2016
comment
@jonrsharpe Какую часть кода вы хотели бы сделать менее абстрактной? Я бы предпочел не изменять и не трогать исходный менеджер контекста. Я продолжу и заменю код фактическим менеджером контекста, который использую, но я чувствую, что сама концепция может применяться к более общим ситуациям.   -  person Jordan Reiter    schedule 21.12.2016
comment
Я согласен с тем, что это кажется дублирующим вопросом, но я думаю, что ответ Алекса Холла лучше, чем те, что на другой странице… Я отмечу его как ответ.   -  person Jordan Reiter    schedule 21.12.2016
comment
Отвечает ли это на ваш вопрос? Возможно ли иметь необязательный параметр с Оператор /as в python?   -  person Lucas Cimon    schedule 22.07.2020


Ответы (4)


Вот простой способ обойти существующий контекстный менеджер, даже не используя какие-либо классы:

from contextlib import contextmanager

@contextmanager
def example_context_manager():
    print('before')
    yield
    print('after')

@contextmanager
def optional(condition, context_manager):
    if condition:
        with context_manager:
            yield
    else:
        yield

with example_context_manager():
    print(1)

with optional(True, example_context_manager()):
    print(2)

with optional(False, example_context_manager()):
    print(3)

Выход:

before
1
after
before
2
after
3
person Alex Hall    schedule 20.12.2016
comment
Я согласен с другими, что это дублирующий вопрос, но ваш ответ мне нравится намного больше, чем те, которые уже есть! Может быть, опубликовать это на другой странице в качестве ответа? stackoverflow.com/questions/27803059/ - person Jordan Reiter; 21.12.2016

Я бы, вероятно, унаследовал от родительского диспетчера контекста и написал бы что-то вроде этого:

class BaseContextManager:
    def __enter__(self):
        print('using Base')
    def __exit__(self, *args, **kwargs):
        print('exiting Base')


class MineContextManager(BaseContextManager):
    def __init__(self, input=None):
        self.input = input

    def __enter__(self):
        if self.input:
            super().__enter__()

    def __exit__(self, *args, **kwargs):
        if self.input:
            super().__exit__()

if __name__ == '__main__':

    with BaseContextManager():
        print('code with base')

    with MineContextManager():
        print('code without base')

    with MineContextManager(input=True):
        print('code again with base')

Это дает:

using Base
code with base
exiting Base
code without base
using Base
code again with base
exiting Base
person quapka    schedule 20.12.2016

Новых посетителей может заинтересовать contextlib.ExitStack:

with ExitStack() as stack:
  if condition:
    stack.enter_context(freeze_time(...))
  lines_of_code_1
  lines_of_code_2
  lines_of_code_3

После этого утверждения with freeze_time имеет значение только в том случае, если условие истинно.

person ntjess    schedule 03.05.2021

Просто используйте

(freeze_time if test_dt else (lambda func: contextmanager(func))(lambda dt: (yield)))(test_dt)

Пример:

from contextlib import contextmanager

test_dt = None

@contextmanager
def freeze_time(test_dt):
    print("frozen")
    yield
    print("unfrozen")

with (freeze_time if test_dt else (lambda func: contextmanager(func))(lambda dt: (yield)))(test_dt):
    print("The cold impairs your judgment.")
person cowlinator    schedule 04.03.2020