Question Gestionnaire de contexte Python: corps d'exécution conditionnel?


J'écris une application basée sur MPI (mais MPI ne compte pas dans ma question, je le mentionne uniquement pour exposer la logique) et dans certains cas, quand il y a moins d'éléments de travail que de processus, je dois créer un nouveau communicateur excluant les processus qui n'ont rien à faire. Enfin, le nouveau communicateur doit être libéré par les processus qui ont du travail à faire (et seulement par eux).

Une bonne façon de le faire serait d'écrire:

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

le corps étant exécuté uniquement par les processus qui ont du travail à faire.

Y at-il un moyen dans un gestionnaire de contexte d’éviter d’exécuter le corps? Je comprends que les gestionnaires de contexte ont été conçus à juste titre pour éviter de cacher les flux de contrôle, mais je me demande s'il est possible de contourner cela, car dans mon cas, je pense que cela serait justifié par souci de clarté.


10
2018-05-04 11:21


origine


Réponses:


La possibilité de sauter conditionnellement le corps du gestionnaire de contexte a été proposée et rejetée, comme indiqué dans PEP 377.

Voici quelques méthodes pour obtenir la fonctionnalité.

Tout d'abord, ce qui ne fonctionne pas: ne pas céder à un générateur de gestionnaire de contexte.

@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)

Ne pas céder entraînera une RuntimeException élevé par contextmanager. Au moins le finally est exécuté de manière fiable.

Méthode 1: ignorez le corps manuellement.

@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'

Ceci, bien qu'explicite, annule une grande partie de la concision d'un corps de gestionnaire de contexte. La logique qui doit être cachée dans le contexte du gestionnaire se répercute sur le corps et doit se répéter pour chaque invocation.

Méthode 2: abuser d'un générateur.

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)

Cela se comporte très bien comme un gestionnaire de contexte qui pouvez sauter le corps. Malheureusement, cela ressemble beaucoup à une boucle.

Méthode 3: faites tout manuellement.

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

Bah. Qu'est-ce que c'est? La verbosité rivalise avec Java.

Généraliser cela ne peut être fait qu'avec un rappel.

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

withdriver(dostuff)

Tant pis. Le gestionnaire de contexte a résumé de nombreux cas. Mais il y a toujours des fissures à traverser. Cela me rappelle la loi des abstractions qui fuient.


Voici un code illustrant ces méthodes et d'autres méthodes.

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)

Et la sortie

============================================
==== 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

6
2018-02-17 22:19



Cette fonctionnalité semble avoir été rejeté. Les développeurs Python préfèrent souvent la variante explicite:

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

Vous pouvez également utiliser des fonctions d'ordre supérieur:

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)

6
2018-05-04 11:29



Que diriez-vous de quelque chose comme ça à la place:

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

Vous implémentez le filter_comm décorateur à faire tout le travail qu'il devrait avec comm et nworkitems, puis, en fonction de ces résultats, décider d’exécuter ou non la fonction qui l’entoure, en passant par newcomm.

Ce n'est pas aussi élégant que with, mais je pense que c'est un peu plus lisible et plus proche de ce que vous voulez que les autres propositions. Vous pourriez nommer la fonction interne autre chose que _ Si vous n'aimez pas ce nom, mais je l'ai suivi car c'est le nom normal utilisé en Python lorsque la grammaire requiert un nom que vous n'utiliserez jamais.


0
2017-07-21 17:37