Python的不可变字符串及其切片

7
Python中的字符串是不可变的,并支持缓冲区接口。当使用切片或.split()方法时,返回指向旧字符串部分的缓冲区可能更有效率。然而,每次都构建了一个新的字符串对象。为什么?我所看到的唯一原因是它可能使垃圾回收更加困难。
在常规情况下,内存开销是线性的并且不太明显。复制和分配都很快。但是在Python中已经做了太多事情,也许这样的缓冲区值得一试?
编辑:
这种方式形成子字符串似乎会使内存管理变得更加复杂。当仅使用任意字符串的20%且无法取消分配其余字符串时,这是一个简单的例子。我们可以改进内存分配器,使其能够部分取消分配字符串,但这可能大多数情况下是不利的。如果内存变得紧要,所有标准函数仍然可以用buffer或memoryview模拟。代码可能不那么简洁,但为了得到某些东西,必须放弃一些东西。

1
你如何返回字符串的部分?你是指它的指针吗?如果原始字符串被删除,子字符串会发生什么? - Ashwini Chaudhary
如果原始对象被删除,缓冲区会发生什么变化?我认为垃圾回收机制足够智能,不会删除原始对象。 - gukoff
我相信你的问题是重复的,和这个问题一样: 如果 .NET 中的字符串是不可变的,那么为什么 Substring 需要 O(n) 的时间? 同样适用于 Python。 - Bakuriu
3个回答

3

是的,这在迭代可变对象时非常有用,例如。但是针对字符串,在这里使用并不会破坏一致性。 - gukoff
@Harold:没错,但也许这并不值得花费精力。我已经编辑了我的回答。 - Tim Pietzcker
看起来Eric试图进行严格的优化,即使是对于字符串拼接。因此,在C#中,StringBuilder类确实消除了这种复杂优化的需求。 - gukoff

3
底层字符串表示是以空字符结尾的,即使它跟踪长度,因此您不能有一个字符串对象引用不是后缀的子字符串。这已经限制了您的建议的实用性,因为它会增加许多处理后缀和非后缀的复杂性(放弃使用空字符结尾的字符串会带来其他后果)。
允许引用字符串的子字符串意味着要大大复杂化垃圾回收和字符串处理。对于每个字符串,您都必须跟踪有多少对象引用每个字符或每个索引范围。这意味着大大复杂化了字符串对象的struct和任何与它们相关的操作,从而导致可能很大的速度下降。
另外,从Python3开始,字符串具有3种不同的内部表示形式,这些事情将变得太混乱,无法维护,您的建议可能没有足够的好处被接受。
这种“优化”的另一个问题是当您想要释放“大字符串”时:
a = "Some string" * 10 ** 7
b = a[10000]
del a

在这些操作之后,您会得到一个子字符串 b,它防止了 a(一个巨大的字符串)被释放。当然,您可以复制小字符串,但如果 b = a[:10000](或其他很大的数字),那该怎么办?10000个字符看起来像是需要使用优化来避免复制的大字符串,但它阻止了释放兆字节的数据。
垃圾收集器必须不断检查是否值得释放大字符串对象并进行复制,而且所有这些操作都必须尽可能快,否则就会降低时间性能。
99% 的情况下,程序中使用的字符串都很“小”(最多 10k 个字符),因此复制非常快,而您提出的优化仅在处理真正大的字符串时才开始变得有效(例如从巨大文本中取大小为 100k 的子字符串),对于真正小的字符串而言则要慢得多,而这是常见的情况,即应该进行优化的情况。
如果您认为这很重要,那么可以自由地提出 PEP,展示实现和您的建议所带来的速度/内存使用方面的变化。如果它确实值得一试,它可能会被包含在未来版本的 Python 中。

1
我不认为OP的提议是个好主意,但出于不同的原因(“不要射杀信使”)。第一段很有趣,确实在将其简单地附加到CPython上存在问题,但这并不是一个根本性的问题,并且字符串表示发生巨大变化也不是第一次了。第二段假设了一个愚蠢的实现。你只需要让子字符串引用正确的字符串对象,并存储起始位置和长度/结束位置(这种表示对字符串对象的内部细节完全不知情,所以第三段逻辑上消失了)。 - user395760
关于以空字符结尾的表示风格的说法相当有力。但有趣的是:这种风格真的有用吗?对于像strcpy这样的函数,也许可以,但它们可以被类似strncpy的函数替换... - gukoff
@Harold,我认为这不是针对strcpy的,因为长度是可用的,只需使用memcpy更简单、更快-同样适用于其他标准函数。它可能是为了避免在将其转换为C字符串以供第三方/客户端代码使用时进行复制(请参见PyBytes_AsString等)。 - user395760

2

如果您担心内存(在处理非常大的字符串时),可以使用 buffer()

>>> a = "12345"
>>> b = buffer(a, 2, 2)
>>> b
<read-only buffer for 0xb734d120, size 2, offset 2 at 0xb734d4a0>
>>> print b
34
>>> print b[:]
34

了解这一点可以让你有替代字符串方法(如split())的选择。

如果你想要split()一个字符串,但又想保留原始字符串对象(因为你可能需要它),你可以这样做:

def split_buf(s, needle):
    start = None
    add = len(needle)
    res = []
    while True:
        index = s.find(needle, start)
        if index < 0:
            break
        res.append(buffer(s, start, index-start))
        start = index + add
    return res

或者,使用.index()
def split_buf(s, needle):
    start = None
    add = len(needle)
    res = []
    try:
        while True:
            index = s.index(needle, start)
            res.append(buffer(s, start, index-start))
            start = index + add
    except ValueError:
        pass
    return res

1
是的,我知道缓冲区。但如果我想在任意字符串上使用split()方法呢? - gukoff
@Harold 你可以“模拟”它以满足你的需求,请查看我的编辑。另一方面,如果你分割一个字符串并且不再需要它,你可以丢弃原始字符串,释放内存,并且具有与之前几乎相同的内存占用。 - glglgl

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接