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

问题描述 投票:0回答:4

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

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

这是可行的,但除非有一个内置对象可以执行此操作(我认为没有),否则我要么必须将该对象复制粘贴到任何地方,要么总是必须导入我的自定义实用程序模块。我觉得有一种更简单的方法可以做到这一点,但我错过了。

python python-2.7 contextmanager
4个回答
3
投票

“上下文管理器包装器”的想法是正确的方法。它不仅是作为函数编写的更轻量级:

from contextlib import contextmanager

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

这是正确选择的标志:自 3.7 起,专用的 nullcontext 在 stdlib 中可用(而不是在询问/使用 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

0
投票

此解决方案避免了像

f_own
这样的显式布尔值(在 @kAlmAcetA 的答案的评论中提到),而只是检查文件句柄
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
    fh = f
    # do stuff

调用函数,如果您有路径而不是所需的对象,只需创建所需的对象,将其作为参数传递给同一函数并返回输出。

对于像您的示例这样的情况感觉很好很优雅,但缺点是如果您需要在循环或其他内容中执行此操作,它可能不起作用。

© www.soinside.com 2019 - 2024. All rights reserved.