有没有更优雅的方法将包含 mpz 值的文本文件读入整数列表?

Is there a more elegant way to read a Textfile containing mpz values into a list of integers?

我有一个包含数字的文本文件,如下所示:

[mpz(0), mpz(0), mpz(0), mpz(0), mpz(4), mpz(54357303843626),...]

是否有一种简单的方法可以将其直接解析为整数列表?目标数据类型是 mpz 整数还是普通 python 整数并不重要。

到目前为止我尝试并工作的是纯解析(注意:目标数组 y_val3 需要提前用零初始化,因为它可能比文本文件中的列表大):

text_file = open("../prod_sum_copy.txt", "r")
content = text_file.read()[1:-1]
text_file.close()
content_list = content.split(",")
y_val3 = [0]*10000
print(content_list)
for idx, str in enumerate(content_list):
    m = re.search('mpz\(([0-9]+)\)', str)
    y_val3[idx]=int(m.group(1))
print(y_val3)

尽管这种方法可行,但我不确定这是否是最佳实践,或者是否存在比普通解析更优雅的方法。

为方便起见:Here 是 GitHub 上的原始文本文件。注意:此文本文件将来可能会增长,这会影响性能和可伸缩性等方面。

我试着从 human-readable 的角度和性能的角度来看一个更优雅的解决方案。

注意事项:

  • 这里发生了很多事情
  • 我没有原始文件,因此下面的数字与您可能在设备上获得的任何数字都不匹配
  • 尝试对所有不同部分进行基准测试的工作量太大,因此我尝试着重于几个最大的组件

下面的突破和时间安排似乎显示了几种方法中的数量级差异,因此它们可能仍可用于衡量计算工作量水平。

我的第一个方法是尝试测量添加到流程中的文件 read/write 的开销量,以便我们可以探索有多少计算工作仅集中在数据处理步骤上。

为此,我制作了一个函数,其中包括文件读取和测量整个过程,从头到尾查看我的迷你示例文件花费了多长时间。我在 Jupyter notebook 中使用 %timeit 完成了此操作。

然后我将文件读取步骤分解为它自己的函数,然后在数据处理步骤中使用 %timeit 来帮助向我们展示:

  • 原始方法中文件读取与数据处理使用了多少时间
  • 改进方法中的数据处理方法使用了多少时间。

原始方法(在函数中)

import re

def original():
    text_file = open("../prod_sum_copy.txt", "r")
    content = text_file.read()[1:-1]
    text_file.close()
    content_list = content.split(",")

    y_val3 = [0]*10000

    for idx, element in enumerate(content_list):
        m = re.search('mpz\(([0-9]+)\)', element)
        y_val3[idx]=int(m.group(1))
    return y_val3

我假设我的非常短的示例数据的处理时间的很大一部分只是用于打开磁盘上的文件、将数据读入内存、关闭文件等的时间。

%timeit original()
140 µs ± 10.2 µs per loop (mean ± std. dev. of 7 runs, 10,000 loops each)

将读取文件与数据处理方法分开

此方法包括对文件读取过程的微小改进。时序测试不包括文件读取过程,所以我们不知道这个微小的变化对整个过程有多大影响。作为记录,我通过将读取过程封装在 with 上下文管理器(在后台处理关闭)中消除了对 .close() 方法的手动调用,因为这是 Python 最佳实践用于读取文件。

import re

def read_filea():
    with open("../prod_sum_copy.txt", "r") as text_file:
        content = text_file.read()[1:-1]
        return content

content = read_filea()
print(content)
def a():
    y_val3 = [0]*10000
    content_list = content.split(",")
    for idx, element in enumerate(content_list):
        m = re.search('mpz\(([0-9]+)\)', element)
        y_val3[idx]=int(m.group(1))
    return y_val3

通过仅对数据处理部分进行计时,我们发现我们的预测似乎是文件读取 (IO) 在这个简单的测试用例中起着重要作用。它还为我们提供了一个想法,即我们应该为数据处理部分花费多少时间。让我们看看另一种方法,看看我们是否可以 trim 把时间缩短一点。

%timeit read_filea()
21.5 µs ± 185 ns per loop (mean ± std. dev. of 7 runs, 10,000 loops each)

简化的数据处理方法(和单独的读取文件)

这里我们将尝试使用一些Python最佳实践或Python工具来减少总体时间,包括:

  • 列表理解
  • 使用re.findall()方法消除部分直接重复调用re.search()函数和直接重复调用m.group()方法(注意:findall是可能会在后台做一些这样的事情,老实说我不知道​​我们避免它是否会有好处)。但是我发现这种方法的可读性要高于原来的方法。

我们来看代码:

import re

def read_fileb():
    with open("../prod_sum_copy.txt", "r") as text_file:
        content = text_file.read()[1:-1]
    return content

content = read_fileb()

def b():
    y_val3 = [int(element) for element in re.findall(r'mpz\(([0-9]+)\)', content)]
    return y_val3

此方法的数据处理部分比原始方法中的数据处理步骤快大约 10 倍。

%timeit b()
2.89 µs ± 210 ns per loop (mean ± std. dev. of 7 runs, 100,000 loops each)  

有一个巧妙的技巧可以将按 Python 格式打印的数据转换回原始对象。只需执行 obj = eval(string),完整示例如下。

您几乎可以将此 eval 解决方案用于任何 python 对象,甚至是通过 print(python_object) 或类似方式打印到文件的复杂对象。基本上任何有效的 python 代码都可以通过 eval().

从字符串转换而来

eval() 允许根本不使用任何字符串 processing/parsing 函数,没有正则表达式或其他任何东西。

请注意,eval() 不会检查它运行的是什么字符串,因此如果字符串来自未知来源,则其中可能包含恶意代码,此代码可以对您的 PC 执行任何操作,因此 eval() 仅在受信任的情况下执行代码字符串。

下面的代码使用了带有示例文件内容的 text 字符串。我使用字符串而不是文件作为示例,因此 Whosebug 访问者可以完全运行我的代码,而无需依赖。在 read-only 打开文件 f 的情况下,您只需将 for line in text.split('\n'): 替换为 for line in f: 即可,代码有效。

Try it online!

from gmpy2 import mpz

text = '''
[mpz(12), mpz(34), mpz(56)]
[mpz(78), mpz(90), mpz(21)]
'''

nums = []
for line in text.split('\n'):
    if not line.strip():
        continue
    nums.append(eval(line))

print(nums)

输出:

[[mpz(12), mpz(34), mpz(56)], [mpz(78), mpz(90), mpz(21)]]