如何清空StringIO对象?

78

我已经创建了一个StringIO对象,并在其中添加了一些文本。我想要清除它现有的值,以便重复使用它,而不是重新调用它。有没有任何方法可以做到这一点?

3个回答

133

简述

不要费力清除,直接创建一个新的--这样会更快。

方法

Python 2

以下是我查找此类信息的方法:

>>> from StringIO import StringIO
>>> dir(StringIO)
['__doc__', '__init__', '__iter__', '__module__', 'close', 'flush', 'getvalue', 'isatty', 'next', 'read', 'readline', 'readlines', 'seek', 'tell', 'truncate', 'write', 'writelines']
>>> help(StringIO.truncate)
Help on method truncate in module StringIO:

truncate(self, size=None) unbound StringIO.StringIO method
    Truncate the file's size.

    If the optional size argument is present, the file is truncated to
    (at most) that size. The size defaults to the current position.
    The current file position is not changed unless the position
    is beyond the new file size.

    If the specified size exceeds the file's current size, the
    file remains unchanged.

所以,您想要.truncate(0)。但初始化一个新的StringIO可能会更便宜(和更容易)。请参见下面的基准测试。

Python 3

(感谢tstone2077指出了不同之处。)

>>> from io import StringIO
>>> dir(StringIO)
['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__enter__', '__eq__', '__exit__', '__format__', '__ge__', '__getattribute__', '__getstate__', '__gt__', '__hash__', '__init__', '__iter__', '__le__', '__lt__', '__ne__', '__new__', '__next__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__setstate__', '__sizeof__', '__str__', '__subclasshook__', '_checkClosed', '_checkReadable', '_checkSeekable', '_checkWritable', 'close', 'closed', 'detach', 'encoding', 'errors', 'fileno', 'flush', 'getvalue', 'isatty', 'line_buffering', 'newlines', 'read', 'readable', 'readline', 'readlines', 'seek', 'seekable', 'tell', 'truncate', 'writable', 'write', 'writelines']
>>> help(StringIO.truncate)
Help on method_descriptor:

truncate(...)
    Truncate size to pos.

    The pos argument defaults to the current file position, as
    returned by tell().  The current file position is unchanged.
    Returns the new absolute position.

需要注意的是,现在当前文件的位置未改变,而将其截断为零则会在Python 2版本中重置位置。

因此,在Python 2中,您只需要:

>>> from cStringIO import StringIO
>>> s = StringIO()
>>> s.write('foo')
>>> s.getvalue()
'foo'
>>> s.truncate(0)
>>> s.getvalue()
''
>>> s.write('bar')
>>> s.getvalue()
'bar'

如果你在Python 3中这样做,你将得不到你期望的结果:

>>> from io import StringIO
>>> s = StringIO()
>>> s.write('foo')
3
>>> s.getvalue()
'foo'
>>> s.truncate(0)
0
>>> s.getvalue()
''
>>> s.write('bar')
3
>>> s.getvalue()
'\x00\x00\x00bar'

所以在Python 3中,您还需要重置位置:

>>> from cStringIO import StringIO
>>> s = StringIO()
>>> s.write('foo')
3
>>> s.getvalue()
'foo'
>>> s.truncate(0)
0
>>> s.seek(0)
0
>>> s.getvalue()
''
>>> s.write('bar')
3
>>> s.getvalue()
'bar'

在 Python 2 代码中使用 truncate 方法时,最好同时调用 seek(0) 方法(在之前或之后都可以),以避免将来将其移植到 Python 3 时代码出现问题。另外,创建一个新的 StringIO 对象也是一个很好的选择!

时间

Python 2

>>> from timeit import timeit
>>> def truncate(sio):
...     sio.truncate(0)
...     return sio
... 
>>> def new(sio):
...     return StringIO()
... 

使用StringIO时为空:

>>> from StringIO import StringIO
>>> timeit(lambda: truncate(StringIO()))
3.5194039344787598
>>> timeit(lambda: new(StringIO()))
3.6533868312835693

使用StringIO存储3KB的数据:

>>> timeit(lambda: truncate(StringIO('abc' * 1000)))
4.3437709808349609
>>> timeit(lambda: new(StringIO('abc' * 1000)))
4.7179079055786133

同样适用于cStringIO:

>>> from cStringIO import StringIO
>>> timeit(lambda: truncate(StringIO()))
0.55461597442626953
>>> timeit(lambda: new(StringIO()))
0.51241087913513184
>>> timeit(lambda: truncate(StringIO('abc' * 1000)))
1.0958449840545654
>>> timeit(lambda: new(StringIO('abc' * 1000)))
0.98760509490966797

忽略可能的内存问题(del oldstringio),截断一个 StringIO.StringIO 更快(空字符串3%更快,3KB数据8%更快),但是创建一个新的 cStringIO.StringIO 更快(空字符串8%更快,3KB数据10%更快)。所以我建议只使用最简单的方法 - 假设您正在使用CPython,请使用cStringIO并创建新的实例。

Python 3

相同的代码,只需添加 seek(0)

>>> def truncate(sio):
...     sio.truncate(0)
...     sio.seek(0)
...     return sio
... 
>>> def new(sio):
...     return StringIO()
...

当为空时:

>>> from io import StringIO
>>> timeit(lambda: truncate(StringIO()))
0.9706327870007954
>>> timeit(lambda: new(StringIO()))
0.8734330690022034

使用3KB的数据:

>>> timeit(lambda: truncate(StringIO('abc' * 1000)))
3.5271066290006274
>>> timeit(lambda: new(StringIO('abc' * 1000)))
3.3496507499985455

因此,对于Python 3来说,创建一个新的空白对象比重复使用空白对象更快11%,创建一个新的而不是重用3K对象会更快5%。同样,创建一个新的StringIO对象而不是截断和搜索。


2
我会点赞这个回答,但是它过于注重性能,尤其是当原帖明确要求清晰且可重用时。哦,实际上我还是会点赞的,因为点赞应该意味着“这个回答有用”,但我仍然认为“所以你想要truncate(0)”之后的所有内容都应该被删除。 - Peter Hansen
4
我认为这个代码是有用的,因为它证明了我的期望(至少在cStringIO库中):与截断现有的StringIO对象相比,创建一个新的StringIO对象更加高效。 - Chris Morgan
我的观点正是如此,即当OP表示他想要做一些除了创建新实例之外的事情时(至少我是这样解释他的请求以避免“重新调用”),这并不意味着它更好。 - Peter Hansen
3
@PeterHansen,我来得很晚了,但我非常感激你提供的时间信息。它表明我的假设(也许是OP的假设)——截断会有更好的性能是不必要的,通常我们最好从新开始。有时候,最好的答案表明你问错了问题(特别是当它们同时提供直接的答案时)。 - TimothyAWiseman
1
截断在修改StringIO时非常有用,这通常是在使用StringIO而不是字符串时进行的引用修改。 - Chris
显示剩余4条评论

16

有一点需要注意(至少对于Python 3.2而言):

在使用truncate(0)之前需要调用seek(0)。这是一段没有调用seek(0)的代码:

from io import StringIO
s = StringIO()
s.write('1'*3)
print(repr(s.getvalue()))
s.truncate(0)
print(repr(s.getvalue()))
s.write('1'*3)
print(repr(s.getvalue()))

输出为:

'111'
''
'\x00\x00\x00111'

在截断之前使用seek(0),我们可以获得预期的输出:

'111'
''
'111'

1
谢谢你的建议,你说得完全正确。我已经根据这个更新了我的答案;寻找和截断的额外负担进一步巩固了创建新StringIO的明智路径的位置。 - Chris Morgan
2
不幸的是,我确实需要重用相同的StringIO实例,因为我正在单元测试中使用@patch修饰符的sys.stdout,并且当调用patch()时,无法访问实例变量,因为实例尚未创建。 我已将seek(0)和truncate(0)添加到我的setup()函数中。 谢谢! - Huw Walters

3
我如何成功优化了处理多个文件序列的过程(分批次读取,处理每个批次,将处理后的流写入文件)是通过复用相同的实例,并在使用后始终它,然后写入它,接着。这样做,我只截断当前文件不需要的末尾部分。这似乎给我带来了约3%的性能提升。更专业的人士可以确认这是否确实优化了内存分配。
sio = cStringIO.StringIO()
for file in files:
    read_file_chunks_and_write_to_sio(file, sio)
    sio.truncate()
    with open('out.bla', 'w') as f:
        f.write(sio.getvalue())
    sio.reset()

是的,这个特殊情况可能会更好。您能否展示基准测试,使用类似于我的代码(对于StringIOcStringIO)?我很想看看它们。 - Chris Morgan
@ChrisMorgan 抱歉,我现在才看到你的评论... 现在太晚了,无法深入挖掘我的记忆/源文件夹 :) - Erik Kaplun
1
请注意,在Python>=3.9中,reset方法已经不存在了:'_io.StringIO' object has no attribute 'reset' - mds

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