【问题标题】:Why is TextIOWrapper closing the given BytesIO stream?为什么 TextIOWrapper 关闭给定的 BytesIO 流?
【发布时间】:2018-07-04 04:52:45
【问题描述】:

如果我在 python 3 中运行以下代码

from io import BytesIO
import csv
from io import TextIOWrapper


def fill_into_stringio(input_io):
    writer = csv.DictWriter(TextIOWrapper(input_io, encoding='utf-8'),fieldnames=['ids'])
    for i in range(100):
        writer.writerow({'ids': str(i)})

with BytesIO() as input_i:
    fill_into_stringio(input_i)
    input_i.seek(0)

我收到一个错误:

ValueError: I/O operation on closed file.

如果我不使用 TextIOWrapper,则 io 流将保持打开状态。例如,如果我将函数修改为

def fill_into_stringio(input_io):
    for i in range(100):
        input_io.write(b'erwfewfwef')

我不再收到任何错误,因此出于某种原因,TestIOWrapper 正在关闭我以后想从中读取的流。这是打算这样吗?是否有办法在不自己编写 csv 编写器的情况下实现我正在尝试的目标?

【问题讨论】:

  • 省略的回溯表明被拒绝的 I/O 操作是最终的seek(0),而不是fill_into_stringio 中的任何writerow 操作。

标签: python python-3.x csv bytesio


【解决方案1】:

csv 模块在这里很奇怪;大多数包装其他对象的类文件对象都假定有问题的对象的所有权,当它们自己关闭(或以其他方式清理)时关闭它。

避免该问题的一种方法是在允许清理之前从TextIOWrapper 明确地detach

def fill_into_stringio(input_io):
    # write_through=True prevents TextIOWrapper from buffering internally;
    # you could replace it with explicit flushes, but you want something 
    # to ensure nothing is left in the TextIOWrapper when you detach
    text_input = TextIOWrapper(input_io, encoding='utf-8', write_through=True)
    try:
        writer = csv.DictWriter(text_input, fieldnames=['ids'])
        for i in range(100):
            writer.writerow({'ids': str(i)})
    finally:
        text_input.detach()  # Detaches input_io so it won't be closed when text_input cleaned up

避免这种情况的唯一其他内置方法是针对真实文件对象,您可以在其中向它们传递文件描述符和closefd=False,并且在close-ed 或以其他方式清理时它们不会关闭底层文件描述符起来。

当然,在您的特定情况下,有一种更简单的方法:只需让您的函数期望基于文本的类文件对象并使用它们而无需重新包装;你的函数真的不应该负责对调用者的输出文件进行编码(如果调用者想要 UTF-16 输出怎么办?)。

那么你可以这样做:

from io import StringIO

def fill_into_stringio(input_io):
    writer = csv.DictWriter(input_io, fieldnames=['ids'])
    for i in range(100):
        writer.writerow({'ids': str(i)})

# newline='' is the Python 3 way to prevent line-ending translation
# while continuing to operate as text, and it's recommended for any file
# used with the csv module
with StringIO(newline='') as input_i:
    fill_into_stringio(input_i)
    input_i.seek(0)
    # If you really need UTF-8 bytes as output, you can make a BytesIO at this point with:
    # BytesIO(input_i.getvalue().encode('utf-8'))

【讨论】:

  • BytesIO(input_i.getvalue().encode('utf-8')) 是否复制整个内容并对其进行编码?流将有很多行,所以如果我以后不必翻译它会更快,如果它复制然后我可能会遇到内存问题,我必须通过引入一半大小的块来解决。
  • @YannickSSE:它确实做了一个完整的复制,但如果你真的接近内存问题,正确的解决方案是切换到真正的文件类对象(例如tempfile.TemporaryFile),这样你就可以溢出到磁盘。您始终可以使用detach 方法而无需加倍内存,但想要 CSV(自然面向文本)表示为二进制数据是非常奇怪的;就个人而言,我会一直使用文本类型。
  • 我想我明白你在说什么,但我正在做的是,我从源获取数据,生成一些新数据并以 csv 格式发送以存储到服务器。所以我不想在本地存储文件。我将它发送到的服务器不是我的,并且有一个需要字节的 API。
  • @YannickSSE:tempfile.TemporaryFile 就是为此而生的;它由磁盘支持,但它在磁盘上没有名称,并且当它关闭时,数据就消失了。当内存可用时(因为它使用真正的系统调用),它比BytesIO/StringIO 慢,但它也不受内存限制。仅仅因为它在磁盘上并不意味着它会永远存在。它正在使用磁盘进行临时存储,完成后会被删除。
  • 这几乎把我逼疯了。祝福你的灵魂得到你的答案。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-11-15
  • 1970-01-01
  • 1970-01-01
  • 2017-05-19
  • 1970-01-01
相关资源
最近更新 更多