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

[复制链接]
sosoyoyo 发表于 2017-12-31 09:40:20 | 显示全部楼层 |阅读模式 打印 上一主题 下一主题
字符串
( V4 H3 t/ \/ M; G, }字符串 (str) 存储 Unicode 文本,是不可变序列类型。相比 Python 2 里的混乱,Python 3 总算顺应时代发展,将文本和二进制彻底分离。9 A8 u  s3 i$ C- u. Q! s
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 = "汉字"
1 B8 v1 S( J$ Q  e- b>>> len(s)* X( c. V* g0 O9 U0 [6 O$ Y
2
# J# J+ o# U% n: f6 H>>> hex(ord("汉")) # code point
* d: Z$ r: [: ~5 A0x6c49$ i, O, H! J2 F) v8 b' F
>>> chr(0x6c49), x7 G- M% v8 \7 o: [/ d

& P8 T2 S! l$ }& H>>> ascii("汉字") # 对 non-ASCII 进行转义。; a* N0 \0 D0 j$ ?6 [
\u6c49\u5b57
; i! e4 }) E4 Z  |5 J3 y字符串字面量(literal)以成对单引号、双引号,或跨行三引号语法构成,自动合并相邻字面量。支持转义、八进制、十六进制,或 Unicode 格式字符。
6 O- m1 Z+ S. p+ C+ i! F+ O3 }% H, f用单引号还是双引号,并没有什么特殊限制。如果文本内引用文字使用双引号,那么外面用单引号可避免转义,更易阅读。通常情况下,建议遵循多数编程语言惯例,使用双引号标示。除去单引号在英文句法里的特殊用途外,它还常用来表示单个字符。; z% `' Y! p+ t! t7 |) i$ j" P
>>> "h\x69, \u6C49\U00005B57"5 `5 `- B: [  M9 T
hi, 汉字
* p1 }5 R. Z2 o2 _1 g# L! r8 j
注意:Unicode 格式大小写分别表示 16 位和 32 位整数,不能混用。
>>> "It's my life" # 英文缩写。9 N+ _5 X. x9 R" ~6 R
>>> 'The report contained the "facts" of the case.' # 包含引文,避免使用 \" 转义。4 G2 l/ X. ^1 W8 f6 ~
>>> "hello" ", " "world" # 合并多个相邻字量。' z; B1 J* F) U) v- X$ V
hello, world- W5 L* _, s) n' t7 L, w. ^
>>> """ # 换行符、前导空格、空行都是组成内容。
, d% U; h1 G$ [ The Zen of Python, by Tim Peters
% w+ E* K- L8 |! N2 V" |7 a Beautiful is better than ugly.
3 t" [# Y4 T8 i- M7 L Explicit is better than implicit. 3 u' b' c4 ?0 A0 W0 u" |1 R
Simple is better than complex.+ u, Q8 r0 H$ n* B0 u
""", u$ _# m# P. l2 }9 k, V3 j
可在字面量前添加标志,指示构建特定格式字符串。
  L, R  H/ g4 \; G" ^( G- Y4 w最常用的原始字符串(r, raw string),它将反斜线视作字符内容,而非转义标志。这在构建类似 Windows 路径、正则表达式匹配模式 (pattern) 之类的文法字符串时很有用。4 q+ O( N7 {2 X- l5 I9 c! b
>>> open(r"c:\windows\readme.txt") # Windows 路径。% K5 {4 ]% O( i# ]3 i0 q
>>> re.findall(r"\b\d+\b", "a10 100") # 正则表达式。
7 C3 d. ~  V2 i9 [['100']8 Z0 V4 g. [- Q' `0 w$ h8 W
>>> type(u"abc") # 默认 str 就是 unicode, 无需添加 u 前缀。( Q% `: @3 @/ j7 @, U  K2 b
str
' J2 {7 v# O: B( z& I8 T  H>>> type(b"abc") # 构建字节数组。5 t  @0 S) u( k7 ?- \
bytes! b6 A8 `6 v' P) g
操作' Z! B1 O& F9 f: y
支持用加法和乘法运算符拼接字符串。
1 ~6 Z" O: J) I4 X3 H>>> s = "hello"2 P+ f( V' Y' c) A  t- M
>>> s += ", world"
4 f4 E9 z9 L) b$ s' E! n: d2 j. d2 P" O>>> "-" * 10
$ ^: F/ \0 w: ^5 h% n----------0 W$ E* \6 z2 F) r6 L, Z
编译器会尝试在编译期直接计算出字面量拼接结果,避免运行时开销。不过此类优化程度有限,并不总是有效。
+ G0 F! D3 U2 J9 M>>> def test():
, d" t1 ~  F3 d+ n a = "x" + "y" + "z"1 w% x( r' T! G9 U
b = "a" * 109 W: z! s* i9 M0 X5 I- A6 `' [
return a, b
" R: X' X9 e. H/ E# [>>> dis.dis(test)
) _3 D6 M2 ?- o& i+ A( Q7 ? 2 0 LOAD_CONST 7 ('xyz') # 直接给出结果,省略加法运算。+ q  D" T6 u8 |& B
3 4 LOAD_CONST 8 ('aaaaaaaaaa') # 省略乘法运算。0 w+ C# `& Z- z, {& p" V
至于多个动态字符串拼接,应优先选择 join 或 format 方式。/ @+ t8 C/ S1 J' Q! W* C
相比多次加法运算和多次内存分配 (字符串是不可变对象),join 这类函数 (方法) 可预先计算出总长度,一次性分配内存,随后直接拷贝内存数据填充。另一方面,将固定内容与变量分离的模版化 format,更易阅读和维护。5 R3 p5 z7 l) |! }) N3 a2 ^6 H" G
>>> username = "qyuhen"4 t. K: l$ d( f9 I
>>> datetime = "2017010"0 ~1 M/ {  y+ f8 w. [2 n
>>> "/data/" + username + "/message/" + datetime + ".txt"
4 r4 p1 i+ f9 }/data/qyuhen/message/20170101.txt% R) G6 b' ]% M2 ]# U! O8 M( P3 F$ m6 O
>>> "/data/{user}/message/{time}.txt".format(user = username, time = datetime)
) c0 `3 V6 D1 _4 C( ^5 F/data/qyuhen/message/20170101.txt+ F; G; V& ~' A
我们用 line_profiler 对比用加法和 join 拼接 26 个大写字母的性能差异。虽然该测试不具备代表性,但可以提供一个粗略的验证方法。5 G- O/ s5 Z; y, h0 \& v- o3 b6 K
#!/usr/bin/env python3
9 j% d1 i( ^/ k& A0 {% Yimport string# D0 E( Y  n* n) R/ y7 O* Z
x = list(string.ascii_uppercase)
0 n$ y( s  D. t" J1 l@profile
0 A- K& U; l) k4 t$ m  udef test_add():. A6 l' W+ x) z' Z8 F6 x4 s9 V# l
s = ""
( k1 D" l) }4 U1 \* w for c in x:, ]" B" b: A2 Y8 U! |" }
s += c
' T$ i# I/ B8 e$ K2 R  o! Z/ W  f6 ?# o return s9 B5 P6 Q8 z( D- W5 ^# P. [
@profile
: f( t% C- D6 B% {7 [7 P: ^def test_join():
; ~/ P! ]9 J0 n7 L/ m return "".join(x)8 p/ S4 v1 a% \( u' B0 r4 Y& y6 h
test_add()" J! g* v0 p" ~3 V% {( Q
test_join()
* i. j, P0 ?9 M* w( U输出:; ^6 }8 o' g' F  c0 i
$ kernprof -l ./test.py && python -m line_profiler test.py.lprof
5 [2 i; K! D. j- q" `4 r
编写代码除保持简单外,还应具备良好的可阅读性。比如判断是否包含子串,in、not in 操作符就比 find 方法自然,更贴近日常阅读习惯。+ b) v3 v; g9 j: l% A
>>> "py" in "python"
+ Y: F1 i; _( V; H0 ZTrue
! E) `. V& k6 J& k>>> "Py" not in "python"; @4 ?, G& p0 y4 @
True0 |3 R3 Y, c9 g$ ~; X& `9 b8 w8 s
作为序列类型,可以使用索引序号访问字符串内容,单个字符或者某一个片段。支持负索引,也就是从尾部以 -1 开始(索引 0 表示正向第一个字符)。
: E! T# b& v' b4 Y>>> s = "0123456789": J3 Z' l- ^7 R: I
>>> s[2]
! f, t7 z: q+ {2
9 b9 Q2 @- n0 n2 s9 z- X>>> s[-1]
3 i. ]1 \( }, q1 I0 n! K- Z9
% _- t$ K( A# U+ N- y% S8 i. A>>> s[2:6]& h3 ^, S" p( B! D
2345
( P: N. D' R9 d- Z  ~, J: g  r6 {>>> s[2:-2]! X+ J% |# s& ?, m" ]  n
234567
- K  O' ?  g  o* X使用两个索引号表示一个序列片段的语法称作切片(slice),可以此返回字符串子串。但无论以哪种方式返回与原字符串内容不同的子串时,都会重新分配内存,并复制数据。不像某些语言那样,仍旧以指针引用原字符串内容缓冲区。$ T0 k% c. m: u6 U  Q1 v
先看相同或不同内容时,字符串对象构建情形。  T$ m% K% j1 [) O8 ]6 ]% I! \) c
>>> s = "-" * 1024* n  ]- k( L  \7 c9 i
>>> s1 = s[10:100] # 片段,内容不同。
! r( y; m5 R  ]& h5 h8 e7 B" A* `$ Y>>> s2 = s[:] # 内容相同
) c  G4 H3 ?/ j$ r>>> s3 = s.split(",")[0] # 内容相同。" {) P4 ~/ I- ]0 Z
>>> s1 is s # 内容不同,构建新对象。$ E1 i: ~4 {- D3 \) ?5 ?6 N/ a% p
False
- D0 y7 y0 U! K0 R>>> s2 is s # 内容相同时,直接引用原字符串对象。. o  V4 Z* K. Y2 u& A; A; p0 H
True+ _) T- J$ R1 Z$ q' I9 |7 q% }
>>> s3 is s1 |. _, y, h) ?1 e7 C
True
- t1 T. w# R9 [; s1 O% ?9 u再进一步用 memory_profiler 观察内存分配情况。
1 Y: v( T8 t+ [( f& `@profile$ ]9 I3 Y2 e3 l# S3 e
def test():; N$ V9 ^5 D" R) O* C+ @
a = x[10:-10]4 k7 V) z- q7 B, k# v" c
b = x.split(",")
" e) H/ X- g2 B return a, b* M9 W$ {+ i: O" p* N1 j
x = "0," * (1 << 20)
1 i/ @6 @. J, Qtest()
2 _; X8 q& X+ _* j5 g  F" I1 s) ?" L( Y0 g输出. i  {9 v! T# V1 s
$ python -m memory_profiler ./test.py
' l6 p; w: I4 X! ]
此类行为,与具体的 Python 实现版本有关,不能一概而论。
字符串类型内置丰富的处理方法,可满足大多数操作需要。对于更复杂的文本处理,还可使用正则表达式(re)或专业的第三方库,比如 NLTK、TextBlob 等。, }  j& }4 X/ j
转换
/ @2 u8 z& K0 {8 h: J! {; i. Y除去与数字、Unicode 码点的转换外,最常见的是在不同编码间进行转换。
& J: \* q! G9 |: M/ Z, ?
Python 3 使用 bytes、bytearray 存储字节数组,不再和 str 混用。
>>> s = "汉字"
, {4 Y- w( J* j# E8 M>>> b = s.encode("utf-16") # to bytes* v2 L+ t+ S( V5 _+ I! Y2 g' D$ R8 w
>>> b.decode("utf-16") # to unicode string/ B% S$ i6 z5 g8 d
汉字
4 {1 m" l3 E4 Y! K1 h- D如要处理 BOM 信息,可导入 codecs 模块。
- W" C7 W; V3 `( {2 |& z>>> s = "汉字"+ ~! `  \' @2 M8 o7 k: @2 `+ Q
>>> s.encode("utf-16").hex()
0 g  }% c+ `8 ~9 T6 e( A6 ?+ ]& i. pfffe496c575b8 W- u  `7 v# O% N
>>> codecs.BOM_UTF16_LE.hex() # BOM 标志。4 v0 m0 M: L5 O
fffe4 V# L; C9 ]/ c
>>> codecs.encode(s, "utf-16be").hex() # 按指定 BOM 转换。3 r( b8 w9 w" d' I% L" w6 \4 z
6c495b57
4 [' G  @0 }3 N1 h>>> codecs.encode(s, "utf-16le").hex()
( e* ^  V" C/ X& b; @/ z496c575b+ _; g; o. [1 ^! M( }
还有,Python 3 默认编码不再是 ASCII,所以无需额外设置。! O# R- p6 t3 M# ^
Python 3.6
1 D8 A7 M9 L/ u+ P>>> sys.getdefaultencoding()
2 a# p4 ]5 z7 Y) C& w2 Q- w' l8 f! `$ tutf-8
$ [7 T( q/ `3 z. U% v1 q! C% M; DPython 2.7, o" [. ?3 l2 |+ S
>>> import sys, U/ g3 V8 A% E5 i0 {: K
>>> reload(sys)
' K, }$ \8 T' T: a9 p- e>>> sys.setdefaultencoding("utf-8")
5 {& ?% u/ A; b6 h/ V. `0 ^>>> b = s.encode("utf-16")
! T4 l- f8 g' r- J: u7 j- D4 o7 L>>> b.decode("utf-16")
" I/ B! s" P; @* S* V% M% L  [u'\u6c49\u5b57'
1 _6 ~, ~% o. K, A>>> type(b)
8 z, ]+ r- B9 F7 w! ?* }+ r<type 'str'>: o/ ~+ Y; w  \/ A# h( T5 d3 x
格式化
6 ~8 u  g: b) g长期发展下来,Python 累积了多种字符串格式化方式。相比古老的面孔,人们更喜欢或倾向于使用新的特征。
" }4 r5 P/ i3 @8 r4 G% U' i) U9 L$ QPython 3.6 新增了 f-strings 支持,这在很多脚本语言里属于标配。
- @2 j. D( ]+ y6 q5 N6 `) m3 Z使用 f 前缀标志,解释器解析大括号内的字段或表达式,从上下文名字空间查找同名对象进行值替换。格式化控制依旧遵循 format 规范,但阅读体验上更加完整和简洁。8 S0 |9 V1 M& ?+ q) W
>>> x = 10
$ u) d  _5 J" R2 \' S" q3 O" M>>> y = 20
$ y$ k: X$ h, ~1 H>>> f"{x} + {y} = {x + y}" # f-strings$ H% R. n$ ]( V1 E3 n0 ^$ T( h3 Y
10 + 20 = 30& r' f( Q  ]5 G' `  Y1 g' p7 Q0 ^
>>> "{} + {} = {}".format(x, y , x + y)
' b  K1 X3 r7 T; o# E5 Y, m: o10 + 20 = 30
0 J% A- ^5 w4 B! u! O表达式除运算符外,还可以是函数调用。6 o9 q9 x9 U4 Q  H9 x" Y
>>> f"{type(x)}"% U# S4 U- e* K- `0 [& H: {
<class 'int'>& y" F5 p! `$ j* h
完整 format 格式化以位置序号、字段名匹配替换值参数,允许对其施加包括对齐、填充、 精度等控制。从某种角度看,f-strings 有点像是 format 的增强语法糖。/ `# x$ _! e, Z7 j' J. t

. l, f) P+ M, b/ [* \! Y$ p将两者进行对比,f-strings 类模版方式更加灵活,一定程度上将输出样式与数据来源分离。 但其缺点是与上下文名字耦合,导致模版内容与代码必须保持同步修改。而 format 的序号与主键匹配方式可避开这点,只可惜它不支持表达式。
/ m7 r" U) |% X& c* @# K另外,对于简短的格式化处理,format 拥有更好的性能。' @$ y% c6 {6 k. ~
手工序号和自动序号
  b: `2 ]$ U" U3 o$ j  i! J>>> "{0} {1} {0}".format("a", 10)
1 C+ v* X+ u/ w7 l7 ^! k' na 10 a, j2 _! k- }+ V; B: S
>>> "{} {}".format(1, 2) # 自动序号,不能与手工序号混用。
  b8 _& J9 M2 L1 2
3 j& g8 q* \1 y# E, O. K/ q: Y主键
' _% @0 b$ c. E  ]>>> "{x} {y}".format(x = 100, y = [1,2,3])1 k8 P' ~! U; `1 x6 B
100 [1, 2, 3]! @+ J9 j' O. ]/ C( s' J  Q
属性和索引0 v3 W8 N# l( j4 r
>>> x.name = "jack"* v' v8 L% ?3 e4 L. N
>>> "{0.name}".format(x) # 对象属性。
6 z- M8 e* R4 i' d  njack0 Z, _1 f  Y" W
>>> "{0[2]}".format([1,2,3,4]) # 索引。& k0 R. s2 D  F. ^: z
3
4 `* Y9 h0 C# M/ d8 ]6 ]! s  [; C宽度、补位( W" m5 R' A. F8 X( z
>>> "{0:#08b}".format(5)
- a; s* y# F' U) {0b000101& t2 ^) f  A: D- N1 q
数字
. J1 u  o  L( d/ ?! a  M- H>>> "{:06.2f}".format(1.234) # 保留 2 位小数。; J+ Q" p* r. a0 f
001.23
2 U3 \/ M# g3 m: K; L1 O: l>>> "{:,}".format(123456789) # 千分位。# ?# f) B3 U9 f: a8 t3 e: U  u' u
123,456,789  C+ _; R5 o8 Q5 U/ K/ _, y
对齐
0 A2 K& y2 }5 z2 m7 S% B>>> "[{:^10}]".format("abc") # 居中- c' q! Z% T( ?9 @5 x
[ abc ]
7 W. D; W' P  H& U5 P  p>>> "[{:.<10}]".format("abc") # 左对齐,以点填充。
5 `7 ^5 W" L9 ]0 W. t) U[abc.......]
: J# V% K  A4 g2 v古老的 printf 百分号格式化方式已被官方标记为 “obsolete”,加上其自身固有的一些问题,可能会被后续版本抛弃,不建议使用。另外,标准库里 string.Template 功能弱,且性能也差,同样不建议使用。
. @: I4 @# R* w& L, V池化# _4 R- Y* E3 U
字符串算是进程里实例数量较多的类型之一,因为无处不在的名字就是字符串实例。
# o3 K6 r  a$ L鉴于相同名字会重复出现在各种名字空间里,那么有必要让它们共享对象。内容相同,且不可变,共享不会导致任何问题。关键是可节约内存,且省去创建新实例的调用开销。
) A  b3 v$ B9 p# z对此,Python 的做法是实现一个字符串池(intern)。池负责管理实例,使用者只需引用即可。另一潜在好处是,从池返回的字符串,只需比较指针就可知道内容是否相同,无需额外计算。可用来提升哈希表等类似结构的查找性能。
2 q* u4 [- K* g& t8 m( A9 z>>> "__name__" is sys.intern("__name__")
# v) n) W+ ?( |* X' {4 nTrue
# q  a4 n( S2 @7 g2 ^# J) z" f7 R除了以常量方式出现的名字和字面量外,动态生成字符串一样可加入池中。如此可保证每 次都引用同一对象,不会有额外的创建和分配操作。* K* ?- t* g" {$ O0 T2 y
>>> a = "hello, world!"- V- e' W7 T- j' G7 R
>>> b = "hello, world!"5 m. h3 o1 r+ D  p7 y
>>> a is b # 不同实例。- \  k* J9 L7 F  Y  s, H
False7 \: c7 Y* L; C8 W  I8 M3 K
>>> sys.intern(a) is sys.intern("hello, world!") # 相同实例。3 a% }' v# F& C% v$ G) O$ ?
True* G5 ]6 q7 x0 V; e7 E- o( W" |+ M
当然,一旦失去所有外部引用,池内字符串对象会被回收。# m' d& ~1 a0 n+ [( n
>>> a = sys.intern("hello, world!")
2 c) e1 V$ }4 k4 Y9 H2 N+ {' Z% z>>> id(a)
* ?9 y$ l5 ^8 H* p44018790244 F  D# p9 f/ R) E( g6 L# j
>>> id(sys.intern("hello, world!")) # 有外部引用。
9 C1 i" j+ j7 W! O: ~$ G  t4401879024. U/ v: _' |% B  N# l
>>> del a # 删除外部引用后被回收。0 i0 c/ o# N) c- a0 \
>>> id(sys.intern("hello, world!")) # 从 id 值不同可以看到新建,入池。/ k9 b8 m" j# e  `! c
4405219056
0 X% G4 U9 ~: }* P7 i0 _
字符串池实现算法很简单,就是简单的字典结构。 详情参考 Objects/unicodeobject.c : PyUnicode_InternInPlace。
做大数据处理时,可能需创建海量主键,使用 intern 有助于减少对象数量,节约大量内存。

本帖子中包含更多资源

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

x
回复

使用道具 举报

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

本版积分规则

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

扫一扫关注我们

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