条件下使用上下文管理器的Pythonic方式

9

我认为这是一个经常出现的问题,但我还没有找到一个好的解决方案。假设我有一个函数,它可能会将一个打开的资源(比如文件或数据库连接对象)作为参数传递,或者需要自己创建一个。如果函数需要自己打开文件,则最佳实践通常是:

with open(myfile) as fh:
    # do stuff with open file handle...

为确保在with块退出时文件始��关闭。但是,如果传递了现有的文件句柄,则该函数可能不应该关闭它本身。
考虑以下函数,它以打开的文件对象或给定文件路径的字符串作为其参数。如果传递文件路径,则应按照上述方式编写。否则应省略with语句。这会导致重复的代码:
def foo(f):
    if isinstance(f, basestring):
        # Path to file, need to open
        with open(f) as fh:
            # do stuff with fh...
    else:
        # Assume open file
        fh = f
        # do the same stuff...

当然,可以通过定义一个帮助函数,并在两个位置调用它来避免这种情况,但这似乎不太优雅。我想到的更好的方法是定义一个上下文管理器类,将一个对象进行包装,例如:

class ContextWrapper(object):
    def __init__(self, wrapped):
        self.wrapped = wrapped
    def __enter__(self):
        return self.wrapped
    def __exit__(self, *args):
        pass

def foo(f):
    if isinstance(f, basestring):
        cm = open(f)
    else:
        cm = ContextWrapper(f)

    with cm as fh:
        # do stuff with fh...

这样做是可行的,但除非有一个内置对象来完成这个任务(我认为没有),否则我要么必须到处复制粘贴该对象,要么总是需要导入我的自定义实用程序模块。我感觉可能有一种更简单的方法,但我可能错过了。


我想不出一个好的理由来编写可以接受路径打开文件句柄的代码。在这种边缘情况下,我建议编写自己的包装器(就像你已经做的那样)。 - Adam Smith
4
我认为一个辅助函数可能会更加优雅。在 foo 中,你所做的大部分工作是将参数转换为正确类型的对象——一个打开的文件句柄。将执行核心工作的代码提取到一个辅助函数中,假设该函数已经有了一个打开的文件句柄,我认为整体结果更清晰。实际上,“helper” 可以成为一个合法的公共函数,而“foo_from_path”只需在打开文件句柄后调用“foo”。 - jme
1
@AdamSmith 说句实话,在 numpy 中这是一种常见的设计。例如,np.load 可以接受字符串或文件对象作为参数(参考链接:http://docs.scipy.org/doc/numpy-1.10.1/reference/generated/numpy.load.html)。我想这只是为了让代码更简洁,而且由于 numpy 经常被交互使用,这也不是不明智的做法。 - jme
@jme 谢谢提供背景信息! :) - Adam Smith
Python 3.x有一个更优雅的解决方案,请参见:https://dev59.com/WVgR5IYBdhLWcg3wf9ZZ - ideasman42
4个回答

3
那个“上下文管理器包装”想法是正确的方法。不仅作为函数更轻量级:
from contextlib import contextmanager

@contextmanager
def nullcontext(obj=None):
    yield obj

选对了这个库的标志:专门用于此的 nullcontext 在 Python 3.7 的标准库中已经提供(但不适用于 Python 2.7),文档中也包含了您精确的使用场景。
def process_file(file_or_path):
    if isinstance(file_or_path, str):
        # If string, open file
        cm = open(file_or_path)
    else:
        # Caller is responsible for closing file
        cm = nullcontext(file_or_path)

    with cm as file:
        # Perform processing on the file

1

然而,我更喜欢这种方式,虽然我不知道它是否符合Pythonic标准,但它很直接明了。

def foo(f):
    if isinstance(f, basestring):
        f = open(f)
    try:
        # do the stuff
    finally:
        f.close()

使用Python 3.4中的singledispatch更好地解决了该问题。

from functools import singledispatch

@singledispatch
def foo(fd):
    with fd as f:
        # do stuff
        print('file')

@foo.register(str)
def _(arg):
    print('string')
    f = open(arg)
    foo(f)


foo('/tmp/file1')  # at first calls registered func and then foo
foo(open('/tmp/file2', 'r'))  # calls foo

如果输入一个打开的句柄并且在 try: 中抛出异常,那么这将关闭该句柄。这很好,但我认为有点奇怪 - 这个函数真的有权利这样做吗?避免这种情况的一种方法 - 这就是 numpy 所做的 - 是通过布尔值 f_own 跟踪函数是否“拥有”文件句柄。然后,在 finally: 块中,只有在 f_own 为 True 时才关闭。 - jme

0

这个解决方案避免了像 @kAlmAcetA 的答案评论中提到的显式布尔值 f_own,而是仅检查输入参数 f 与文件句柄 fh 的身份。使用 try/finally 子句是在不创建上下文管理器作为辅助类的情况下执行此操作的唯一方法。

def foo(f):
    fh = open(f) if isinstance(f, basestring) else f

    try:
        # do stuff...
    finally:
        if fh is not f:
            fh.close()

如果你需要在多个函数中执行类似的操作,那么你可能应该创建一个实用模块,并使用上下文管理器类来完成它,就像这样:
class ContextWrapper(object):
    def __init__(self, file):
        self.f = file

    def __enter__(self):
        self.fh = open(self.f) if isinstance(self.f, basestring) else self.f
        return self.fh

    def __exit__(self, *args):
        if self.fh is not self.f:
            self.fh.close()

然后你可以无条件地像这样进行包装:

def foo(f):
    with ContextManager(f) as fh:
        # do stuff...

0
递归怎么样?
def foo(f):
    if isinstance(f, basestring):
        # Path to file, need to open
        with open(f) as fh:
            # recurse with now-opened file
            return foo(fh)
    # Assume open file
    # do stuff

如果你有一个路径而不是所需的对象,只需创建所需的对象,将其作为参数传递给同一个函数并返回输出,然后调用该函数。
对于像你的例子这样的情况,这种方法感觉很好,很优雅,但缺点是如果你需要在循环或其他情况下执行此操作,可能无法正常工作。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接