有意义的IO错误消息,没有Python的开销

时间:2017-02-15 13:28:37

标签: python performance io

我有以下两难困境。我正在使用python解析大量的CSV文件,理论上这些文件可能包含无效记录。为了能够快速解决问题,我希望在错误消息中看到行号。但是,由于我正在解析许多文件并且错误非常罕见,我不希望我的错误处理向主管道添加开销。这就是我不想使用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%。

3 个答案:

答案 0 :(得分:2)

请使用enumerate

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

它没有增加任何重大开销。从文件中获取记录所涉及的工作远远超过了保留计数器所涉及的工作,而for语句中的元组赋值只是一个名称绑定,而不是line引用的数据的额外副本。

替换更新:

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

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

答案 1 :(得分:1)

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

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

缺点是当发生错误时,处理将再次开始。但是,如果你必须手动纠正错误,在这种情况下浪费几秒钟的惩罚不应该伤害。此外,func_verbose()不必在第一个错误时停止,并可能检查整个文件并列出所有错误。

答案 2 :(得分:0)

标准库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)

BTW我建议只捕获相关的异常,可能是自定义异常,否则你将掩盖意外的异常。