在 Python 中加快字符串与对象的配对

Posted

技术标签:

【中文标题】在 Python 中加快字符串与对象的配对【英文标题】:Speeding up pairing of strings into objects in Python 【发布时间】:2012-10-07 06:05:40 【问题描述】:

我正在尝试找到一种有效的方法来将包含整数点的数据行配对,并将它们存储为 Python 对象。数据由XY 坐标点组成,以逗号分隔的字符串表示。这些点必须配对,如(x_1, y_1), (x_2, y_2), ... 等,然后存储为对象列表,其中每个点都是一个对象。 get_data 下面的函数会生成这个示例数据:

def get_data(N=100000, M=10):
    import random
    data = []
    for n in range(N):
        pair = [[str(random.randint(1, 10)) for x in range(M)],
                [str(random.randint(1, 10)) for x in range(M)]]
        row = [",".join(pair[0]),
               ",".join(pair[1])]
        data.append(row)
    return data

我现在的解析代码是:

class Point:
    def __init__(self, a, b):
        self.a = a
        self.b = b

def test():
    import time
    data = get_data()
    all_point_sets = []
    time_start = time.time()
    for row in data:
        point_set = []
        first_points, second_points = row
        # Convert points from strings to integers
        first_points = map(int, first_points.split(","))
        second_points = map(int, second_points.split(","))
        paired_points = zip(first_points, second_points)
        curr_points = [Point(p[0], p[1]) \
                       for p in paired_points]
        all_point_sets.append(curr_points)
    time_end = time.time()
    print "total time: ", (time_end - time_start)

目前,100,000 个点需要将近 7 秒,这似乎非常低效。部分低效率似乎源于first_pointssecond_pointspaired_points 的计算——以及将这些转换为对象。

效率低下的另一部分似乎是all_point_sets 的建立。取出 all_point_sets.append(...) 行似乎使代码从 ~7 秒缩短到 2 秒!

如何加快速度?谢谢。

跟进感谢大家的好建议 - 他们都很有帮助。但即使进行了所有改进,处理 100,000 个条目仍然需要大约 3 秒。我不确定为什么在这种情况下它不仅仅是即时的,以及是否有另一种表示可以使它即时。在 Cython 中编码会改变事情吗?有人可以提供一个例子吗?再次感谢。

【问题讨论】:

如果你想处理整数列表,为什么要把它们变成字符串,用逗号连接它们,然后在 test 函数中将它们拆分并返回整数? 数据是一个制表符分隔的文件,由逗号分隔的整数列表组成。 get_data 只是模拟这个。显然,如果我将这些数据作为解析整数列表,那么就不会有解析问题...... python -mcProfile your_script.py 说什么? 我认为这将是一个更好的测试用例,如果您将使用 `get_data' 生成的数据以您必须处理的格式写入文件。 您需要多久加载一次文件?如果您只需要执行一次,3 秒似乎完全可以接受。如果您发现自己重复加载相同的数据(或缓慢变化的数据),请考虑将其缓存在替代表示中以加快加载速度。 【参考方案1】:

在处理创建大量个对象时,通常可以使用的最大性能增强是关闭垃圾收集器。每一“代”对象,垃圾收集器都会遍历内存中的所有活动对象,寻找属于循环的一部分但活动对象未指向的对象,从而有资格进行内存回收。有关一些信息,请参阅Doug Helmann's PyMOTW GC article(更多信息可能可以通过 google 和一些决心找到)。默认情况下,垃圾收集器每 700 个左右创建但未回收的对象运行一次,后续生成的运行频率稍低(我忘记了确切的细节)。

使用标准元组而不是 Point 类可以为您节省一些时间(使用命名元组介于两者之间),而巧妙的拆包可以节省一些时间,但最大的收获可以通过在您之前关闭 gc创建许多您知道不需要 gc'd 的对象,然后再将其重新打开。

一些代码:

def orig_test_gc_off():
    import time
    data = get_data()
    all_point_sets = []
    import gc
    gc.disable()
    time_start = time.time()
    for row in data:
        point_set = []
        first_points, second_points = row
        # Convert points from strings to integers
        first_points = map(int, first_points.split(","))
        second_points = map(int, second_points.split(","))
        paired_points = zip(first_points, second_points)
        curr_points = [Point(p[0], p[1]) \
                       for p in paired_points]
        all_point_sets.append(curr_points)
    time_end = time.time()
    gc.enable()
    print "gc off total time: ", (time_end - time_start)

def test1():
    import time
    import gc
    data = get_data()
    all_point_sets = []
    time_start = time.time()
    gc.disable()
    for index, row in enumerate(data):
        first_points, second_points = row
        curr_points = map(
            Point,
            [int(i) for i in first_points.split(",")],
            [int(i) for i in second_points.split(",")])
        all_point_sets.append(curr_points)
    time_end = time.time()
    gc.enable()
    print "variant 1 total time: ", (time_end - time_start)

def test2():
    import time
    import gc
    data = get_data()
    all_point_sets = []
    gc.disable()
    time_start = time.time()
    for index, row in enumerate(data):
        first_points, second_points = row
        first_points = [int(i) for i in first_points.split(",")]
        second_points = [int(i) for i in second_points.split(",")]
        curr_points = [(x, y) for x, y in zip(first_points, second_points)]
        all_point_sets.append(curr_points)
    time_end = time.time()
    gc.enable()
    print "variant 2 total time: ", (time_end - time_start)

orig_test()
orig_test_gc_off()
test1()
test2()

一些结果:

>>> %run /tmp/flup.py
total time:  6.90738511086
gc off total time:  4.94075202942
variant 1 total time:  4.41632509232
variant 2 total time:  3.23905301094

【讨论】:

这里是重新启用垃圾收集之前的结束时间。我怀疑总(实际)时间可能没有那么大的改进,因为在那之后垃圾收集仍然需要时间。 @Keith,同意。但是我们只做一个垃圾回收周期而不是很多,而且我们不做的垃圾回收周期越多,节省的就越多。我们意识到,在工作中处理多到数十 GB 的输入文件时,可以节省大量资金,其中每隔几行就创建一个对象。如果你的程序在你的主循环之后完成,不要重新打开 gc,让操作系统回收内存。 这可以通过避免在循环中的对象上调用函数来进一步优化。例如在循环外设置“fpsplit = first_points.split”,然后在循环内调用 fpsplit(",")。解决“。”可能需要相当长的时间。【参考方案2】:

简单地使用 pypy 运行会有很大的不同

$ python pairing_strings.py 
total time:  2.09194397926
$ pypy pairing_strings.py 
total time:  0.764246940613

禁用 gc 对 pypy 没有帮助

$ pypy pairing_strings.py 
total time:  0.763386964798

Point 的命名元组使情况变得更糟

$ pypy pairing_strings.py 
total time:  0.888827085495

使用 itertools.imap 和 itertools.izip

$ pypy pairing_strings.py 
total time:  0.615751981735

使用 int 的记忆版本和迭代器来避免 zip

$ pypy pairing_strings.py 
total time:  0.423738002777 

这是我完成的代码。

def test():
    import time
    def m_int(s, memo=):
        if s in memo:
            return memo[s]
        else:
            retval = memo[s] = int(s)
            return retval
    data = get_data()
    all_point_sets = []
    time_start = time.time()
    for xs, ys in data:
        point_set = []
        # Convert points from strings to integers
        y_iter = iter(ys.split(","))
        curr_points = [Point(m_int(i), m_int(next(y_iter))) for i in xs.split(",")]
        all_point_sets.append(curr_points)
    time_end = time.time()
    print "total time: ", (time_end - time_start)

【讨论】:

【参考方案3】:

我愿意

使用numpy 数组解决这个问题(Cython 是一个选项,如果这仍然不够快)。 将点存储为向量而不是单个 Point 实例。 依赖现有的解析器 (如果可能)解析一次数据,然后以二进制格式(如 hdf5)存储以供进一步计算,这将是最快的选择(见下文)

Numpy 内置了读取文本文件的函数,例如loadtxt。 如果您将数据存储在结构化数组中,则不一定需要将其转换为另一种数据类型。 我将使用Pandas,它是在numpy 之上构建的库。处理和处理结构化数据更方便一些。 Pandas 有自己的文件解析器read_csv

为了计时,我将数据写入一个文件,就像你原来的问题一样(它基于你的get_data):

import numpy as np
import pandas as pd

def create_example_file(n=100000, m=20):
    ex1 = pd.DataFrame(np.random.randint(1, 10, size=(10e4, m)),
                       columns=(['x_%d' % x for x in range(10)] +
                                ['y_%d' % y for y in range(10)]))
    ex1.to_csv('example.csv', index=False, header=False)
    return

这是我用来读取pandas.DataFrame中数据的代码:

def with_read_csv(csv_file):
    df = pd.read_csv(csv_file, header=None,
                     names=(['x_%d' % x for x in range(10)] +
                            ['y_%d' % y for y in range(10)]))
    return df

(请注意,我假设您的文件中没有标题,因此我必须创建列名。)

读取数据的速度很快,内存效率应该更高(请参阅this question),并且数据存储在一个数据结构中,您可以以一种快速、矢量化的方式进一步处理:

In [18]: %timeit string_to_object.with_read_csv('example.csv')
1 loops, best of 3: 553 ms per loop

开发分支中有一个新的C based parser,在我的系统上需要 414 毫秒。 您的测试在我的系统上花费了 2.29 秒,但实际上并没有可比性,因为数据不是从文件中读取的,而是您创建了 Point 实例。

如果您曾经读入过数据,您可以将其存储在hdf5 文件中:

In [19]: store = pd.HDFStore('example.h5')

In [20]: store['data'] = df

In [21]: store.close()

下次需要数据的时候可以从这个文件中读取,真的很快:

In [1]: store = pd.HDFStore('example.h5')

In [2]: %timeit df = store['data']
100 loops, best of 3: 16.5 ms per loop

但是,它仅适用于您多次需要相同数据的情况。

在进行进一步计算时,将基于 numpy 的数组用于大型数据集将具有优势。如果您可以使用矢量化的numpy 函数和索引,Cython 不一定会更快,如果您确实需要迭代,它会更快(另请参阅this answer)。

【讨论】:

【参考方案4】:

更快的方法,使用 Numpy(加速大约 7x):

import numpy as np
txt = ','.join(','.join(row) for row in data)
arr = np.fromstring(txt, dtype=int, sep=',')
return arr.reshape(100000, 2, 10).transpose((0,2,1))

性能对比:

def load_1(data):
    all_point_sets = []
    gc.disable()
    for xs, ys in data:
        all_point_sets.append(zip(map(int, xs.split(',')), map(int, ys.split(','))))
    gc.enable()
    return all_point_sets

def load_2(data):
    txt = ','.join(','.join(row) for row in data)
    arr = np.fromstring(txt, dtype=int, sep=',')
    return arr.reshape(100000, 2, 10).transpose((0,2,1))

load_1 在我的机器上运行 1.52 秒; load_20.20 秒内运行,提高了 7 倍。这里最大的警告是,它要求您 (1) 提前知道所有内容的长度,以及 (2) 每行包含完全相同数量的点。这适用于您的 get_data 输出,但可能不适用于您的真实数据集。

【讨论】:

【参考方案5】:

通过使用数组和一个在访问时懒惰地构造 Point 对象的持有者对象,我得到了 50% 的改进。我还“开槽”了 Point 对象以获得更好的存储效率。但是,元组可能会更好。

如果可能的话,更改数据结构也可能会有所帮助。但这永远不会是瞬时的。

from array import array

class Point(object):
    __slots__ = ["a", "b"]
    def __init__(self, a, b):
        self.a = a
        self.b = b

    def __repr__(self):
        return "Point(%d, %d)" % (self.a, self.b)

class Points(object):
    def __init__(self, xs, ys):
        self.xs = xs
        self.ys = ys

    def __getitem__(self, i):
        return Point(self.xs[i], self.ys[i])

def test3():
    xs = array("i")
    ys = array("i")
    time_start = time.time()
    for row in data:
        xs.extend([int(val) for val in row[0].split(",")])
        ys.extend([int(val) for val in row[1].split(",")])
    print ("total time: ", (time.time() - time_start))
    return Points(xs, ys)

但是在处理大量数据时,我通常会使用 numpy N 维数组(ndarray)。如果可以更改原始数据结构,那么这可能是最快的。如果它可以被构造成线性读取 x,y 对,然后重塑 ndarray。

【讨论】:

【参考方案6】:

    使Point 成为namedtuple(~10% 加速):

    from collections import namedtuple
    Point = namedtuple('Point', 'a b')
    

    在迭代期间解包(~2-4% 加速):

    for xs, ys in data:
    

    使用n-map 的参数形式来避免压缩(~10% 加速):

    curr_points = map(Point,
        map(int, xs.split(',')),
        map(int, ys.split(',')),
    )
    

鉴于点集很短,生成器可能有点矫枉过正,因为它们具有更高的固定开销。

【讨论】:

+1,但是.. 我想指出,虽然namedtuple 在创建过程中加快了速度,但读取速度却慢得多。此处的快速测试复制了创建期间约 10% 的加速,但也显示了读取期间 100% 的减速(即时间加倍)。在这里,如果我访问每个Pointab 三次,我会失去从~10% 加速中节省的时间。 (即,使用namedtuple 阅读 3 次需要多花费约 1 秒) 如果你使用元组索引读取它并不会变慢,尽管我同意这是一个缺点。 如果您要建立索引,那么完全不使用Point 会快得多很多。 ;)(即map(None, map(...), map(...)))然后我的7.7s变成3.1s,而不是使用namedtuple的6.9s和gc.disable()的2.7s。 你确实是对的。我能得到的绝对最快的是all_point_sets.append(zip(map(int, xs.split(',')), map(int, ys.split(',')))),比原来快了大约60%。【参考方案7】:

cython 能够将速度提高 5.5 倍

$ python split.py
total time:  2.16252303123
total time:  0.393486022949

这是我使用的代码

split.py

import time
import pyximport; pyximport.install()
from split_ import test_


def get_data(N=100000, M=10):
    import random
    data = []
    for n in range(N):
        pair = [[str(random.randint(1, 100)) for x in range(M)],
                [str(random.randint(1, 100)) for x in range(M)]]
        row = [",".join(pair[0]),
               ",".join(pair[1])]
        data.append(row)
    return data

class Point:
    def __init__(self, a, b):
        self.a = a
        self.b = b

def test(data):
    all_point_sets = []
    for row in data:
        point_set = []
        first_points, second_points = row
        # Convert points from strings to integers
        first_points = map(int, first_points.split(","))
        second_points = map(int, second_points.split(","))
        paired_points = zip(first_points, second_points)
        curr_points = [Point(p[0], p[1]) \
                       for p in paired_points]
        all_point_sets.append(curr_points)
    return all_point_sets

data = get_data()
for func in test, test_:
    time_start = time.time()
    res = func(data)
    time_end = time.time()
    print "total time: ", (time_end - time_start)

split_.pyx

from libc.string cimport strsep
from libc.stdlib cimport atoi

cdef class Point:
    cdef public int a,b

    def __cinit__(self, a, b):
        self.a = a
        self.b = b

def test_(data):
    cdef char *xc, *yc, *xt, *yt
    cdef char **xcp, **ycp
    all_point_sets = []
    for xs, ys in data:
        xc = xs
        xcp = &xc
        yc = ys
        ycp = &yc
        point_set = []
        while True:
            xt = strsep(xcp, ',')
            if xt is NULL:
                break
            yt = strsep(ycp, ",")
            point_set.append(Point(atoi(xt), atoi(yt)))
        all_point_sets.append(point_set)
    return all_point_sets

进一步探索,我可以大致分解一些 cpu 资源

         5% strsep()
         9% atoi()
        23% creating Point instances
        35% all_point_sets.append(point_set)

如果 cython 能够直接从 csv(或其他)文件中读取,而不必遍历 Python 对象,我希望会有改进。

【讨论】:

+1 用于 cython 解决方案。但是,使用文件解析器将文件中的数据读取到 numpy 数组(或数据帧)中所花费的时间几乎与使用文件解析器的时间一样长(OP 的基本问题是什么,但问题中并没有真正反映什么,而是在 cmets 中) )。不确定对于基本问题什么会更快。 @bmu,是的,现在大部分剩余时间都花在将字符串转换为整数上。对此无能为力。基本问题是 OP 使用糟糕的文件格式来存储数据。 pickle 或可以直接读入数组的二进制文件会快得多 确实如此。我在答案中添加了一个使用 hdf5 的示例,这比解析文件快 30 倍左右。 hdf5 非常适合此类问题,我越来越多地使用它。【参考方案8】:

你可以剃掉几秒钟:

class Point2(object):
    __slots__ = ['a','b']
    def __init__(self, a, b):
        self.a = a
        self.b = b

def test_new(data):
    all_point_sets = []
    for row in data:
        first_points, second_points = row
        r0 = map(int, first_points.split(","))
        r1 = map(int, second_points.split(","))
        cp = map(Point2, r0, r1)
        all_point_sets.append(cp)

这给了我

In [24]: %timeit test(d)
1 loops, best of 3: 5.07 s per loop

In [25]: %timeit test_new(d)
1 loops, best of 3: 3.29 s per loop

通过在all_point_sets 中预分配空间,我可以间歇性地再缩短 0.3 秒,但这可能只是噪音。当然还有让事情变得更快的老式方法:

localhost-2:coding $ pypy pointexam.py
1.58351397514

【讨论】:

真正的老式方式是import psyco; psyco.full()。太糟糕了,它在 2.7 中不起作用... 在这种情况下,在 2.7 之前的版本中会产生什么影响? psyco 的主要负责人现在正在研究 pypy。 Pypy,顺便说一句,可能会加快速度。我做了一个微基准测试,其中 pypy 明显优于 cython。【参考方案9】:

您对以.x.y 属性访问您的坐标有多重视?令我惊讶的是,我的测试表明最大的单一时间接收器不是对list.append() 的调用,而是Point 对象的构造。构建一个元组需要四倍的时间,而且数量很多。只需在代码中将 Point(int(x), int(y)) 替换为元组 (int(x), int(y)) 即可将执行时间缩短 50% 以上(Win XP 上的 Python 2.6)。也许您当前的代码仍有优化空间?

如果你真的想使用.x.y 访问坐标,你可以尝试使用collections.namedtuple。它不如普通元组快,但 似乎 比代码中的 Pair 类快得多(我在对冲,因为单独的计时基准给了我奇怪的结果)。

Pair = namedtuple("Pair", "x y")  # instead of the Point class
...
curr_points = [ Pair(x, y) for x, y in paired_points ]

如果你需要走这条路,从元组派生一个类也是值得的(比普通元组成本最低)。如果需要,我可以提供详细信息。

PS 我看到@MattAnderson 很久以前就提到过对象元组问题。但这是一个重大影响(至少在我的盒子上),甚至在禁用垃圾收集之前。

               Original code: total time:  15.79
      tuple instead of Point: total time:  7.328
 namedtuple instead of Point: total time:  9.140

【讨论】:

【参考方案10】:

数据是一个制表符分隔的文件,由逗号列表组成 分隔的整数。

使用示例get_data() 我制作了一个像这样的.csv 文件:

1,6,2,8,2,3,5,9,6,6     10,4,10,5,7,9,6,1,9,5
6,2,2,5,2,2,1,7,7,9     7,6,7,1,3,7,6,2,10,5
8,8,9,2,6,10,10,7,8,9   4,2,10,3,4,4,1,2,2,9
...

然后我通过 JSON 滥用 C 优化解析:

def test2():
    import json
    import time
    time_start = time.time()
    with open('data.csv', 'rb') as f:
        data = f.read()
    data = '[[[' + ']],[['.join(data.splitlines()).replace('\t', '],[') + ']]]'
    all_point_sets = [Point(*xy) for row in json.loads(data) for xy in zip(*row)]
    time_end = time.time()
    print "total time: ", (time_end - time_start)

我的盒子上的结果:你原来的 test() ~8s,禁用 gc ~6s,而我的版本(包括 I/O)分别给出 ~6s 和 ~4s。即大约 50% 的加速。但是查看分析器数据很明显,最大的瓶颈在于对象实例化本身,因此 Matt Anderson 的回答将使您在 CPython 上获得最大收益。

【讨论】:

【参考方案11】:

我不知道你能做些什么。

您可以使用生成器来避免额外的内存分配。这给了我大约 5% 的加速。

first_points  = (int(p) for p in first_points .split(","))
second_points = (int(p) for p in second_points.split(","))
paired_points = itertools.izip(first_points, second_points)
curr_points   = [Point(x, y) for x,y in paired_points]

即使将整个循环折叠成一个庞大的列表理解也没有多大作用。

all_point_sets = [
    [Point(int(x), int(y)) for x, y in itertools.izip(xs.split(','), ys.split(','))]
    for xs, ys in data
]

如果你继续迭代这个大列表,那么你可以把它变成一个生成器。这将分散解析 CSV 数据的成本,因此您不会受到很大的前期打击。

all_point_sets = (
    [Point(int(x), int(y)) for x, y in itertools.izip(xs.split(','), ys.split(','))]
    for xs, ys in data
)

【讨论】:

这是一个很好的观点,但我并没有真正看到性能差异 减速似乎在get_data超过test 但是get_data 不是正在计时的。我想提高后面代码的性能。【参考方案12】:

这里有很多很好的答案。然而,目前尚未解决的这个问题的一方面是 python 中各种迭代器实现之间的列表到字符串的时间成本差异。

有一篇文章在Python.org essays:list2str 上测试不同迭代器在列表到字符串转换方面的效率。 请记住,当我遇到类似的优化问题,但具有不同的数据结构和大小时,本文中提供的结果并没有以相同的速度扩展,因此值得为您的特定用例测试不同的迭代器实现。

【讨论】:

【参考方案13】:

由于内置函数(例如 zip(a,b)map(int, string.split(",")) 用于长度为 2000000 的数组的时间可以忽略不计,我不得不假设最耗时的操作是 append

因此,解决问题的正确方法是递归连接字符串: 由 10 个元素组成的 10 个字符串变为更大的字符串 10 个 100 个元素的字符串 10 个 1000 个元素的字符串

最后到zip(map(int,huge_string_a.split(",")),map(int,huge_string_b.split(",")));

然后进行微调以找到 追加和征服方法的最佳基数。

【讨论】:

我的印象是,就像其他海报所写的那样,最大的成本是创建对象而不是附加

以上是关于在 Python 中加快字符串与对象的配对的主要内容,如果未能解决你的问题,请参考以下文章

Python3 - 字符串

盘点Python中易忽略的函数

加快匹配字符串python

如何在 Python 中的字符串中的数字前面添加“+”号?

贪心算法(10):括号的平衡配对问题

一部分正则匹配