我应该传递要打开的文件名还是打开文件?


53

假设我有一个处理文本文件的函数-例如,读取文本文件并删除单词“ a”。我可以将其传递给文件名并处理函数中的打开/关闭,也可以将其传递给打开的文件并期望有人调用它可以处理关闭它的问题。

第一种方法似乎是一种更好的方法,可以确保没有文件保持打开状态,但是会阻止我使用StringIO对象之类的东西

第二种方法可能有点危险-无法知道文件是否将关闭,但是我可以使用类似文件的对象

def ver_1(filename):
    with open(filename, 'r') as f:
        return do_stuff(f)

def ver_2(open_file):
    return do_stuff(open_file)

print ver_1('my_file.txt')

with open('my_file.txt', 'r') as f:
    print ver_2(f)

这些通常是其中之一吗?通常是否期望某个函数以这两种方式之一运行?还是应该对其进行详细记录,以便程序员可以适当地使用该函数?

Answers:


39

方便的界面非常好,有时也很方便。但是,在大多数情况下,良好的可组合性比便捷性更重要,因为可组合抽象使我们能够在其之上实现其他功能(包括便捷包装器)。

函数使用文件的最通用方法是将打开的文件句柄作为参数,因为这允许它也使用不属于文件系统的文件句柄(例如,管道,套接字等):

def your_function(open_file):
    return do_stuff(open_file)

如果拼写with open(filename, 'r') as f: result = your_function(f)对用户要求太多,则可以选择以下解决方案之一:

  • your_function将打开的文件或文件名作为参数。如果是文件名,则打开和关闭文件,并传播异常。这里有一个含糊不清的问题,可以使用命名参数解决。
  • 提供一个简单的包装程序来处理打开文件的问题,例如

    def your_function_filename(file):
        with open(file, 'r') as f:
            return your_function(f)

    我通常将此类功能视为API膨胀,但是如果它们提供了常用功能,则获得的便利性就足够了。

  • with open功能包装在另一个可组合功能中:

    def with_file(filename, callback):
        with open(filename, 'r') as f:
            return callback(f)

    用于with_file(name, your_function)或更复杂的情况with_file(name, lambda f: some_function(1, 2, f, named=4))


6
这种方法的唯一缺点是,有时需要文件状对象的名称,例如用于错误报告:最终用户更喜欢看到“ foo.cfg(12)中的错误”,而不是“ <stream @ 0x03fd2bb6>中的错误”。 (12)”。your_function在这方面可以使用可选的“ stream_name”参数。

22

真正的问题是完整性。您的文件处理功能是对文件的完整处理,还是只是一系列处理步骤中的一个?如果它本身是完整的,则可以将所有文件访问封装在一个函数中。

def ver(filepath):
    with open(filepath, "r") as f:
        # do processing steps on f
        return result

这具有在with语句末尾完成资源(关闭文件)的很好的属性。

但是,如果可能需要处理已经打开的文件,则将您ver_1和的区别ver_2更有意义。例如:

def _ver_file(f):
    # do processing steps on f
    return result

def ver(fileobj):
    if isinstance(fileobj, str):
        with open(fileobj, 'r') as f:
            return _ver_file(f)
    else:
        return _ver_file(fileobj)

这种显式类型测试通常不被接受,尤其是在Java,Julia和Go这样的语言中,这些语言直接支持基于类型或接口的调度。但是,在Python中,不支持基于类型的调度的语言。您可能偶尔会看到对Python中直接类型测试的批评,但是在实践中,它非常普遍且非常有效。它使函数具有高度的通用性,可以处理可能会出现的任何数据类型,也就是“鸭子类型”。注意下划线_ver_file; 这是指定“专用”功能(或方法)的常规方式。尽管从技术上可以直接调用它,但它表明该功能并非旨在直接用于外部消耗。


2019更新:在Python 3鉴于最近的更新,例如该路径现在可能存储pathlib.Path不仅仅是对象strbytes(3.4+),以及该类型提示已经从神秘的主流了(大约3.6 +,但仍积极发展的),这里的更新的代码考虑了这些进步:

from pathlib import Path
from typing import IO, Any, AnyStr, Union

Pathish = Union[AnyStr, Path]  # in lieu of yet-unimplemented PEP 519
FileSpec = Union[IO, Pathish]

def _ver_file(f: IO) -> Any:
    "Process file f"
    ...
    return result

def ver(fileobj: FileSpec) -> Any:
    "Process file (or file path) f"
    if isinstance(fileobj, (str, bytes, Path)):
        with open(fileobj, 'r') as f:
            return _ver_file(f)
    else:
        return _ver_file(fileobj)

1
鸭子的类型将根据您可以对对象执行的操作(而不是对象的类型)进行测试。例如,尝试调用read可能类似于文件的内容,或者调用open(fileobj, 'r')并捕获TypeErrorif fileobj不是字符串。
user2357112

你主张在鸭打字使用。该示例提供鸭打字效果凹口-是,得到了用户的ver操作独立式的。ver如您所说,也有可能通过鸭子类型实现。但是生成然后捕获的异常比简单的类型检查要慢,并且IMO并没有带来任何特殊的好处(明晰,通用等)。以我的经验,鸭子的输入“很棒”,而“小”则适得其反”。
乔纳森·尤妮丝

3
不,您要做的还不是鸭式打字。一个hasattr(fileobj, 'read')测试就是鸭式打字。的isinstance(fileobj, str)测试是没有的。这是区别的一个示例:isinstance测试u'adsf.txt'不是unicode文件名,因为不是str。您已经测试了过于具体的类型。鸭子打字测试,无论是基于调用open还是假设的does_this_object_represent_a_filename功能,都不​​会出现这个问题。
user2357112

1
如果代码是生产代码而不是说明性示例,那么我也不会遇到这个问题,因为我不会使用is_instance(x, str)而是类似的东西is_instance(x, string_types),并string_types在PY2和PY3 上正确设置了正确的操作。给定像字符串这样的嘎嘎声,它ver会适当地做出反应;像文件一样嘎嘎叫 一个用户ver,不会有什么区别-除了型式检验实现将运行得更快。鸭子纯粹主义者:随时可以不同意。
乔纳森·尤尼斯

5

如果传递的是文件名而不是文件句柄,则不能保证第二个文件在打开时与第一个文件是同一文件。这可能会导致正确性错误和安全漏洞。


1
真正。但这必须与另一个权衡相抵消:如果您绕过文件句柄,则所有阅读器都必须协调对文件的访问,因为每个阅读器都有可能移动“当前文件位置”。
乔纳森·尤妮丝

@JonathanEunice:在什么意义上进行协调?他们需要做的就是将文件位置设置为所需的位置。
Mehrdad 2014年

1
如果有多个实体正在读取文件,则可能存在依赖性。一个人可能需要从另一个人离开的地方开始(或在先前读取的数据所定义的地方)。而且,阅读器可能在不同的线程中运行,从而打开了蠕虫的其他协调罐。传递的文件对象变为公开的全局状态,随之而来的是所有问题(以及收益)。
乔纳森·尤尼斯

1
它没有绕过关键的文件路径。它具有一个功能(或类,方法或其他控制源)承担“文件的完整处理”的责任。如果文件访问被封装在某处,则您无需传递诸如打开文件句柄之类的可变全局状态。
乔纳森·尤尼斯

1
好吧,我们可以同意不同意。我说的是,在易变的全球状态下顺利进行的设计存在明显的弊端。也有一些优点。因此,“权衡”。传递文件路径的设计通常以一种封装的方式一次完成I / O。我认为这是一种有利的结合。YMMV。
乔纳森·尤妮丝

1

这是关于所有权和关闭文件的责任。你可以通过在一个流或文件句柄或任何啄应关闭/设置在某个时间点的另一种方法,只要你确保它是明确谁拥有它,一定是由业主当您完成关闭。这通常涉及最终的构造或一次性使用的图案。


-1

如果选择传递打开的文件,则可以执行以下操作,但不能访问写入文件的函数中的文件名。

如果我想拥有一个100%负责文件/流操作的类以及其他幼稚且不希望打开或关闭所述文件/流的类或函数,则可以这样做。

请记住,上下文管理器的工作就像有一个finally子句。因此,如果在writer函数中引发了异常,则无论如何该文件都将关闭。

import contextlib

class FileOpener:

    def __init__(self, path_to_file):
        self.path_to_file = path_to_file

    @contextlib.contextmanager
    def open_write(self):
        # ...
        # Here you can add code to create the directory that will accept the file.
        # ...
        # And you can add code that will check that the file does not exist 
        # already and maybe raise FileExistsError
        # ...
        try:            
            with open(self.path_to_file, "w") as file:
                print(f"open_write: has opened the file with id:{id(file)}")            
                yield file                
        except IOError:
            raise
        finally:
            # The try/catch/finally is not mandatory (except if you want to manage Exceptions in an other way, as file objects have predefined cleanup actions 
            # and when used with a 'with' ie. a context manager (not the decorator in this example) 
            # are closed even if an error occurs. Finally here is just used to demonstrate that the 
            # file was really closed.
            print(f"open_write: has closed the file with id:{id(file)} - {file.closed}")        


def writer(file_open, data, raise_exc):
    with file_open() as file:
        print("writer: started writing data.")
        file.write(data)
        if raise_exc:
            raise IOError("I am a broken data cable in your server!")
        print("writer: wrote data.")
    print("writer: finished.")

if __name__ == "__main__":
    fo = FileOpener('./my_test_file.txt')    
    data = "Hello!"  
    raise_exc = False  # change me to True and see that the file is closed even if an Exception is raised.
    writer(fo.open_write, data, raise_exc)

与仅使用它相比,这有什么更好/不同之处with open?这如何解决使用文件名和类似文件的对象的问题?
Dannnno

这显示了一种隐藏文件/流打开/关闭行为的方法。正如您在注释中可以清楚看到的那样,它为您提供了在打开对“编写器”透明的流/文件之前添加逻辑的方法。“写程序”可以是另一个包的类的方法。本质上,它是开放的包装。另外,感谢您的答复和投票。
Vls

该行为已经被解决了with open,对吗?您实际上提倡的功能是仅使用类似文件的对象,而不关心它来自何处?
Dannnno
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.