Контекстный менеджер Python: условно выполняется тело?

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

Оптимальным способом сделать это было бы написать:

with filter_comm(comm, nworkitems) as newcomm:
    ... do work with communicator newcomm...

тело выполняется только процессами, которые должны работать.

Есть ли способ в менеджере контекста избегать выполнения тела? Я понимаю, что менеджеры контекста по праву были разработаны, чтобы избежать скрытия потоков управления, но мне интересно, можно ли обойти это, поскольку в моем случае я думаю, что это было бы оправдано для ясности.

Ответ 1

Было предложено и отклонено возможность условного пропуска тела менеджера контекста, как описано в PEP 377.

Ниже приведены некоторые способы достижения функциональности.

Во-первых, что не работает: не получается из генератора contextmanager.

@contextlib.contextmanager
def drivercontext():
  driver, ok = driverfactory()
  try:
    if ok:
      yield driver
    else:
      print 'skip because driver not ok'
  finally:
    driver.quit()

with drivercontext() as driver:
  dostuff(driver)

Невыполнение приведет к a RuntimeException, поднятому на contextmanager. По крайней мере, finally надежно выполняется.

Способ 1. Пропустите тело вручную.

@contextlib.contextmanager
def drivercontext():
  driver, ok = driverfactory()
  try:
    yield driver, ok
  finally:
    driver.quit()

with drivercontext() as (driver, ok):
  if ok:
    dostuff(driver)
  else:
    print 'skip because driver not ok'

Это, хотя и явное, отрицает большую часть краткости тела менеджера контекста. Логика, которая должна быть скрыта в менеджере контекста, выливается в тело и должна повторяться для каждого вызова.

Способ 2: используйте генератор.

def drivergenerator():
  driver, ok = driverfactory()
  try:
    if ok:
      yield driver
    else:
      print 'skip because driver not ok'
  finally:
    driver.quit()

for driver in drivergenerator():
  dostuff(driver)

Это ведет себя очень как менеджер контекста, который может пропустить тело. К сожалению, это очень похоже на цикл.

Способ 3: сделать все вручную.

driver, ok = driverfactory()
try:
  if ok:
    dostuff(driver)
  else:
    print 'skip because driver not ok'
finally:
  driver.quit()

Ба. Что это? Многословность соперничает с Java.

Обобщение этого может быть сделано только с обратным вызовом.

def withdriver(callback):
  driver, ok = driverfactory()
  try:
    if ok:
      callback(driver)
    else:
      print 'skip because driver not ok'
  finally:
    driver.quit()

withdriver(dostuff)

Хорошо. Диспетчер контекста решает много случаев. Но всегда есть трещины. Это напоминает мне закон негерметичных абстракций.


Вот несколько примеров, демонстрирующих эти методы и некоторые другие методы.

import contextlib
import functools

# ----------------------------------------------------------------------
# this code is a simulation of the code not under my control
# report and ok and fail are variables for use in the simulation
# they do not exist in the real code
# report is used to report certain checkpoints
# ok is used to tell the driver object whether it is ok or not
# fail is used tell dostuff whether it should fail or not

class Driver(object):
  def __init__(self, report, ok):
    # driver can be ok or not ok
    # driver must always quit after use
    # regardless if it is ok or not
    print 'driver init (ok: %s)' % ok
    self.report = report

  def drivestuff(self):
    # if driver is not ok it is not ok to do stuff with it
    self.report.drivestuffrun = True

  def quit(self):
    # driver must always quit regardless of ok or not
    print 'driver quit'
    self.report.driverquit = True

def driverfactory(report, ok=True):
  # driver factory always returns a driver
  # but sometimes driver is not ok
  # this is indicated by second return value
  # not ok driver must still be quit
  return Driver(report, ok), ok

class DoStuffFail(Exception):
  pass

def dostuff(driver, fail=False):
  # this method does a lot of stuff
  # dostuff expects an ok driver
  # it does not check whether the driver is ok
  driver.drivestuff()
  # do stuff can also fail independent of driver
  if fail:
    print 'dostuff fail'
    raise DoStuffFail('doing stuff fail')
  else:
    print 'dostuff'

# ----------------------------------------------------------------------
class AbstractScenario(object):
  def __init__(self, driverfactory, dostuff):
    self.driverfactory = functools.partial(driverfactory, report=self)
    self.dostuff = dostuff
    self.driverquit = False
    self.drivestuffrun = False

# ----------------------------------------------------------------------
class Scenario0(AbstractScenario):

  def run(self):
    print '>>>> not check driver ok and not ensure driver quit'
    driver, ok = self.driverfactory()
    self.dostuff(driver)
    driver.quit()

# ----------------------------------------------------------------------
class Scenario1(AbstractScenario):

  def run(self):
    print '>>>> check driver ok but not ensure driver quit'
    driver, ok = self.driverfactory()
    if ok:
      self.dostuff(driver)
    else:
      print 'skip because driver not ok'
    driver.quit()

# ----------------------------------------------------------------------
class Scenario2(AbstractScenario):

  def run(self):
    print '>>>> check driver ok and ensure driver quit'
    driver, ok = self.driverfactory()
    try:
      if ok:
        self.dostuff(driver)
      else:
        print 'skip because driver not ok'
    finally:
      driver.quit()

# ----------------------------------------------------------------------
class Scenario3(AbstractScenario):

  @contextlib.contextmanager
  def drivercontext(self, driverfactory):
    driver, ok = driverfactory()
    try:
      if ok:
        yield driver
      else:
        print 'skip because driver not ok'
    finally:
      driver.quit()

  def run(self):
    print '>>>> skip body by not yielding (does not work)'
    with self.drivercontext(self.driverfactory) as driver:
      self.dostuff(driver)

# ----------------------------------------------------------------------
class Scenario4(AbstractScenario):

  @contextlib.contextmanager
  def drivercontext(self, driverfactory):
    driver, ok = driverfactory()
    try:
      yield driver, ok
    finally:
      driver.quit()

  def run(self):
    print '>>>> skip body manually by returning flag with context'
    with self.drivercontext(self.driverfactory) as (driver, ok):
      if ok:
        self.dostuff(driver)
      else:
        print 'skip because driver not ok'

# ----------------------------------------------------------------------
class Scenario5(AbstractScenario):

  def drivergenerator(self, driverfactory):
    driver, ok = driverfactory()
    try:
      if ok:
        yield driver
      else:
        print 'skip because driver not ok'
    finally:
      driver.quit()

  def run(self):
    print '>>>> abuse generator as context manager'
    for driver in self.drivergenerator(self.driverfactory):
      self.dostuff(driver)

# ----------------------------------------------------------------------
def doscenarios(driverfactory, dostuff, drivestuffrunexpected=True):
  for Scenario in AbstractScenario.__subclasses__():
    print '-----------------------------------'
    scenario = Scenario(driverfactory, dostuff)
    try:
      try:
        scenario.run()
      except DoStuffFail as e:
        print 'dostuff fail is ok'
      if not scenario.driverquit:
        print '---- fail: driver did not quit'
      if not scenario.drivestuffrun and drivestuffrunexpected:
        print '---- fail: drivestuff did not run'
      if scenario.drivestuffrun and not drivestuffrunexpected:
        print '---- fail: drivestuff did run'
    except Exception as e:
      print '----- fail with exception'
      print '--------', e

# ----------------------------------------------------------------------
notokdriverfactory = functools.partial(driverfactory, ok=False)
dostufffail = functools.partial(dostuff, fail=True)

print '============================================'
print '==== driver ok and do stuff will not fail =='
doscenarios(driverfactory, dostuff)

print '============================================'
print '==== do stuff will fail ================='
doscenarios(driverfactory, dostufffail)

print '==========================================='
print '===== driver is not ok ==================='
doscenarios(notokdriverfactory, dostuff, drivestuffrunexpected=False)

И вывод.

============================================
==== driver ok and do stuff will not fail ==
-----------------------------------
>>>> not check driver ok and not ensure driver quit
driver init (ok: True)
dostuff
driver quit
-----------------------------------
>>>> check driver ok but not ensure driver quit
driver init (ok: True)
dostuff
driver quit
-----------------------------------
>>>> check driver ok and ensure driver quit
driver init (ok: True)
dostuff
driver quit
-----------------------------------
>>>> skip body by not yielding (does not work)
driver init (ok: True)
dostuff
driver quit
-----------------------------------
>>>> skip body manually by returning flag with context
driver init (ok: True)
dostuff
driver quit
-----------------------------------
>>>> abuse generator as context manager
driver init (ok: True)
dostuff
driver quit
============================================
==== do stuff will fail =================
-----------------------------------
>>>> not check driver ok and not ensure driver quit
driver init (ok: True)
dostuff fail
dostuff fail is ok
---- fail: driver did not quit
-----------------------------------
>>>> check driver ok but not ensure driver quit
driver init (ok: True)
dostuff fail
dostuff fail is ok
---- fail: driver did not quit
-----------------------------------
>>>> check driver ok and ensure driver quit
driver init (ok: True)
dostuff fail
driver quit
dostuff fail is ok
-----------------------------------
>>>> skip body by not yielding (does not work)
driver init (ok: True)
dostuff fail
driver quit
dostuff fail is ok
-----------------------------------
>>>> skip body manually by returning flag with context
driver init (ok: True)
dostuff fail
driver quit
dostuff fail is ok
-----------------------------------
>>>> abuse generator as context manager
driver init (ok: True)
dostuff fail
driver quit
dostuff fail is ok
===========================================
===== driver is not ok ===================
-----------------------------------
>>>> not check driver ok and not ensure driver quit
driver init (ok: False)
dostuff
driver quit
---- fail: drivestuff did run
-----------------------------------
>>>> check driver ok but not ensure driver quit
driver init (ok: False)
skip because driver not ok
driver quit
-----------------------------------
>>>> check driver ok and ensure driver quit
driver init (ok: False)
skip because driver not ok
driver quit
-----------------------------------
>>>> skip body by not yielding (does not work)
driver init (ok: False)
skip because driver not ok
driver quit
----- fail with exception
-------- generator didn't yield
-----------------------------------
>>>> skip body manually by returning flag with context
driver init (ok: False)
skip because driver not ok
driver quit
-----------------------------------
>>>> abuse generator as context manager
driver init (ok: False)
skip because driver not ok
driver quit

Ответ 2

Эта функция, похоже, была отклонено. Разработчики Python часто предпочитают явный вариант:

if need_more_workers():
    newcomm = get_new_comm(comm)
    # ...

Вы также можете использовать функции более высокого порядка:

def filter_comm(comm, nworkitems, callback):
    if foo:
        callback(get_new_comm())

# ...

some_local_var = 5
def do_work_with_newcomm(newcomm):
    # we can access the local scope here

filter_comm(comm, nworkitems, do_work_with_newcomm)

Ответ 3

Как насчет чего-то подобного:

@filter_comm(comm, nworkitems)
def _(newcomm):  # Name is unimportant - we'll never reference this by name.
    ... do work with communicator newcomm...

Вы реализуете декоратор filter_comm, чтобы выполнять любую работу с comm и nworkitems, а затем на основе этих результатов решаете, следует ли выполнять функцию, которую она обертывает или нет, передавая в newcomm.

Это не так элегантно, как with, но я думаю, что это немного читаемо и ближе к тому, что вы хотите, чем другие предложения. Вы можете назвать внутреннюю функцию чем-то иным, чем _, если вам не нравится это имя, но я пошел с ним, поскольку это обычное имя, используемое в Python, когда для грамматики требуется имя, которое вы никогда не будете использовать.