在 Python 中加快字符串与对象的配对
Posted
技术标签:
【中文标题】在 Python 中加快字符串与对象的配对【英文标题】:Speeding up pairing of strings into objects in Python 【发布时间】:2012-10-07 06:05:40 【问题描述】:我正在尝试找到一种有效的方法来将包含整数点的数据行配对,并将它们存储为 Python 对象。数据由X
和Y
坐标点组成,以逗号分隔的字符串表示。这些点必须配对,如(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_points
、second_points
和paired_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_2
在 0.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% 的减速(即时间加倍)。在这里,如果我访问每个Point
的a
和b
三次,我会失去从~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 中加快字符串与对象的配对的主要内容,如果未能解决你的问题,请参考以下文章