【问题标题】:Replacing the empty strings in a string替换字符串中的空字符串
【发布时间】:2013-01-04 02:23:56
【问题描述】:

无意中发现,在python中,表单的一个操作

string1.join(string2)

可以等价表示为

string2.replace('', string1)[len(string1):-len(string1)]

此外,在尝试timeit 与几个不同大小的输入之后,这种奇怪的加入方式似乎快了两倍多。

  1. 为什么join方法要慢一些?
  2. 这样替换空字符串是否安全/明确?

【问题讨论】:

  • 将字符串传递给join 似乎是瓶颈。将string2 转换为list 将时间从697 减少到148 ns。
  • 您跳过x.join(y) 的一个非常常见的用法这一事实不算数?例如,' '.join(['1', '2', '3']) 呢?
  • 这有一些问题,首先是可读性。这对人们阅读来说是可怕的。其次,您的方法的性能可能会因使用的 Python 实现而有很大差异(CPython 并不是唯一的),并且正如 mmgp 指出的那样,使用带有字符串作为第一个参数的 join() 实际上是一种非常罕见的操作。

标签: python string performance


【解决方案1】:

首先,让我们分解一下为什么会这样。

>>> string1 = "foo"
>>> string2 = "bar"
>>> string1.join(string2)
'bfooafoor'

这是在string2的每个项目(字符)之间放置string1的操作。

所以替换空字符串会做一些有趣的事情,它将空字符之间的间隙计算为空字符串,因此基本上完成了相同的任务,除了在开始和结束处有一个额外的分隔符:

>>> string2.replace('', string1)
'foobfooafoorfoo'

因此,切掉这些会产生与str.join() 相同的结果:

>>> string2.replace('', string1)[len(string1):-len(string1)]
'bfooafoor'

显然,这个解决方案比str.join() 可读性差很多,所以我总是建议不要这样做。 str.join() 也被开发为在所有平台上都高效。在某些版本的 Python 上替换空字符串的效率可能要低得多(我不知道是否是这种情况,但这是一种可能性——就像在 CPython 中重复连接相当快,但在其他地方不一定是这种情况。)

我什至在文档中找不到任何内容表明替换空字符串的行为应该以这种方式起作用,the docs for str.replace() 简单地说:

返回字符串的副本,其中所有出现的子字符串 old 都替换为 new。如果给出了可选参数 count,则仅替换第一个 count 出现。

我认为我们没有理由假设字母之间的间隙应该算作空字符串的出现(可以说,您可以在字符串中的任何位置放置无限的空字符串),因此,依赖这种行为可能是个坏主意。

这种操作也非常少见 - 将一系列字符串连接在一起更为常见 - 连接字符串的单个字符并不是我个人经常需要做的事情。

有趣的是,这个x.replace("", y) 似乎是the Python source 的特殊情况:

2347 /* Algorithms for different cases of string replacement */
2348
2349 /* len(self)>=1, from="", len(to)>=1, maxcount>=1 */
2350 Py_LOCAL(PyStringObject *)
2351 replace_interleave(PyStringObject *self,
2352 const char *to_s, Py_ssize_t to_len,
2353 Py_ssize_t maxcount)
2354 {
...

很可能是这种特殊的外壳使它表现良好。同样,由于文档中没有提到,这是一个实现细节,假设它在其他 Python 版本中也能快速(或完全)工作是错误的。

【讨论】:

【解决方案2】:

正如 Lattyware 所提到的,对于空字符串替换,它是一种特殊情况,replace_interleave,它是一个直接循环,其中来自源和来自字符串的交替字符被复制到结果字符串。 Loop 被编码为尽可能快

count = self_len+1;

count -= 1;
Py_MEMCPY(result_s, to_s, to_len);
result_s += to_len;
for (i=0; i<count; i++) {
    *result_s++ = *self_s++;
    Py_MEMCPY(result_s, to_s, to_len);
    result_s += to_len;
}

/* Copy the rest of the original string */
Py_MEMCPY(result_s, self_s, self_len-i);

Join方法也有一个Loop,但是有一些改进的地方(通过我没有找到所有方面的原因都已经按照下面的方式编码了)和瓶颈的原因。

char *sep = PyString_AS_STRING(self);
seq = PySequence_Fast(orig, "");
/* Catenate everything. */
p = PyString_AS_STRING(res);
for (i = 0; i < seqlen; ++i) {
    size_t n;
    item = PySequence_Fast_GET_ITEM(seq, i);
    n = PyString_GET_SIZE(item);
    Py_MEMCPY(p, PyString_AS_STRING(item), n);
    p += n;
    if (i < seqlen - 1) {
        Py_MEMCPY(p, sep, seplen);
        p += seplen;
    }
}

正如您在此处看到的,在循环内部

  • 字符串的每一项都被索引
  • 项目的大小已确定
  • 索引项转换为字符串

上述三个操作,即使它可能是内联的,也会有相当大的开销。 注意这也解释了为什么使用 List 与使用 STing 相比,使用一个不同的结果,正如 Blended 所观察到的那样

同时比较两个循环,

以前的

最后说明

str.join 的编写牢记所有形式的可迭代和序列,而不仅仅是字符串,并且没有详细说明,它完全可以预期通用例程的执行速度可能不如专用例程来服务特定形式的数据。

【讨论】:

  • 这是一个很好的见解,但需要明确的是,这是对 CPython 的纯粹分析,而不是一般的 Python,因此严重依赖它并不是一个好主意。
  • @Lattyware:验证所有实现是一项艰巨的任务,但我认为我的 **Final Note ** 可以是一个通才的结论
  • 这是对性能问题的总结,但这里也有其他问题。这是有用的信息,但我建议不要排他性地查看它。
  • @Lattyware:当然,我并不否认这一事实,您的回答已正确解决。这个答案是对你的补充,而不是补充。
猜你喜欢
  • 1970-01-01
  • 2014-11-12
  • 1970-01-01
  • 1970-01-01
  • 2012-08-09
  • 2013-05-09
  • 2018-12-01
  • 1970-01-01
  • 2016-03-01
相关资源
最近更新 更多