Python大神带你深入理解 Python 字符串

[复制链接]
sosoyoyo 发表于 2017-12-31 09:40:20 | 显示全部楼层 |阅读模式 打印 上一主题 下一主题
字符串# g9 u- F* _' d
字符串 (str) 存储 Unicode 文本,是不可变序列类型。相比 Python 2 里的混乱,Python 3 总算顺应时代发展,将文本和二进制彻底分离。9 Y. [) ~3 }. Z! v' P0 t. j+ K0 F+ o
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 = "汉字"% o; i7 x# M5 P& X, H6 M, T( C
>>> len(s)
& T$ t3 U1 f( T! G( N2) b% J; c+ U; J
>>> hex(ord("汉")) # code point
+ |, e' f, C6 d0x6c49
! H1 z' a9 I: r. o  A( u>>> chr(0x6c49)& r: `6 [! U0 j5 [; q' H
; u1 Z5 s/ P7 F7 D
>>> ascii("汉字") # 对 non-ASCII 进行转义。
" G5 L+ l+ X+ s\u6c49\u5b57
3 U: ]- M  w8 m# c2 r字符串字面量(literal)以成对单引号、双引号,或跨行三引号语法构成,自动合并相邻字面量。支持转义、八进制、十六进制,或 Unicode 格式字符。
2 V& K7 \; e8 c( u8 A) K: G/ }用单引号还是双引号,并没有什么特殊限制。如果文本内引用文字使用双引号,那么外面用单引号可避免转义,更易阅读。通常情况下,建议遵循多数编程语言惯例,使用双引号标示。除去单引号在英文句法里的特殊用途外,它还常用来表示单个字符。
; ]: E; _) C1 b: Q) K>>> "h\x69, \u6C49\U00005B57"
' n) [+ F3 U3 `6 whi, 汉字
6 k6 ]8 m' a0 S8 q7 p* I
注意:Unicode 格式大小写分别表示 16 位和 32 位整数,不能混用。
>>> "It's my life" # 英文缩写。
2 N" T1 ?7 h! i>>> 'The report contained the "facts" of the case.' # 包含引文,避免使用 \" 转义。& z8 ~( H9 T  L7 m
>>> "hello" ", " "world" # 合并多个相邻字量。
. P# W" r' L+ L0 q1 bhello, world+ b; F3 \+ ^' X4 V& N
>>> """ # 换行符、前导空格、空行都是组成内容。
6 p4 u7 m! T' u8 g. |7 v The Zen of Python, by Tim Peters % E. U6 Z% i: s+ I2 _
Beautiful is better than ugly.
8 L, O& M5 ]0 M3 F6 T Explicit is better than implicit.
1 x8 c& {/ n! o- y Simple is better than complex.2 w$ v5 i2 E9 P" @$ E3 P0 C. X
"""
& [7 A6 i, {5 r$ ]2 _" d可在字面量前添加标志,指示构建特定格式字符串。
: F* M$ ~, U+ e- w. Y/ j7 J, C最常用的原始字符串(r, raw string),它将反斜线视作字符内容,而非转义标志。这在构建类似 Windows 路径、正则表达式匹配模式 (pattern) 之类的文法字符串时很有用。/ T8 q$ R. G/ L, m/ [7 Q8 s6 S
>>> open(r"c:\windows\readme.txt") # Windows 路径。
" F' c1 N' o: h! p& X0 T8 _>>> re.findall(r"\b\d+\b", "a10 100") # 正则表达式。' c) r$ i% g3 b! K6 H
['100']% H& i  \5 e3 Y
>>> type(u"abc") # 默认 str 就是 unicode, 无需添加 u 前缀。
" |( w2 A3 m* _: Lstr
5 O1 c- `; w5 x>>> type(b"abc") # 构建字节数组。2 A& g' k3 o: p& Y3 ?
bytes
5 [% v$ q6 U1 l$ g6 p. O1 V0 e  ?操作: R( c( s1 j2 Q3 r2 x' e8 l
支持用加法和乘法运算符拼接字符串。
' y9 e  N6 W( }2 A+ }7 n( u>>> s = "hello"9 T( |8 x  Y6 s1 _5 ?2 p( m) d* a6 f/ P; T
>>> s += ", world"5 e, d7 ^% @5 |3 e& S8 X
>>> "-" * 10
  u+ J7 o' U. Z9 @" F) Z----------
  {, Q) `8 u- c& n/ }9 j, e编译器会尝试在编译期直接计算出字面量拼接结果,避免运行时开销。不过此类优化程度有限,并不总是有效。
0 m) B. d9 z( x2 K# x( U2 u>>> def test():
5 _9 \8 r& y7 K/ v8 F) B5 |. Y a = "x" + "y" + "z"
5 ?$ {2 {- i* U9 Z8 D9 ? b = "a" * 10
/ n; U& J$ ^- S2 z2 k, J return a, b4 l$ O- F, Q0 Y% ^# k' w
>>> dis.dis(test)' P9 Z6 E- v- S/ ]
2 0 LOAD_CONST 7 ('xyz') # 直接给出结果,省略加法运算。
2 O: m# U. U! K$ l6 b# L6 k+ ] 3 4 LOAD_CONST 8 ('aaaaaaaaaa') # 省略乘法运算。
. f% Y: y5 h3 B3 B. [至于多个动态字符串拼接,应优先选择 join 或 format 方式。& h7 k# @6 `  A
相比多次加法运算和多次内存分配 (字符串是不可变对象),join 这类函数 (方法) 可预先计算出总长度,一次性分配内存,随后直接拷贝内存数据填充。另一方面,将固定内容与变量分离的模版化 format,更易阅读和维护。8 W* O- W" I  {) ^9 O4 ?! u
>>> username = "qyuhen"
$ ?- v3 M4 u% q* _# R>>> datetime = "2017010"
: k8 @* [- D* A9 t) B! u>>> "/data/" + username + "/message/" + datetime + ".txt": u' {, ?3 l& W- v7 X5 T' `
/data/qyuhen/message/20170101.txt4 \% K+ V% X6 J) [
>>> "/data/{user}/message/{time}.txt".format(user = username, time = datetime)
2 v, a9 V- v5 \3 E1 `3 d% h- y/data/qyuhen/message/20170101.txt
" y- I5 w6 H  s3 ]0 R9 P+ ^* a我们用 line_profiler 对比用加法和 join 拼接 26 个大写字母的性能差异。虽然该测试不具备代表性,但可以提供一个粗略的验证方法。/ f' q. `, W2 ]: j9 s4 U7 O
#!/usr/bin/env python3* G" C7 C0 G+ A' J7 D
import string
5 D" q0 Q- A4 o3 k& H5 px = list(string.ascii_uppercase)
/ f- g. F; b1 e/ {) t@profile* k# K. z! ?& `& \! w- \
def test_add():
4 w/ |  i9 w0 W1 r9 c s = ""9 f, Y  }% M. u6 S( w5 t
for c in x:, Z9 t8 f9 q5 t3 X- C9 E( i( @! B
s += c
% j1 @2 M2 s7 ~ return s- }% H& Z% ^5 F+ G
@profile9 R" n0 ^+ p+ Q4 j5 s. J
def test_join():! v1 E4 i. a: b1 b5 T; k! m
return "".join(x)
, l% U& i5 t( v2 m1 I( ^test_add()1 x8 `" O8 j) u0 M& j- l
test_join()- U6 S- O3 u: H, {) r6 D  Q1 M: i
输出:
; o2 U8 R6 o0 A' }' H% ~' Z$ kernprof -l ./test.py && python -m line_profiler test.py.lprof
- y) J2 @3 a" G5 [% A
编写代码除保持简单外,还应具备良好的可阅读性。比如判断是否包含子串,in、not in 操作符就比 find 方法自然,更贴近日常阅读习惯。1 I2 J7 [6 I0 z! s6 ?
>>> "py" in "python"% j: u, j  O& q5 a! @
True9 T- O, X' L0 i
>>> "Py" not in "python"+ d8 {1 l' K  s; F' Y
True6 W+ ^% }8 O1 h* f! ~7 `3 c
作为序列类型,可以使用索引序号访问字符串内容,单个字符或者某一个片段。支持负索引,也就是从尾部以 -1 开始(索引 0 表示正向第一个字符)。
/ ?$ w3 s- v3 U+ U/ \! U>>> s = "0123456789"! F; k4 A% r: {: X% w
>>> s[2]
# I: T2 m- g+ b$ P2( E# X" Z3 S! q( |
>>> s[-1]
' u& t6 R$ D% P3 `9
) h2 C" R/ A# i# i, e* M# J7 o>>> s[2:6]) m  E: C. ~0 G, y
2345
; w, r' A* w5 D+ q1 }5 _>>> s[2:-2]' G3 m* \: t- w
234567# N: H* Y. v6 [, ^1 X* G! q
使用两个索引号表示一个序列片段的语法称作切片(slice),可以此返回字符串子串。但无论以哪种方式返回与原字符串内容不同的子串时,都会重新分配内存,并复制数据。不像某些语言那样,仍旧以指针引用原字符串内容缓冲区。  N' q' h6 T" e1 r" A) Q
先看相同或不同内容时,字符串对象构建情形。
$ b0 D+ M" k- m6 Q: o>>> s = "-" * 10245 O: y( `  q4 g0 `: Y; ~/ C. A
>>> s1 = s[10:100] # 片段,内容不同。( p; s, _# _( q  k+ \
>>> s2 = s[:] # 内容相同2 g- M  c6 j7 T9 ^9 A" J6 ?
>>> s3 = s.split(",")[0] # 内容相同。
4 ^4 J/ s; I$ Y# O# ~>>> s1 is s # 内容不同,构建新对象。
- I% x1 O0 K8 U* gFalse1 c, Q! F0 l- n
>>> s2 is s # 内容相同时,直接引用原字符串对象。
# `% e. D! U1 q' r% |7 XTrue
. \; g$ h8 K0 z. t/ w0 I1 {3 y>>> s3 is s
+ H& P% J2 j$ h; ITrue
9 T2 U( o* q* B6 M5 `再进一步用 memory_profiler 观察内存分配情况。1 m' f! N. F5 W6 B- J3 W3 f$ s% ^; c
@profile, ]" C! ]( R, A' A; H$ R
def test():& Q8 n* q! L6 ~6 K5 ~' b$ |
a = x[10:-10]
$ U# t/ a/ S* s+ d4 n7 z: ^+ O b = x.split(",")
2 l. t  A4 M# Y, Y  e) | return a, b9 U3 r7 J+ p# w" `
x = "0," * (1 << 20)/ `! c0 R& q+ B; p
test()
2 e" N5 I+ F. G* \% R输出% G" D1 {: Q) @* n& L. W
$ python -m memory_profiler ./test.py) O% T) ]& p; o* W/ @8 m$ i7 M  @0 l3 Y
此类行为,与具体的 Python 实现版本有关,不能一概而论。
字符串类型内置丰富的处理方法,可满足大多数操作需要。对于更复杂的文本处理,还可使用正则表达式(re)或专业的第三方库,比如 NLTK、TextBlob 等。# O1 ?6 B! ^! k! p% A
转换
2 l7 l( V4 I1 I除去与数字、Unicode 码点的转换外,最常见的是在不同编码间进行转换。+ G! G3 f3 L- i% A: _& m! J% i
Python 3 使用 bytes、bytearray 存储字节数组,不再和 str 混用。
>>> s = "汉字"& r6 S9 r) m% J  K" H
>>> b = s.encode("utf-16") # to bytes
; J0 u! {9 P3 f>>> b.decode("utf-16") # to unicode string3 k$ t8 ]) t9 {/ v6 }
汉字
: N: X1 {0 p& _- k8 O) o如要处理 BOM 信息,可导入 codecs 模块。3 j0 e% t3 r, W+ z* e
>>> s = "汉字"
, b' Z" [* o+ F, ^7 E1 K$ S>>> s.encode("utf-16").hex()
* i" i& s9 F, mfffe496c575b. q# c! W- o! B# J5 `+ D
>>> codecs.BOM_UTF16_LE.hex() # BOM 标志。* |1 G5 w/ c! a8 [8 o
fffe
* N3 L* j6 l- O5 }3 m, K>>> codecs.encode(s, "utf-16be").hex() # 按指定 BOM 转换。, a" c6 u# J. P0 n. I  G% B
6c495b57
3 v9 D4 X) w% j4 S* l>>> codecs.encode(s, "utf-16le").hex()2 O: S- \5 u8 w
496c575b
; l1 V. ^! W5 F1 @还有,Python 3 默认编码不再是 ASCII,所以无需额外设置。5 q' a7 L) Q  c& U7 e& q6 T: a( Z
Python 3.6
5 x" N; ^! U% h- @: G: S>>> sys.getdefaultencoding()
+ X7 K, x2 a6 u* E8 m( J  h& uutf-8+ D; ~0 Y7 A& Q7 u8 B  E7 a: {, m
Python 2.7
3 }# p1 {1 M) u; I9 o- Y>>> import sys4 G; ~8 Y" s1 Z2 g; \* a% ~
>>> reload(sys)# A, y6 j% `2 y+ d& B+ b  r+ ^
>>> sys.setdefaultencoding("utf-8")
2 `% l  `/ s! ~  _; i7 y6 |! l>>> b = s.encode("utf-16"), Z1 L/ V1 O8 j$ f1 x) d
>>> b.decode("utf-16")
3 V/ A5 \- }6 C$ Zu'\u6c49\u5b57'9 R! z2 O6 a5 f. |6 a. ^' O: j
>>> type(b)
# s* x1 Y+ [& H  S<type 'str'>
1 E0 e  h) D3 H格式化! P& {( k* H( q% f0 n$ C
长期发展下来,Python 累积了多种字符串格式化方式。相比古老的面孔,人们更喜欢或倾向于使用新的特征。: F4 y  h# @( P- _
Python 3.6 新增了 f-strings 支持,这在很多脚本语言里属于标配。
- w$ r2 O$ i5 m2 m& K4 x使用 f 前缀标志,解释器解析大括号内的字段或表达式,从上下文名字空间查找同名对象进行值替换。格式化控制依旧遵循 format 规范,但阅读体验上更加完整和简洁。$ B1 H( J5 E" z- e6 W6 e( r5 N
>>> x = 10, P6 z$ ^+ X7 @& p  Y/ J
>>> y = 20
7 j8 I( l/ n; z1 {. x- u>>> f"{x} + {y} = {x + y}" # f-strings& z' C) E/ b/ C2 [1 o! q
10 + 20 = 30
& R& A  Y; x( y# x3 {>>> "{} + {} = {}".format(x, y , x + y)2 ^6 O0 V% w: n/ }
10 + 20 = 30
6 i- Z9 @  `  [: d/ D; O, m3 l表达式除运算符外,还可以是函数调用。
  \- ^2 Q: f- G, S. Y' R7 h% _>>> f"{type(x)}") b1 ^: k5 _' Q/ Z4 C* z3 z* R
<class 'int'>( i/ b  p( ]* `# g/ W* O' v( i' d2 S
完整 format 格式化以位置序号、字段名匹配替换值参数,允许对其施加包括对齐、填充、 精度等控制。从某种角度看,f-strings 有点像是 format 的增强语法糖。+ V0 O; E+ ]  B" C

$ D( ]" a3 T' O将两者进行对比,f-strings 类模版方式更加灵活,一定程度上将输出样式与数据来源分离。 但其缺点是与上下文名字耦合,导致模版内容与代码必须保持同步修改。而 format 的序号与主键匹配方式可避开这点,只可惜它不支持表达式。8 y2 j7 m# X( y; K* L
另外,对于简短的格式化处理,format 拥有更好的性能。5 z( k7 |' l1 Q7 k: N$ _
手工序号和自动序号
2 m; m1 u6 G5 r$ J>>> "{0} {1} {0}".format("a", 10)6 v# j4 [7 D, G
a 10 a
2 Z; `9 D5 z; l! G" v>>> "{} {}".format(1, 2) # 自动序号,不能与手工序号混用。' b7 \7 N1 q. s/ B- D* W* C
1 20 l$ v0 k/ c( t/ q$ w/ c
主键
. t4 d5 K! k; S" h( C: m$ d# O>>> "{x} {y}".format(x = 100, y = [1,2,3])
# c  Q3 v2 n4 r, r100 [1, 2, 3]
1 J9 T$ p  j6 o属性和索引
4 W2 b! F; ]( G9 S  [$ Y2 E>>> x.name = "jack"4 L# Y! F" J: i
>>> "{0.name}".format(x) # 对象属性。  i7 \; k- T& w( J& X
jack$ f( J* z4 L9 [8 [" Y' H
>>> "{0[2]}".format([1,2,3,4]) # 索引。
7 B+ X& ]% y) Y3 b0 Z36 \0 [3 s4 _- l0 g
宽度、补位
' H- C1 U; P. }. Q" b>>> "{0:#08b}".format(5)
, Q! }$ T6 N* g4 {; E1 o8 }0b000101/ n( C1 ^- @( i1 {
数字
2 F+ W% g8 t" [8 E, p7 q" O>>> "{:06.2f}".format(1.234) # 保留 2 位小数。" y& X+ q7 A5 T/ u6 [# D
001.23/ {) b  N8 |2 `: B* N8 a& b7 x5 F
>>> "{:,}".format(123456789) # 千分位。
0 E& A1 v% f( }4 V  C" Q6 y" S2 I123,456,789. W# _+ r6 ^0 I0 A3 J# z& [
对齐
5 |* S5 q  `$ z9 ?- m$ k& M>>> "[{:^10}]".format("abc") # 居中+ U4 i& i2 ~+ D. d7 g- T6 h
[ abc ]
' E3 Q$ }# d+ }" L8 u>>> "[{:.<10}]".format("abc") # 左对齐,以点填充。
! }2 a% [7 H  m[abc.......]
1 {3 \' M! H3 _古老的 printf 百分号格式化方式已被官方标记为 “obsolete”,加上其自身固有的一些问题,可能会被后续版本抛弃,不建议使用。另外,标准库里 string.Template 功能弱,且性能也差,同样不建议使用。
6 W7 Y$ j4 C2 o3 }" h7 ^0 i" G池化
3 U2 B, L# j- Q7 L" \# q0 T7 {字符串算是进程里实例数量较多的类型之一,因为无处不在的名字就是字符串实例。# u( n0 }! w0 _" C8 v
鉴于相同名字会重复出现在各种名字空间里,那么有必要让它们共享对象。内容相同,且不可变,共享不会导致任何问题。关键是可节约内存,且省去创建新实例的调用开销。
/ [$ E5 I3 Q1 V对此,Python 的做法是实现一个字符串池(intern)。池负责管理实例,使用者只需引用即可。另一潜在好处是,从池返回的字符串,只需比较指针就可知道内容是否相同,无需额外计算。可用来提升哈希表等类似结构的查找性能。$ k& Y. Q( f- }! Y6 U9 u
>>> "__name__" is sys.intern("__name__")
# v- \/ p- I: }2 |, U- xTrue
  _5 ]9 \4 ~& Z2 Z  `除了以常量方式出现的名字和字面量外,动态生成字符串一样可加入池中。如此可保证每 次都引用同一对象,不会有额外的创建和分配操作。5 S# l2 T& y( N
>>> a = "hello, world!"& s) w% D$ T1 w; k, A& ~3 q
>>> b = "hello, world!"3 p# l% d- M, |3 w
>>> a is b # 不同实例。
' ^3 C& V+ F, x- z; g3 q- [0 dFalse
( T' H5 ^' |+ c& ~0 n, l$ u>>> sys.intern(a) is sys.intern("hello, world!") # 相同实例。4 y; r+ }) a: l7 s% Y
True
0 M+ e" H+ j! k& u; L当然,一旦失去所有外部引用,池内字符串对象会被回收。
# q) z7 x7 F9 s; k  M0 T>>> a = sys.intern("hello, world!")9 r. P- ^1 N0 w) r+ H, ?: H
>>> id(a)7 D$ h4 s$ D, R, N/ i
4401879024
" P3 E0 _1 X$ v- Q>>> id(sys.intern("hello, world!")) # 有外部引用。# B- ^9 G7 C. J( Y
4401879024/ T& f) N% ^* I. _8 e6 H
>>> del a # 删除外部引用后被回收。1 b) W$ I. a; J2 E$ v
>>> id(sys.intern("hello, world!")) # 从 id 值不同可以看到新建,入池。$ d, j" ~& B% @( C8 ^% p0 O8 r2 k
4405219056
& l9 ]; _2 ?- P0 v& b  f6 X  J
字符串池实现算法很简单,就是简单的字典结构。 详情参考 Objects/unicodeobject.c : PyUnicode_InternInPlace。
做大数据处理时,可能需创建海量主键,使用 intern 有助于减少对象数量,节约大量内存。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有帐号?立即注册

x
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

发布主题
推荐阅读更多+
阅读排行更多+
用心服务创业者
0851-88611148
周一至周五 9:00-18:00
意见反馈:admin@0851life.com

扫一扫关注我们

Powered by 童码少儿编程 X3.4© 2001-2013 0851life Inc.|网站地图