全国报名热线

021-6769 0939

首页>Python>正文

Python学习:深入理解python字符串

时间:2017-12-29 11:48:59   来源:雨痕   阅读:

字符串

字符串 (str) 存储 Unicode 文本,是不可变序列类型。相比 Python 2 里的混乱,Python 3 总算顺应时代发展,将文本和二进制彻底分离。

Unicode 设计意图是为了解决跨语言和跨平台转换和处理需求,用统一编码方案容纳不同国家地区的文字,以解决传统编码方案的不兼容问题,故又称作统一码、万国码等等。
Unicode 为每个字符分配一个称作码点(code point)的整数序号,此对应编码方案叫做通用字符 集(Universal Character Set, UCS)。依据编码整数长度,可分做 UCS-2 和 UCS-4 两种,后者可 容纳更多字符。UCS 只规定了字符和码点的对应关系,并不涉及如何显示和存储。 
UTF(Unicode Transformation Format) 的作用是将码点整数转换为计算机可存储的字节格式。 发展至今,有 UTF-8、UTF-16、UTF-32 等多种方案。其中 UTF-8 采用变长格式,因与 ASCII 兼 容,是当下使用最广泛的一种。对于英文为主的内容,UTF-8 可获得最好的存储效率。而使用两 字节等长方案的 UTF-16,有更快的处理效率,常被用作执行编码。 
UTF 还可在文本头部插入称作 BOM(byte order mark)的标志来标明字节序信息,以区分大小 端(BE、LE)。如此,又可细分为 UTF-16LE、UTF-32BE 等。

>>> s = "汉字"
>>> len(s)
2

 

>>> hex(ord("汉")) # code point
0x6c49

 

>>> chr(0x6c49)

 

>>> ascii("汉字") # 对 non-ASCII 进行转义。
\u6c49\u5b57

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

用单引号还是双引号,并没有什么特殊限制。如果文本内引用文字使用双引号,那么外面用单引号可避免转义,更易阅读。通常情况下,建议遵循多数编程语言惯例,使用双引号标示。除去单引号在英文句法里的特殊用途外,它还常用来表示单个字符。

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

注意:Unicode 格式大小写分别表示 16 位和 32 位整数,不能混用。

>>> "It's my life" # 英文缩写。
>>> 'The report contained the "facts" of the case.' # 包含引文,避免使用 \" 转义。

 

>>> "hello" ", " "world"  # 合并多个相邻字量。
hello, world

 

>>> """                             # 换行符、前导空格、空行都是组成内容。
   The Zen of Python, by Tim Peters    
   
   Beautiful is better than ugly.    
   Explicit is better than implicit.    
   Simple is better than complex.
"""

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

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

>>> open(r"c:\windows\readme.txt") # Windows 路径。

>>> re.findall(r"\b\d+\b", "a10 100") # 正则表达式。
['100']

 

>>> type(u"abc") # 默认 str 就是 unicode, 无需添加 u 前缀。
str
>>> type(b"abc") # 构建字节数组。
bytes

 

操作

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

>>> s = "hello"
>>> s += ", world"

>>> "-" * 10
----------

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

>>> def test():
       a = "x" + "y" + "z"
       b = "a" * 10
       return a, b
       
>>> dis.dis(test)
  2           0 LOAD_CONST           7 ('xyz')  # 直接给出结果,省略加法运算。
  3           4 LOAD_CONST           8 ('aaaaaaaaaa')  # 省略乘法运算。

至于多个动态字符串拼接,应优先选择 join 或 format 方式。

相比多次加法运算和多次内存分配 (字符串是不可变对象),join 这类函数 (方法) 可预先计算出总长度,一次性分配内存,随后直接拷贝内存数据填充。另一方面,将固定内容与变量分离的模版化 format,更易阅读和维护。

>>> username = "qyuhen"
>>> datetime = "2017010"

>>> "/data/" + username + "/message/" + datetime + ".txt"
/data/qyuhen/message/20170101.txt

>>> "/data/{user}/message/{time}.txt".format(user = username, time = datetime)
/data/qyuhen/message/20170101.txt

我们用 line_profiler 对比用加法和 join 拼接 26 个大写字母的性能差异。虽然该测试不具备代表性,但可以提供一个粗略的验证方法。

#!/usr/bin/env python3

import string
x = list(string.ascii_uppercase)

@profile
def test_add():
   s = ""
   for c in x:
       s += c
   return s


@profile
def test_join():
   return "".join(x)


test_add()
test_join()

输出:

$ kernprof -l ./test.py && python -m line_profiler test.py.lprof

 

 

有关 line_profiler 使用方法请参阅第十二章。

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

>>> "py" in "python"
True

>>> "Py" not in "python"
True

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

>>> s = "0123456789"

>>> s[2]
2

>>> s[-1]
9

>>> s[2:6]
2345

>>> s[2:-2]
234567

使用两个索引号表示一个序列片段的语法称作切片(slice),可以此返回字符串子串。但无论以哪种方式返回与原字符串内容不同的子串时,都会重新分配内存,并复制数据。不像某些语言那样,仍旧以指针引用原字符串内容缓冲区。

先看相同或不同内容时,字符串对象构建情形。

>>> s = "-" * 1024
>>> s1 = s[10:100]  # 片段,内容不同。
>>> s2 = s[:]  # 内容相同
>>> s3 = s.split(",")[0]  # 内容相同。

>>> s1 is s  # 内容不同,构建新对象。
False

>>> s2 is s  # 内容相同时,直接引用原字符串对象。
True

>>> s3 is s
True

再进一步用 memory_profiler 观察内存分配情况。

@profile
def test():
   a = x[10:-10]
   b = x.split(",")
   return a, b

x = "0," * (1 << 20)
test()

输出


$ python -m memory_profiler ./test.py

 




 

 

此类行为,与具体的 Python 实现版本有关,不能一概而论。

 

字符串类型内置丰富的处理方法,可满足大多数操作需要。对于更复杂的文本处理,还可使用正则表达式(re)或专业的第三方库,比如 NLTK、TextBlob 等。

 

转换

除去与数字、Unicode 码点的转换外,最常见的是在不同编码间进行转换。

Python 3 使用 bytes、bytearray 存储字节数组,不再和 str 混用。

>>> s = "汉字"

>>> b = s.encode("utf-16") # to bytes
>>> b.decode("utf-16") # to unicode string
汉字

如要处理 BOM 信息,可导入 codecs 模块。

>>> s = "汉字"

>>> s.encode("utf-16").hex()
fffe496c575b

>>> codecs.BOM_UTF16_LE.hex() # BOM 标志。
fffe

 

>>> codecs.encode(s, "utf-16be").hex() # 按指定 BOM 转换。
6c495b57

>>> codecs.encode(s, "utf-16le").hex()
496c575b

还有,Python 3 默认编码不再是 ASCII,所以无需额外设置。

 

Python 3.6

>>> sys.getdefaultencoding()
utf-8

 

Python 2.7

>>> import sys
>>> reload(sys)
>>> sys.setdefaultencoding("utf-8")

>>> b = s.encode("utf-16")
>>> b.decode("utf-16")
u'\u6c49\u5b57'

>>> type(b)
<type 'str'>

 

格式化

长期发展下来,Python 累积了多种字符串格式化方式。相比古老的面孔,人们更喜欢或倾向于使用新的特征。

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

使用 f 前缀标志,解释器解析大括号内的字段或表达式,从上下文名字空间查找同名对象进行值替换。格式化控制依旧遵循 format 规范,但阅读体验上更加完整和简洁。

>>> x = 10
>>> y = 20

>>> f"{x} + {y} = {x + y}"                  # f-strings
10 + 20 = 30

 

>>> "{} + {} = {}".format(x, y , x + y)
10 + 20 = 30

表达式除运算符外,还可以是函数调用。

>>> f"{type(x)}"
<class 'int'>

完整 format 格式化以位置序号、字段名匹配替换值参数,允许对其施加包括对齐、填充、 精度等控制。从某种角度看,f-strings 有点像是 format 的增强语法糖。

 



 

 

将两者进行对比,f-strings 类模版方式更加灵活,一定程度上将输出样式与数据来源分离。 但其缺点是与上下文名字耦合,导致模版内容与代码必须保持同步修改。而 format 的序号与主键匹配方式可避开这点,只可惜它不支持表达式。

另外,对于简短的格式化处理,format 拥有更好的性能。

 

手工序号和自动序号

>>> "{0} {1} {0}".format("a", 10)
a 10 a

>>> "{} {}".format(1, 2)  # 自动序号,不能与手工序号混用。
1 2

 

主键

>>> "{x} {y}".format(x = 100, y = [1,2,3])
100 [1, 2, 3]

 

属性和索引

>>> x.name = "jack"

>>> "{0.name}".format(x)  # 对象属性。
jack

>>> "{0[2]}".format([1,2,3,4]) # 索引。
3

 

宽度、补位

>>> "{0:#08b}".format(5)
0b000101

 

数字

>>> "{:06.2f}".format(1.234)  # 保留 2 位小数。
001.23

>>> "{:,}".format(123456789)  # 千分位。
123,456,789

 

对齐

>>> "[{:^10}]".format("abc") # 居中
[ abc ]

 

>>> "[{:.<10}]".format("abc") # 左对齐,以点填充。
[abc.......]

古老的 printf 百分号格式化方式已被官方标记为 “obsolete”,加上其自身固有的一些问题,可能会被后续版本抛弃,不建议使用。另外,标准库里 string.Template 功能弱,且性能也差,同样不建议使用。

 

池化

字符串算是进程里实例数量较多的类型之一,因为无处不在的名字就是字符串实例。

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

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

>>> "__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)
4401879024

>>> id(sys.intern("hello, world!"))  # 有外部引用。
4401879024

>>> del a  # 删除外部引用后被回收。

>>> id(sys.intern("hello, world!")) # 从 id 值不同可以看到新建,入池。
4405219056

字符串池实现算法很简单,就是简单的字典结构。 详情参考 Objects/unicodeobject.c : PyUnicode_InternInPlace。  
做大数据处理时,可能需创建海量主键,使用 intern 有助于减少对象数量,节约大量内存。
分享:0