python 如何存储、连接和切片字符串?
How does python store, join and slice strings?
我是一名 PHP 程序员,很快就会转到 Python。
在PHP 世界中,有两个函数用于处理字符串切片mb_substr
和substr
。多字节变体 mb_substr
是出了名的慢,因为 PHP 不知道每个字符有多少字节;所以它需要遍历每个字符并检查它们的长度以找到给定偏移量的字节位置。
我写了下面的基准测试,看看 Python (3.8) 是否有同样的问题:
from random import randrange
from time import time
alphabet = ["a", "ü", "字", ""]
alphabet_length = len(alphabet)
string_length = 10_000_000
time_g0 = time()
rand_char_list = list(map(lambda _: alphabet[randrange(0, alphabet_length)], range(string_length)))
time_g = time() - time_g0
print("Time to generate chars:", time_g)
time_j0 = time()
rand_string = ''.join(rand_char_list)
time_j = time() - time_j0
print("Time to join chars:", time_j)
offset_i = 0
offset_m = int(string_length/2)
offset_f = string_length - 1
time_i0 = time()
utf8_char = rand_string[offset_i]
time_i = time() - time_i0
print("Time to slice initial char:", time_i)
time_m0 = time()
utf8_char = rand_string[offset_m]
time_m = time() - time_m0
print("Time to slice middle char:", time_m)
time_f0 = time()
utf8_char = rand_string[offset_f]
time_f = time() - time_f0
print("Time to slice final char:", time_f)
输出如下:
Time to generate chars: 5.610808849334717
Time to join chars: 0.21134543418884277
Time to slice initial char: 9.5367431640625e-07
Time to slice middle char: 4.76837158203125e-07
Time to slice final char: 4.76837158203125e-07
我已经 运行 多次测试,结果相当一致。
我很惊讶 join 操作花了这么长时间,而 slice 操作却非常快。更令人惊讶的是,在 9/10 运行 秒内,对初始 char 进行切片比其他两个操作稍微慢一些。
怎么会这样?
python是否将各种字符的字节索引存储在一个utf8字符串中?它使用固定的(4 字节)字符吗?
Python实际上使用了灵活的内部表示,并选择了最适合随机访问的格式。例如,如果所有字符都是 UTF-8 中的单个字节,则该字符串将是一个 1 字节字符的数组。如果其中一个是 2 字节字符,则所有内容都是 2 字节,依此类推。详细信息描述如下:
https://www.python.org/dev/peps/pep-0393/
这意味着对于所有字符大小,随机访问字符串切片的速度应该大致相同。可能会出现差异,因为某些优化不适用于宽字符,或者因为内部格式之间的转换可能会延迟发生。例如,所有单个 ASCII 字符串(就像小整数值一样)不是动态分配的,而是从此类字符串的预分配池中获取的。这显然不适用于所有 4 字节字符的集合。
就字符串连接而言,它有时表现不佳是一个偶尔讨论的话题。参见此处的示例:
https://lwn.net/Articles/816415/
我是一名 PHP 程序员,很快就会转到 Python。
在PHP 世界中,有两个函数用于处理字符串切片mb_substr
和substr
。多字节变体 mb_substr
是出了名的慢,因为 PHP 不知道每个字符有多少字节;所以它需要遍历每个字符并检查它们的长度以找到给定偏移量的字节位置。
我写了下面的基准测试,看看 Python (3.8) 是否有同样的问题:
from random import randrange
from time import time
alphabet = ["a", "ü", "字", ""]
alphabet_length = len(alphabet)
string_length = 10_000_000
time_g0 = time()
rand_char_list = list(map(lambda _: alphabet[randrange(0, alphabet_length)], range(string_length)))
time_g = time() - time_g0
print("Time to generate chars:", time_g)
time_j0 = time()
rand_string = ''.join(rand_char_list)
time_j = time() - time_j0
print("Time to join chars:", time_j)
offset_i = 0
offset_m = int(string_length/2)
offset_f = string_length - 1
time_i0 = time()
utf8_char = rand_string[offset_i]
time_i = time() - time_i0
print("Time to slice initial char:", time_i)
time_m0 = time()
utf8_char = rand_string[offset_m]
time_m = time() - time_m0
print("Time to slice middle char:", time_m)
time_f0 = time()
utf8_char = rand_string[offset_f]
time_f = time() - time_f0
print("Time to slice final char:", time_f)
输出如下:
Time to generate chars: 5.610808849334717
Time to join chars: 0.21134543418884277
Time to slice initial char: 9.5367431640625e-07
Time to slice middle char: 4.76837158203125e-07
Time to slice final char: 4.76837158203125e-07
我已经 运行 多次测试,结果相当一致。
我很惊讶 join 操作花了这么长时间,而 slice 操作却非常快。更令人惊讶的是,在 9/10 运行 秒内,对初始 char 进行切片比其他两个操作稍微慢一些。
怎么会这样?
python是否将各种字符的字节索引存储在一个utf8字符串中?它使用固定的(4 字节)字符吗?
Python实际上使用了灵活的内部表示,并选择了最适合随机访问的格式。例如,如果所有字符都是 UTF-8 中的单个字节,则该字符串将是一个 1 字节字符的数组。如果其中一个是 2 字节字符,则所有内容都是 2 字节,依此类推。详细信息描述如下: https://www.python.org/dev/peps/pep-0393/
这意味着对于所有字符大小,随机访问字符串切片的速度应该大致相同。可能会出现差异,因为某些优化不适用于宽字符,或者因为内部格式之间的转换可能会延迟发生。例如,所有单个 ASCII 字符串(就像小整数值一样)不是动态分配的,而是从此类字符串的预分配池中获取的。这显然不适用于所有 4 字节字符的集合。
就字符串连接而言,它有时表现不佳是一个偶尔讨论的话题。参见此处的示例: https://lwn.net/Articles/816415/