18

我正在编写一个基于 MPI 的应用程序(但 MPI 在我的问题中并不重要,我提到它只是为了揭示基本原理)并且在某些情况下,当工作项少于进程时,我需要创建一个新的通信器不包括无关的进程。最后,新的通信器必须由有工作要做的进程(并且只能由它们)释放。

一个巧妙的方法是编写:

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

主体仅由有工作要做的进程执行。

上下文管理器中有没有办法避免执行正文?我理解上下文管理器的设计是为了避免隐藏控制流,但我想知道是否有可能规避这一点,因为在我的情况下,我认为为了清楚起见这是合理的。

4

3 回答 3

16

已提出有条件地跳过上下文管理器主体的能力,但如PEP 377中所述被拒绝。

我做了一些关于替代品的研究。这是我的发现。

首先让我解释一下我的代码示例的背景。您有一堆想要使用的设备。对于每个设备,您必须获取设备的驱动程序;然后使用驱动程序使用设备;最后释放驱动程序,以便其他人可以获取驱动程序并使用该设备。

这里没有什么不寻常的。代码大致如下:

driver = getdriver(devicename)
try:
  dowork(driver)
finally:
  releasedriver(driver)

但是,当行星没有正确对齐的每个满月时,获得的设备驱动程序是错误的,并且无法对设备进行任何操作。这没什么大不了的。只需在这一轮跳过该设备,然后在下一轮重试。通常司机是好的。但即使是坏的驱动程序也需要发布,否则无法获得新的驱动程序。

(固件是专有的,供应商不愿意修复甚至承认这个错误)

代码现在如下所示:

driver = getdriver(devicename)
try:
  if isgooddriver(driver):
    dowork(driver)
  else:
    handledrivererror(geterrordetails(driver))
finally:
  release(driver)

每次需要使用设备完成工作时,都需要重复大量样板代码。python上下文管理器的主要候选者,也称为with 语句。它可能看起来像这样:

# note: this code example does not work
@contextlib.contextmanager
def contextgetdriver(devicename):
  driver = getdriver(devicename)
  try:
    if isgooddriver(driver):
      yield driver
    else:
      handledrivererror(geterrordetails(driver))
  finally:
    release(driver)

然后使用设备时的代码又短又甜:

# note: this code example does not work
with contextgetdriver(devicename) as driver:
  dowork(driver)

但这不起作用。因为上下文管理器必须让步。它可能不会产生。不屈服将导致RuntimeException提高 by contextmanager

所以我们必须从上下文管理器中取出检查

@contextlib.contextmanager
def contextgetdriver(devicename):
  driver = getdriver(devicename)
  try:
    yield driver
  finally:
    release(driver)

并将其放在with语句的正文中

with contextgetdriver(devicename) as driver:
  if isgooddriver(driver):
    dowork(driver)
  else:
    handledrivererror(geterrordetails(driver))

这很丑陋,因为现在我们再次有一些样板文件,每次我们想要使用设备时都需要重复这些样板文件。

所以我们想要一个可以有条件地执行主体的上下文管理器。但我们没有,因为PEP 377(正是建议这个特性)被拒绝了。

我们可以自己引发异常,而不是不让步:

@contextlib.contextmanager
def contextexceptgetdriver(devicename):
  driver = getdriver(devicename)
  try:
    if isgooddriver(driver):
      yield driver
    else:
      raise NoGoodDriverException(geterrordetails(driver))
  finally:
    release(driver)

但现在您需要处理异常:

try:
  with contextexceptgetdriver(devicename) as driver:
    dowork(driver)
except NoGoodDriverException as e:
  handledrivererror(e.errordetails)

它的代码复杂性成本与上面对好的驱动程序的显式测试几乎相同。

不同之处:有一个例外,我们可以决定不在这里处理它,而是让它冒泡调用堆栈并在其他地方处理它。

还有区别:当我们处理异常时,驱动程序已经被释放了。虽然通过显式检查,驱动程序尚未发布。(except 在 with 语句之外,而 else 在 with 语句内)

我发现滥用生成器可以很好地替代可以跳过正文的上下文管理器

def generatorgetdriver(devicename):
  driver = getdriver(devicename)
  try:
    if isgooddriver(driver):
      yield driver
    else:
      handledrivererror(geterrordetails(driver))
  finally:
    release(driver)

但是调用代码看起来很像一个循环

for driver in generatorgetdriver(devicename):
  dowork(driver)

如果您可以忍受这个(请不要),那么您就有一个可以有条件地执行正文的上下文管理器。

似乎防止样板代码的唯一方法是使用回调

def workwithdevice(devicename, callback):
  driver = getdriver(devicename)
  try:
    if isgooddriver(driver):
      callback(driver)
    else:
      handledrivererror(geterrordetails(driver))
  finally:
    release(driver)

和调用代码

workwithdevice(devicename, dowork)
于 2015-02-17T22:19:26.053 回答
6

这个功能似乎被拒绝了。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)
于 2012-05-04T11:29:57.133 回答
1

像这样的东西怎么样:

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

你实现filter_comm装饰器来做它应该用command做的任何工作nworkitems,然后根据这些结果决定是否执行它所包裹的函数,传入newcomm.

它不如 优雅with,但我认为这比其他提案更具可读性并且更接近您想要的内容。如果你不喜欢这个名字,你可以给内部函数_命名,但我选择了它,因为它是 Python 中使用的正常名称,当语法需要一个你永远不会实际使用的名称时。

于 2015-07-21T17:37:09.983 回答