万物之中, 希望至美.

「Python3学习笔记」读书笔记—字符串

2018.06.14

字符串字面量以成对的单引号(`)、双引号(),或可跨行的三引号(”””)语法构成,自动合并相邻的字面量。字符串支持转义、八进制、十六进制,或 Unicode 格式字符。

使用单引号还是双引号,并没有什么特殊限制。如果文本内的引用文字使用双引号,那么外面用单引号可避免转义,更易阅读。

通常情况下,建议遵循多数编程语言惯例,使用双引号标示。

Python 3 中字符串存储的是 Unicode 文本,是不可变序列类型。而 Unicode 格式大小写分别表示 16 位( \u )和 32 位(\U)整数,不能混用。

>>> "h\x69, \u6C49\U00005B57"
'hi, 汉字'

在字面量前添加标志,表示构建指定格式的字符串。

>>> type(u"abc")
<class 'str'>
>>> type(b"abc")
<class 'bytes'>

最常用的原始字符串(r, raw string),它将反斜线视作字符内容,而非转义标志。这在构建类似 Windows 路径、正则表达式匹配模式之类的文法字符串时很有用。

>>> open(r"c:\windows\readme.txt”)

>>> import re
>>> re.findall(r"\b\d+\b", "a10 100")
['100']

操作

字符串支持用加法或乘法运算符拼接字符串。

>>> s = "hello"

>>> s += ", world"
>>> "-" * 10
'----------'

编译器会尝试在编译期直接计算出字面量拼接结果,可避免运行时的内存开销。不可此类优化程度有限,并不总是有效。

>>> def test():
...     a = "x" + "y" + "z"
...     b = "a" * 10
...     return a, b
...
>>> import dis
>>> dis.dis(test)
  2           0 LOAD_CONST               7 ('xyz')	# 直接给出结果,省略加法结果
              2 STORE_FAST               0 (a)

  3           4 LOAD_CONST               8 ('aaaaaaaaaa')	# 省略乘法运算
              6 STORE_FAST               1 (b)

  4           8 LOAD_FAST                0 (a)
             10 LOAD_FAST                1 (b)
             12 BUILD_TUPLE              2
             14 RETURN_VALUE

多个动态字符串拼接,应优先考虑 join 或 format 方式,而不是使用 + 号操作符直接拼接,这是因为相比于多次加法运算和多次内存分配(字符串是不可变对象),join 这类函数(方法)可预先计算出总长度,一次性分配内存,随后直接复制内存数据填充。另一方面,将固定模版内容与变量分离的 format,更易阅读和维护,可参考以前的这篇记录Python连接字符串优先使用join而不是+ | M-in’s Blog

编写代码时除保持简单外,还应具备良好的可读性。比如:判断是否包含子字符串,in、not in 操作符就比 find 方法自然,更贴近日常阅读习惯。

>>> "py" in "python"
True
>>> "py" not in "python"
False

>>> "python".find("py")
0

作为序列类型,可使用索引序号访问字符串的单个字符或某一片段(切片)。Python 3 支持负索引,也就是反向从尾部以 -1 开始(索引 0 表示正向第一个字符)。

>>> s = "0123456789"
>>> s[2]
'2'
>>> s[-1]
'9'
>>> s[2:6]
'2345'
>>> s[2:-2]
'234567'

无论以哪种方式返回与原字符串内容不同的子字符串时,都可能会重新分配内存,并复制数据。

转换

除了与数字、Unicode 码点的转化外,最常见的是在不用编码间进行转换。Python 3 使用 bytes、bytearray 存储字节序列,不再和 str 混用。

格式化

Python 3.6 新增了 f-strings 支持,这在多数脚本语言里属于标配。

使用 f 前缀标志,Python 解释器在解析大括号内的字段或表达式时,在上下文命名空间(namespace)查找同名对象进行值替换。

>>> x = 10
>>> y = 20
>>> f"{x} + {y} = {x + y}"
'10 + 20 = 30'

# 除运算符外,还可以是函数调用。
>>> f"{type(x)}"
"<class 'int'>"

完整的 format 格式化以位置序号或字段名匹配参数进行值替换,可添加对齐、填充、精度等控制。具体请参考官方文档6.1.3. Format String Syntax

池化

因为无处不在的名字就是字符串实例,导致字符串可能是进程里实例数量最多的类型之一。

鉴于相同的名字会出现在不同的命名空间里,那么有必要共享实例。内容相同,且不可变,共享内存不会导致任何问题。关键是节约内存,且可省去创建新实例的开销。

对此,Python 的做法是实现一个字符串池( intern )。

池负责管理实例,使用者只需饮用即可。另一潜在的好处是,从池返回的字符串,只需比较指针就可知道内容是否相同,无需额外计算。可以使用池来提升哈希表等类似结构的查找性能。

>>> import sys
>>> "__name__" is sys.intern("__name__")
True

除以常量方式出现的名字和字面量外,动态生成的字符串一样可加入池中。如此可保证每次都引用同一对象,不会有额外的创建和分配操作。

>>> a = "hello, world!"
>>> b = "hello, world!"
>>> a is b											# 不同实例
False
>>> sys.intern(a) is sys.intern("hello, world!")		# 相同实例
True

当然,一旦失去所有外部引用,池内的字符串对象一样会被回收。

>>> a = sys.intern("hello, world!")
>>> id(a)
4318361584
>>> id(sys.intern("hello, world!"))		# 有外部引用
4318361584

>>> del a									# 删除外部引用后被回收
>>> id(sys.intern("hello, world!"))		# 从 id 值不同可以看到字符串是新建后入池的
4318389808

字符串池的实现算法很简单,就是简单的字典结构。
详细可参考 Objects/unicodeobject.c : PyUnicode_InternInPlace。
做大数据处理时,可能须创建海量主键,使用类似机制有助于减少对象数量,节约大量内存。当然,可以选择更高效的数据结构,而不一定是系统内置的字符串池。

#读书笔记

comments powered by Disqus