StringIO和'with'语句(上下文管理器)的兼容性问题

60

我有一些遗留的代码,其中包括一个以文件名作为参数并处理文件内容的遗留函数。下面是一个可行的代码示例。

我想做的是不必将我生成的某些内容写入磁盘,就可以使用这个遗留函数。因此,我认为可以使用 StringIO 来创建一个对象来代替实际的文件名。但是,如下所示,这样做是行不通的。

我认为使用 StringIO 是合适的方法。有没有人能告诉我是否有一种方法可以在参数中传递不是磁盘上的文件,但是可以被这个遗留函数视为这样的东西?该遗留函数确实对 filename 参数值使用了 with 上下文管理器进行操作。

我在 Google 上找到的唯一一件事是:http://bugs.python.org/issue1286,但这并没有帮助我解决问题...

代码

from pprint import pprint
import StringIO

    # Legacy Function
def processFile(filename):
    with open(filename, 'r') as fh:
        return fh.readlines()

    # This works
print 'This is the output of FileOnDisk.txt'
pprint(processFile('c:/temp/FileOnDisk.txt'))
print

    # This fails
plink_data = StringIO.StringIO('StringIO data.')
print 'This is the error.'
pprint(processFile(plink_data))

输出

这是存储在 FileOnDisk.txt 文件中的输出内容:

['This file is on disk.\n']

这是错误信息:

Traceback (most recent call last):
  File "C:\temp\test.py", line 20, in <module>
    pprint(processFile(plink_data))
  File "C:\temp\test.py", line 6, in processFile
    with open(filename, 'r') as fh:
TypeError: coercing to Unicode: need string or buffer, instance found
4个回答

83

StringIO实例已经是一个打开的文件。而open命令只接受文件名,返回一个打开的文件。因此StringIO实例不适合作为文件名。

此外,您不需要关闭StringIO实例,因此也没有必要将其用作上下文管理器。关闭实例会释放分配的内存,让垃圾收集器回收对象也会释放内存。无论如何,如果您想确保释放内存同时仍然保留对对象的引用, contextlib.closing()上下文管理器可以负责关闭对象。

如果您的旧代码只接受文件名,则StringIO实例不适合使用。可以使用tempfile模块生成临时文件名。

这里是一个使用上下文管理器来确保在之后清除临时文件的示例:

import os
import tempfile
from contextlib import contextmanager

@contextmanager
def tempinput(data):
    temp = tempfile.NamedTemporaryFile(delete=False)
    temp.write(data)
    temp.close()
    try:
        yield temp.name
    finally:
        os.unlink(temp.name)

with tempinput('Some data.\nSome more data.') as tempfilename:
    processFile(tempfilename)

你还可以切换到由 io 模块提供的较新的Python 3基础架构(在Python 2和3中可用),其中 io.BytesIOStringIO.StringIO / cStringIO.StringIO 更为强大的替代品。该对象支持被用作上下文管理器(但仍然不能传递给open())。


1
@mike:由于创建时使用了 delete=False 参数,因此命名临时文件在关闭后 不会 立即被删除 —— 请参阅 文档。从 yield temp.name 语句之前的 temp.close() 可以看出这一点... - martineau
1
@Genius:它不仅仅是让对象被垃圾回收,从而实现完全相同的效果。但是,调用.close()将清除为内存文件数据分配的内存缓冲区。 - Martijn Pieters
@pippo1980:我不知道你在这里问什么。如果你有同样的问题,并且有代码需要文件名,只需使用我的tempinput()上下文管理器来提供指向带有给定数据的临时文件的文件名。 - Martijn Pieters
这个问题对于Python 2.x很有用,但标题并没有传达清楚,可能会误导像我这样的新手。编辑队列已满,无法将“在Python 2.x中”添加到标题中。 - pippo1980
是的,我已经意识到了。这就是为什么我提到了标题。通过谷歌搜索,我得到的是标题而不是标签。从现在开始,我会更加注意标签。 - pippo1980
显示剩余3条评论

6
你可以定义自己的打开函数。
fopen = open
def open(fname,mode):
    if hasattr(fname,"readlines"): return fname
    else: return fopen(fname,mode)

然而,想要在完成后调用__exit__并且StringIO没有exit方法...

您可以定义一个自定义类来与此打开一起使用。

class MyStringIO:
     def __init__(self,txt):
         self.text = txt
     def readlines(self):
          return self.text.splitlines()
     def __exit__(self):
          pass

不幸的是,这并不能解决问题,因为它必须在传统函数内部。 - jdi
1
实际上让遗留模块使用自定义的 open 的唯一方法是先定义新的 open,然后导入遗留模块,并执行:legacy.open = open。因为遗留模块正在使用它自己的作用域。 - jdi
1
我开始写了另一个答案,但很快意识到它只解决了问题的一半,而你的第二个例子则涵盖了这一点。你可以建议使用tempfile.SpooledTemporaryFile,并设置max_size=10e8或其他较高的值。这将是一个类似文件的对象,在底层使用StringIO,并已经有了上下文管理器。 - jdi
感谢大家。就jdi的最后一条评论而言,据我所知,SpooledTemporaryFile与StringIO存在相同的问题,即它们都是类似文件的对象,但我的旧函数需要一个作为文件路径的字符串。最终我采用了Martijn Pieters下面的解决方案,它可以正常工作。我真的希望能找到一种解决方案,可以将一个字符串/对象传递给旧函数,该字符串/对象可以在open函数中使用,但实际上并不是磁盘上的文件,而是内存中的文件。 - mpettis
@JoranBeasley,现在在Python3中,io.StringIO既有__exit__方法又有readlines方法,我们该如何解决这个问题?我正在面临OP重构代码的问题。 - pippo1980
显示剩余4条评论

2
这段话的意思是:这个程序基于Python文档中的contextmanager。它只是用简单的上下文封装了StringIO,当调用“exit”时,它会返回到yield点,并正确关闭StringIO。这避免了制作临时文件的需要,但对于大字符串来说,它仍然会占用内存,因为StringIO缓冲区包含了该字符串。在大多数情况下,它可以很好地工作,只要你知道字符串数据不会很长。
from contextlib import contextmanager

@contextmanager
def buildStringIO(strData):
    from cStringIO import StringIO
    try:
        fi = StringIO(strData)
        yield fi
    finally:
        fi.close()

然后你可以这样做:
with buildStringIO('foobar') as f:
    print(f.read()) # will print 'foobar'

7
这可以使用标准库完成: "使用closing(StringIO(.... data ....))来打开文件:with as f:" - Paul Du Bois

0

即使

您也可以切换到io模块提供的更新的Python 3基础架构(适用于Python 2和3),其中io.BytesIO是StringIO.StringIO / cStringIO.StringIO的更强大的替代品。该对象支持用作上下文管理器(但仍无法传递给open())。

在Python3中,这对我有效:

from pprint import pprint

from io import StringIO

import contextlib

@contextlib.contextmanager
def as_handle(handleish, mode="r", **kwargs):
    try:
        with open(handleish, mode, **kwargs) as fp:
            yield fp
    except TypeError:
        yield handleish


def processFile(filename):
    #with filename as fh:     ### OK for StringIO
        
    #with(open(filename)) as fh: #TypeError: expected str, bytes or os.PathLike                          #object, not _io.StringIO
    
    with as_handle(filename) as fh:
        return fh.readlines()   


    # This fails ## doesnt fail anymore
plink_data = StringIO('StringIO data.')
print('This is the error.')
pprint(processFile(plink_data))

输出:

This is the error.
['StringIO data.']

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