【问题标题】:Meaningful IO error messages without overhead in Python在 Python 中没有开销的有意义的 IO 错误消息
【发布时间】:2017-07-04 04:39:27
【问题描述】:

我有以下两难境地。我正在解析巨大的 CSV 文件,理论上可以包含无效记录,python。为了能够快速解决问题,我想查看错误消息中的行号。但是,由于我正在解析许多文件并且错误非常罕见,我不希望我的错误处理增加了主管道的开销。这就是为什么我不想使用enumerate 或类似方法的原因。

简而言之,我正在寻找一个像这样工作的get_line_number 函数:

with open('file.csv', 'r') as f:
    for line in f:
        try:
            process(line)
        except:
            line_no = get_line_number(f)
            raise RuntimeError('Error while processing the line ' + line_no)

但是,这似乎很复杂,因为 f.tell() will not work 在这个循环中。

编辑:

似乎开销相当大。在我的真实案例中(这很痛苦,因为文件是非常短的记录列表:单个浮点数、int-float 对或 string-int 对;file.csv 大约有 800MB 大,大约有 80M 行),它是enumerate 的每个文件读取大约需要 2.5 秒。出于某种原因,fileinput 非常很慢。

import timeit
s = """
with open('file.csv', 'r') as f:
    for line in f:
        pass
"""
print(timeit.repeat(s, number = 10, repeat = 3))
s = """
with open('file.csv', 'r') as f:
    for idx, line in enumerate(f):
        pass
"""
print(timeit.repeat(s, number = 10, repeat = 3))
s = """
count = 0
with open('file.csv', 'r') as f:
    for line in f:
        count += 1
"""
print(timeit.repeat(s, number = 10, repeat = 3))
setup = """
import fileinput
"""
s = """
for line in fileinput.input('file.csv'):
    pass
"""
print(timeit.repeat(s, setup = setup, number = 10, repeat = 3))

输出

[45.790788270998746, 44.88589363079518, 44.93949336092919]
[70.25306860171258, 70.28569177398458, 70.2074502906762]
[75.43606997421011, 74.39759518811479, 75.02027251804247]
[325.1898657102138, 321.0400970801711, 326.23809849238023]

编辑 2:

接近真实世界的场景。 try-except 子句位于循环之外以减少开销。

import timeit
setup = """
def process(line):
    if float(line) < 0.5:
        outliers += 1
"""
s = """
outliers = 0
with open('file.csv', 'r') as f:
    for line in f:
        process(line)
"""
print(timeit.repeat(s, setup = setup, number = 10, repeat = 3))
s = """
outliers = 0
with open('file.csv', 'r') as f:
    try:
        for idx, line in enumerate(f):
            process(line)
    except ValueError:
        raise RuntimeError('Invalid value in line' + (idx + 1)) from None
"""
print(timeit.repeat(s, setup = setup, number = 10, repeat = 3))

输出

[244.9097429071553, 242.84596176538616, 242.74369075801224
[293.32093235617504, 274.17732743313536, 274.00854821596295]

因此,就我而言,enumerate 的开销约为 10%。

【问题讨论】:

  • 所以,我不得不问,问题是您的示例运行速度太慢还是您认为可能运行速度太慢?它实际上对性能有多大影响?您是否测量了您知道没有错误的文件的差异?
  • 哇,没想到会慢 2 倍。将您的 process(line) 调用包含在 try/catch 中会有多大影响?
  • 我也不会,但是用pass 代替真正对数据做的任何事情并不是最公平的比较。此外,一个 csv 文件每行只有 10 个字节是非常不寻常的。
  • @nigel222:你的平台是什么?
  • 您可以在try/except 内运行整个循环,从而减少开销。

标签: python performance io


【解决方案1】:

请使用enumerate

for line_ref, line in enumerate(f):
    line_no = line_ref + 1  # enumerate starts at zero

它不会增加任何显着的开销。从文件中取出记录所涉及的工作大大超过了保存计数器所涉及的工作,并且for语句中的元组赋值只是名称绑定,而不是line引用的数据的额外副本

更换更新:

在生成我的测试文件时出错。现在几乎已经确认了添加到问题中的第一个计时测试。

我个人认为,对于具有 10 字节记录的最坏(ish)情况的文件,10% 的开销是完全可以接受的,因为替代方案不知道 8000 万条记录中的哪一条有错误。

【讨论】:

  • OP 说他不想使用enumerate。如果您要使用他说他不想使用的东西来发布答案,您至少可以解释为什么他想要使用它。
  • 使用enumerate(f, 1)从1开始计数。
【解决方案2】:

如果您确定添加调试信息的开销太大(我不想就该主题争论),您可以实现该函数的两个版本。具有彻底检查和详细调试的高性能一对一。基本思路是:

try:
    func_quick(args)
except Exception:
    func_verbose(args)

缺点是出现错误时会重新开始处理。但是如果你必须手动纠正错误,在这种情况下浪费几秒钟的惩罚应该没有什么坏处。此外 func_verbose() 不必在第一个错误时停止,并且可以检查整个文件并列出所有错误。

【讨论】:

    【解决方案3】:

    标准库fileinput 模块内存高效地处理大文件并提供内置的行号计数器。它还自动为要从命令行参数读取的文件选择多个文件名。然而,似乎没有一种(简单的?)方法可以将它与上下文处理程序一起使用。

    至于性能,您需要与其他方法进行比较来测试它。

    import fileinput
    
    for line in fileinput.input():
        try:
            process(line)
        except:
            line_no = fileinput.filelineno()
            raise RuntimeError('Error while processing the line ' + line_no)
    

    顺便说一句,我建议只捕获相关异常,可能是自定义异常,否则您将屏蔽意外异常。

    【讨论】:

      猜你喜欢
      • 2020-01-26
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2016-10-19
      • 2013-06-14
      • 1970-01-01
      相关资源
      最近更新 更多