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

[复制链接]
sosoyoyo 发表于 2017-12-31 09:40:20 | 显示全部楼层 |阅读模式 打印 上一主题 下一主题
字符串8 T( V" v. ~/ q. {% }
字符串 (str) 存储 Unicode 文本,是不可变序列类型。相比 Python 2 里的混乱,Python 3 总算顺应时代发展,将文本和二进制彻底分离。7 O- p, y9 h6 [, T3 }
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 = "汉字"& V, h9 ]# n* H" q( X" d
>>> len(s)
/ `' ?2 k7 E- t2 C2 c2 d8 F& E2
% d" q( p8 X1 I" _% c! @>>> hex(ord("汉")) # code point
% |7 c" z' S% K. P1 z0x6c496 v& b: M1 A, h# T3 a. o
>>> chr(0x6c49)  Q+ m) K0 j7 }- C
0 _0 A/ ]- |" L8 |0 N
>>> ascii("汉字") # 对 non-ASCII 进行转义。
. Q0 I. y( N# d; `# O3 q\u6c49\u5b57
* t8 m8 j9 K) B4 ~1 [字符串字面量(literal)以成对单引号、双引号,或跨行三引号语法构成,自动合并相邻字面量。支持转义、八进制、十六进制,或 Unicode 格式字符。6 g3 A7 K( g$ s7 _) r
用单引号还是双引号,并没有什么特殊限制。如果文本内引用文字使用双引号,那么外面用单引号可避免转义,更易阅读。通常情况下,建议遵循多数编程语言惯例,使用双引号标示。除去单引号在英文句法里的特殊用途外,它还常用来表示单个字符。
$ K; X6 i1 E! |$ u, S1 P>>> "h\x69, \u6C49\U00005B57"
  ~" F6 W; \9 A0 K) fhi, 汉字2 J! b( k4 L' k/ ~$ W2 [. s
注意:Unicode 格式大小写分别表示 16 位和 32 位整数,不能混用。
>>> "It's my life" # 英文缩写。: k& U6 p4 x. y2 x# O& j- j' G; p
>>> 'The report contained the "facts" of the case.' # 包含引文,避免使用 \" 转义。* ~) U' V9 d9 s. a& W9 K, f
>>> "hello" ", " "world" # 合并多个相邻字量。1 X! q# F, r* k  [, T( E
hello, world* \6 ^/ @, ^& }9 }
>>> """ # 换行符、前导空格、空行都是组成内容。
4 O& K/ Y! _6 G The Zen of Python, by Tim Peters ) C4 y# F3 f* X
Beautiful is better than ugly. 8 y1 T( r$ N" T% z, f5 L1 L9 O
Explicit is better than implicit. : c$ M9 e4 N7 X. t
Simple is better than complex.
* c% G7 ?) s9 p9 `* y"""
! D' Z7 N9 c# N* L, R7 c可在字面量前添加标志,指示构建特定格式字符串。2 p8 v7 z0 s7 E3 Z& q$ J) {
最常用的原始字符串(r, raw string),它将反斜线视作字符内容,而非转义标志。这在构建类似 Windows 路径、正则表达式匹配模式 (pattern) 之类的文法字符串时很有用。
7 l' j# T4 X+ G* j>>> open(r"c:\windows\readme.txt") # Windows 路径。) v* V. M) t* e5 I
>>> re.findall(r"\b\d+\b", "a10 100") # 正则表达式。
% X+ R/ d, B) V8 f['100']
4 ?0 D! E, v2 L3 ]# M4 E, q* {2 p' j>>> type(u"abc") # 默认 str 就是 unicode, 无需添加 u 前缀。
9 l' g" Y  ~* z: i$ Y" istr! z7 e' g4 G. _; ^6 r8 ?- k
>>> type(b"abc") # 构建字节数组。2 j3 W, O; s5 D2 @6 t
bytes: i9 K+ X. B3 S: c" r
操作9 f/ }- k6 @9 A# x
支持用加法和乘法运算符拼接字符串。, `. K/ \% P# l; m
>>> s = "hello"
/ a/ }* \6 L) b2 G8 k>>> s += ", world"
8 ?$ G$ \: Y$ W& f- `5 x  E/ ~>>> "-" * 10& V) Y/ M% f/ v, `4 F% b3 a
----------
6 v) k3 _3 O$ }7 g9 e  P编译器会尝试在编译期直接计算出字面量拼接结果,避免运行时开销。不过此类优化程度有限,并不总是有效。
* a+ F# Z/ w6 @9 R0 m4 }& @# Q>>> def test():! h4 C4 M( P# Z# \* o( j' \
a = "x" + "y" + "z". q9 B6 c3 U2 d' z3 I: ^5 W, j! D# c; Z6 g" w
b = "a" * 10" }1 `' {& y& V
return a, b" H- k7 K, y7 _+ l4 ^" ^
>>> dis.dis(test)" p1 K3 U* [2 K
2 0 LOAD_CONST 7 ('xyz') # 直接给出结果,省略加法运算。
$ m- ]* r! t# O 3 4 LOAD_CONST 8 ('aaaaaaaaaa') # 省略乘法运算。
' ]$ m- R6 q3 y至于多个动态字符串拼接,应优先选择 join 或 format 方式。+ w7 S, M% K# F0 W# x2 b& a
相比多次加法运算和多次内存分配 (字符串是不可变对象),join 这类函数 (方法) 可预先计算出总长度,一次性分配内存,随后直接拷贝内存数据填充。另一方面,将固定内容与变量分离的模版化 format,更易阅读和维护。! y* W' h! I6 z( x' b. u; r  }
>>> username = "qyuhen"
' X6 j. @) A, [% o) u>>> datetime = "2017010"
1 x4 W, }" A3 ~& L>>> "/data/" + username + "/message/" + datetime + ".txt": N( e+ o! I7 Q! T" o, n
/data/qyuhen/message/20170101.txt" p, z8 y# R9 f* U+ ^. E) N
>>> "/data/{user}/message/{time}.txt".format(user = username, time = datetime)
2 E  P1 g" Q8 S. f) Y4 l$ ~: N/ n/data/qyuhen/message/20170101.txt( Y+ _( I* H$ }( V2 ~+ t5 M
我们用 line_profiler 对比用加法和 join 拼接 26 个大写字母的性能差异。虽然该测试不具备代表性,但可以提供一个粗略的验证方法。# C) n/ E% I8 y# Y2 k5 m" P, C
#!/usr/bin/env python3
2 O* _( ?& J) a; d6 u7 g1 c6 o: q% Kimport string1 m0 X' m+ {' I7 C
x = list(string.ascii_uppercase)
9 Z5 r$ k9 E3 w: D( O& V@profile( v/ C9 k5 K6 ]1 X1 O
def test_add():- E: v5 d. S0 v6 ~9 k+ a: s# L
s = ""* a( Q4 P) k4 e+ p! o. }
for c in x:; v5 m# K; p  c
s += c* M! w5 I/ q) @# j$ ?
return s
; v1 f( c( [3 g  d8 R! n, Z' r0 e% h@profile
. B, s9 G# H7 K$ c) Z- n& qdef test_join():
9 a7 [3 m6 \' d return "".join(x)9 Q2 L: E) Z8 t# ~0 k
test_add()) t( d7 G4 k9 `: ?: j6 g
test_join()
% [# X  q+ H) n输出:+ ?4 r4 f" U6 _
$ kernprof -l ./test.py && python -m line_profiler test.py.lprof- h9 p( o2 @7 c: \  R( P
编写代码除保持简单外,还应具备良好的可阅读性。比如判断是否包含子串,in、not in 操作符就比 find 方法自然,更贴近日常阅读习惯。
. N9 ?' H' i+ n9 \7 t
>>> "py" in "python"
4 \3 d* j% h" c7 J& Y7 f- [True+ v7 S/ G* f: o% [2 p  l+ r
>>> "Py" not in "python"
. ~/ c- t2 ?5 S2 f8 U, ^5 HTrue: n: I' c+ e! g/ Y' R
作为序列类型,可以使用索引序号访问字符串内容,单个字符或者某一个片段。支持负索引,也就是从尾部以 -1 开始(索引 0 表示正向第一个字符)。
4 s& {$ p5 M9 q& }4 `! T>>> s = "0123456789"
7 F: M( s: q# Z1 Z' \- `& n& R* l>>> s[2]
, e, B1 V/ r7 R9 _8 Y+ Q0 R28 G" D0 R6 y7 g( Z+ q9 m- Z
>>> s[-1], I" r0 X1 ~+ Q- h/ \$ C
9
  E/ d3 V) n2 U+ E) L* ?. C>>> s[2:6]" Z1 q$ @4 I1 v, U! X# S
2345: y( l7 x! r) }" `" H
>>> s[2:-2]$ U, {2 V0 U  e) [' u
234567
/ V2 m0 M3 A# ^# o使用两个索引号表示一个序列片段的语法称作切片(slice),可以此返回字符串子串。但无论以哪种方式返回与原字符串内容不同的子串时,都会重新分配内存,并复制数据。不像某些语言那样,仍旧以指针引用原字符串内容缓冲区。
+ X% t) E# `% ]4 g先看相同或不同内容时,字符串对象构建情形。# s" F. I) \% I7 v$ ]- N
>>> s = "-" * 1024
  v# {2 Q7 a9 T  x% B" N2 H>>> s1 = s[10:100] # 片段,内容不同。
8 M4 P9 N- f8 [# _! r>>> s2 = s[:] # 内容相同
& ~9 x( A" C; k>>> s3 = s.split(",")[0] # 内容相同。% x; U. D( N6 F) O' N, L/ s
>>> s1 is s # 内容不同,构建新对象。; U! ?: r+ l" V6 h9 J( K, ~
False  ^; J7 r- n! D3 u& }
>>> s2 is s # 内容相同时,直接引用原字符串对象。/ \$ @! R+ o1 |; u, G9 K. C7 h
True
6 T  Q) U! K# q; k>>> s3 is s$ u7 j2 |; n( o6 k" b( S
True
! L6 Y. N" b0 {9 o5 Q% [再进一步用 memory_profiler 观察内存分配情况。, w  L7 m" E% X7 Y- s6 k
@profile! y( @5 B, p' M
def test():
# i0 w8 j* g1 v! q a = x[10:-10]
1 I3 Z, c* d, \. [& C b = x.split(","). Z) w; P, Z( R0 K! q2 x; I
return a, b
1 p, \4 S6 P' Q2 {  K" ^8 |: Zx = "0," * (1 << 20)% f: G8 n( e: P7 }4 E
test()9 r6 R; b4 T9 [7 ^* U
输出8 W! V  W' N% R7 f  ^& B) ]: m
$ python -m memory_profiler ./test.py* ]0 \/ B! c# B1 H
此类行为,与具体的 Python 实现版本有关,不能一概而论。
字符串类型内置丰富的处理方法,可满足大多数操作需要。对于更复杂的文本处理,还可使用正则表达式(re)或专业的第三方库,比如 NLTK、TextBlob 等。
7 s2 j( w; i' a  D0 _% |9 k% j转换
, w; ?9 n8 r* p; \( O$ a除去与数字、Unicode 码点的转换外,最常见的是在不同编码间进行转换。- `# E7 `1 ^9 @" b8 a3 u8 A
Python 3 使用 bytes、bytearray 存储字节数组,不再和 str 混用。
>>> s = "汉字"8 I, Y8 d& D% v7 G
>>> b = s.encode("utf-16") # to bytes; w1 @. G  x6 k' O5 g6 i
>>> b.decode("utf-16") # to unicode string1 I: N7 Y3 r6 c$ {2 C+ r
汉字
5 W' Y, @( @  O$ b# V# c如要处理 BOM 信息,可导入 codecs 模块。7 u1 U8 _% A, p9 c! ]
>>> s = "汉字"- D. \' [6 c. p
>>> s.encode("utf-16").hex()
; k, x) `+ z% g" H  Efffe496c575b2 @, |" J6 o+ d, W/ W
>>> codecs.BOM_UTF16_LE.hex() # BOM 标志。0 I$ y' ]! j# T
fffe
+ L/ j- I4 \5 Y  Q>>> codecs.encode(s, "utf-16be").hex() # 按指定 BOM 转换。
* y8 u" t0 Z3 P0 \9 j; G* ^$ S6c495b57( {+ Y5 v/ }) m* A% `$ ^& R
>>> codecs.encode(s, "utf-16le").hex()8 d9 s, o4 B& q' A2 T+ y
496c575b
& r  H  ^- t: O0 v9 O还有,Python 3 默认编码不再是 ASCII,所以无需额外设置。
* b  d* t5 w  V9 xPython 3.6& J$ O# ^/ J! t; y
>>> sys.getdefaultencoding()
+ Q) t2 @3 [, T* B/ Eutf-8  r6 ^1 X, c' ]/ ~
Python 2.7  |5 [/ u# _" v! ~1 e' e$ v* A7 i
>>> import sys1 K# P/ L! l+ i7 q" K+ K  D9 j9 T
>>> reload(sys)
) d" a" [; P& L% I  s; q, |>>> sys.setdefaultencoding("utf-8")
# F( O# F) o! P; T) l+ B2 k( r: C>>> b = s.encode("utf-16")
/ s+ g- j6 P, O4 |4 m9 W' O( e+ L>>> b.decode("utf-16")
9 p$ ]& N& |+ n9 t# |u'\u6c49\u5b57'1 j! e" B7 D6 G9 K! G( u
>>> type(b)
6 Y- {# R# ~9 A- h4 Z4 r<type 'str'>7 W  @* y3 }- j, {, L+ a
格式化
0 X1 L( [! }4 \$ Z9 y6 I长期发展下来,Python 累积了多种字符串格式化方式。相比古老的面孔,人们更喜欢或倾向于使用新的特征。
* P+ W* h8 y. B* j+ I8 o8 iPython 3.6 新增了 f-strings 支持,这在很多脚本语言里属于标配。; l/ g0 Y) }6 F& z- ~/ o
使用 f 前缀标志,解释器解析大括号内的字段或表达式,从上下文名字空间查找同名对象进行值替换。格式化控制依旧遵循 format 规范,但阅读体验上更加完整和简洁。
9 {5 d2 F6 k6 W% m>>> x = 103 k/ H% q5 i  g4 N) P
>>> y = 20( K5 V, h, }# S( k+ L
>>> f"{x} + {y} = {x + y}" # f-strings
2 j: U5 K; t9 L5 o1 K10 + 20 = 30. C, e/ O6 M( z$ `% ?  D9 Q- \5 g" I
>>> "{} + {} = {}".format(x, y , x + y)9 G& R$ Z& d5 Z6 Y% _
10 + 20 = 30
) U) L- q$ j1 Y9 x7 N4 j1 R/ |- }表达式除运算符外,还可以是函数调用。
+ t3 U7 e! T# `/ L, _9 p>>> f"{type(x)}"2 r+ ]2 I6 f+ S- C! `) A
<class 'int'>
$ T  r  R; M5 w# y: k完整 format 格式化以位置序号、字段名匹配替换值参数,允许对其施加包括对齐、填充、 精度等控制。从某种角度看,f-strings 有点像是 format 的增强语法糖。
& |4 }" O& O" `+ C
" v) k5 [. u0 P8 r2 t0 w6 i; W将两者进行对比,f-strings 类模版方式更加灵活,一定程度上将输出样式与数据来源分离。 但其缺点是与上下文名字耦合,导致模版内容与代码必须保持同步修改。而 format 的序号与主键匹配方式可避开这点,只可惜它不支持表达式。' B5 I/ L" h& Q% ?  `9 U
另外,对于简短的格式化处理,format 拥有更好的性能。# I# C& _$ n) K) p
手工序号和自动序号
  u3 U0 o! w5 [3 z8 U9 r>>> "{0} {1} {0}".format("a", 10)
, o' g2 h+ N$ za 10 a/ q5 @$ s+ ?6 I
>>> "{} {}".format(1, 2) # 自动序号,不能与手工序号混用。6 ]( y2 @0 a. G, S0 S/ P
1 2; z0 d  j) i* t/ Q) w
主键7 G) P, z/ [+ z1 u, }- \
>>> "{x} {y}".format(x = 100, y = [1,2,3])9 y$ W& e$ b# M% i! f
100 [1, 2, 3]
# o6 m. i$ _/ o2 L, @属性和索引
2 y5 G9 e6 q7 ^( t# V4 ?>>> x.name = "jack"$ ^5 z1 k5 O. W: V
>>> "{0.name}".format(x) # 对象属性。$ C9 J6 @9 u7 J. w, Q; ^* k) W
jack* U; l2 t8 Q( o
>>> "{0[2]}".format([1,2,3,4]) # 索引。* \( U# e, A) ^3 T) B3 o
3
6 T& Z/ |" |/ }/ i! |宽度、补位
. ~, ~: I8 z( M) {; p! r>>> "{0:#08b}".format(5)
, e# I. H9 n6 ]% X% ?9 g0b000101; r& K7 z0 G( N/ b: p' Y: P6 d
数字; ?2 U; p% S7 E) C2 n5 q
>>> "{:06.2f}".format(1.234) # 保留 2 位小数。
6 e- R* {4 C0 A$ d4 n/ Y8 c' K001.23) o& i: {- C2 _; Q! T9 w# G$ B
>>> "{:,}".format(123456789) # 千分位。
0 |) X/ T! [' Q7 @123,456,789+ d7 e* f& S: H: \
对齐8 s7 u' f7 T: ^" i2 {
>>> "[{:^10}]".format("abc") # 居中
7 E% k% @% d0 o5 Y' ]4 w[ abc ]
& p% {3 A6 `' o3 F( q/ a# t/ v2 i3 x>>> "[{:.<10}]".format("abc") # 左对齐,以点填充。2 Y) t  f$ _+ J3 @& S" v
[abc.......]
# W" j$ |+ _* O. b7 P古老的 printf 百分号格式化方式已被官方标记为 “obsolete”,加上其自身固有的一些问题,可能会被后续版本抛弃,不建议使用。另外,标准库里 string.Template 功能弱,且性能也差,同样不建议使用。5 ^. h2 h# h8 X1 b/ w: x: ]* H
池化
# E. l  o0 L% u, n字符串算是进程里实例数量较多的类型之一,因为无处不在的名字就是字符串实例。
- m' |8 V7 |& {, O- w8 L鉴于相同名字会重复出现在各种名字空间里,那么有必要让它们共享对象。内容相同,且不可变,共享不会导致任何问题。关键是可节约内存,且省去创建新实例的调用开销。# N7 a+ w/ {& `9 j" ~7 b( I4 V3 K
对此,Python 的做法是实现一个字符串池(intern)。池负责管理实例,使用者只需引用即可。另一潜在好处是,从池返回的字符串,只需比较指针就可知道内容是否相同,无需额外计算。可用来提升哈希表等类似结构的查找性能。
5 _4 E( @; M0 i( b2 g4 c" H>>> "__name__" is sys.intern("__name__")# r- q' M' z  z3 J5 c! X
True
! U3 V" P( X8 S$ ~. ~; R; F4 y' O除了以常量方式出现的名字和字面量外,动态生成字符串一样可加入池中。如此可保证每 次都引用同一对象,不会有额外的创建和分配操作。
- ~) p- `9 t  L% h) [7 `1 ~>>> a = "hello, world!"
; \2 f4 ~- v. p5 r# G  _# p5 ?>>> b = "hello, world!"
, d6 V9 U$ n' K' j9 J>>> a is b # 不同实例。7 t) o6 X4 Q- n5 b# g
False
) K7 [8 N; r. Y! O>>> sys.intern(a) is sys.intern("hello, world!") # 相同实例。# S# z3 j8 Z1 t/ C
True
( A( ?% |4 y2 i* C: g+ q' Q当然,一旦失去所有外部引用,池内字符串对象会被回收。1 F& Q$ o- j! [5 R# a& \+ H) u
>>> a = sys.intern("hello, world!")
7 ?/ H5 a: H. A) A/ a$ G2 [1 `>>> id(a)
% v0 k" E3 t- d  ]/ E4401879024" s' a/ T4 U9 w: o. y0 R+ z
>>> id(sys.intern("hello, world!")) # 有外部引用。
" r1 V% |3 n# O0 v- j: O7 z4401879024& R) S  V1 A+ F# ?' U$ t% u
>>> del a # 删除外部引用后被回收。  `! j5 s0 }6 G/ ^
>>> id(sys.intern("hello, world!")) # 从 id 值不同可以看到新建,入池。5 n: N% H1 `1 v2 w$ G
4405219056
1 U5 }. R6 u, j8 q# c3 \  B. o0 ]' d
字符串池实现算法很简单,就是简单的字典结构。 详情参考 Objects/unicodeobject.c : PyUnicode_InternInPlace。
做大数据处理时,可能需创建海量主键,使用 intern 有助于减少对象数量,节约大量内存。

本帖子中包含更多资源

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

x
回复

使用道具 举报

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

本版积分规则

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

扫一扫关注我们

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