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

[复制链接]
sosoyoyo 发表于 2017-12-31 09:40:20 | 显示全部楼层 |阅读模式 打印 上一主题 下一主题
字符串. X" S. W3 y' W7 N
字符串 (str) 存储 Unicode 文本,是不可变序列类型。相比 Python 2 里的混乱,Python 3 总算顺应时代发展,将文本和二进制彻底分离。. E0 ~% d* I0 Q. _0 F0 k  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 = "汉字"+ r4 O* \5 w4 r- i( b2 _
>>> len(s)
  U2 J- r% Z% J5 }* |3 r2
. `, y4 e) C" W>>> hex(ord("汉")) # code point6 G8 ~# G! ?7 \6 F! d/ S  `- K, K
0x6c49
/ _. i/ [; F3 r) l>>> chr(0x6c49)
: V0 }. N* n4 ?0 M& f
6 y( b& Y& ?) `/ U>>> ascii("汉字") # 对 non-ASCII 进行转义。  V* }3 U7 ~% i' [
\u6c49\u5b57
& e, a6 C% @  _* p8 L' `8 [$ e' g字符串字面量(literal)以成对单引号、双引号,或跨行三引号语法构成,自动合并相邻字面量。支持转义、八进制、十六进制,或 Unicode 格式字符。
, p! `; T0 m. \" _5 U+ j用单引号还是双引号,并没有什么特殊限制。如果文本内引用文字使用双引号,那么外面用单引号可避免转义,更易阅读。通常情况下,建议遵循多数编程语言惯例,使用双引号标示。除去单引号在英文句法里的特殊用途外,它还常用来表示单个字符。
6 w  ^$ Q9 N/ M5 @6 \% d>>> "h\x69, \u6C49\U00005B57". }7 l+ M" [) s! H4 {# [
hi, 汉字4 E3 e" x) U2 E: U
注意:Unicode 格式大小写分别表示 16 位和 32 位整数,不能混用。
>>> "It's my life" # 英文缩写。
9 W3 E$ F  l7 ~5 Q: C>>> 'The report contained the "facts" of the case.' # 包含引文,避免使用 \" 转义。
# Y9 B4 |1 u" X# A9 u>>> "hello" ", " "world" # 合并多个相邻字量。  V7 s6 C2 x& N, G9 @7 n
hello, world
$ Z: O' R' ?( {1 q>>> """ # 换行符、前导空格、空行都是组成内容。7 G7 u. c7 F% p- i6 D
The Zen of Python, by Tim Peters % J) I. |5 @9 P
Beautiful is better than ugly.
1 B  e+ O5 @$ a/ c Explicit is better than implicit. ! v4 I7 N, y: l
Simple is better than complex.1 G) S2 n& Z1 v7 A4 j4 N* W
"""
2 V  k9 J1 C  a1 k可在字面量前添加标志,指示构建特定格式字符串。
' W1 d- f, s+ K9 ?. f  B最常用的原始字符串(r, raw string),它将反斜线视作字符内容,而非转义标志。这在构建类似 Windows 路径、正则表达式匹配模式 (pattern) 之类的文法字符串时很有用。4 M5 Y$ v5 Q* E3 A8 ?
>>> open(r"c:\windows\readme.txt") # Windows 路径。4 o- }) g+ ]3 d5 x9 u
>>> re.findall(r"\b\d+\b", "a10 100") # 正则表达式。( t  C" d, ^7 S! t
['100']
" B6 u& i0 t" |7 C# q0 v$ U- R>>> type(u"abc") # 默认 str 就是 unicode, 无需添加 u 前缀。3 v6 ?8 S3 W' ]& U5 W7 A
str
$ K6 Q. O' W$ q3 c" _  }' s>>> type(b"abc") # 构建字节数组。
3 {1 }# D" J  N9 o: F: G& Abytes
0 ^* A4 k+ L. d3 M0 \操作) j8 q1 h. @" Z  S
支持用加法和乘法运算符拼接字符串。# P. L4 M8 N9 }( v6 R- Y
>>> s = "hello"! X* N# k8 ~+ B
>>> s += ", world"
0 T  O" o6 C( v. O: L( e>>> "-" * 10
7 v, g. l/ R$ B. g) P# q7 ^----------- l0 n6 B0 Z* s' C* r; x; l1 Y
编译器会尝试在编译期直接计算出字面量拼接结果,避免运行时开销。不过此类优化程度有限,并不总是有效。! I/ z9 |5 U9 N/ b- a, Q; Z
>>> def test():; w4 L; E, f4 L/ l/ y; {9 Y2 p
a = "x" + "y" + "z"2 S- k0 y0 s7 s! ^  H' l- u+ D3 r
b = "a" * 10
# f( J1 ?9 ~/ }! |) ?, q return a, b+ X. d' a8 v+ e8 @7 \2 h
>>> dis.dis(test)  F" ~# S& Y  H4 ^: H0 Q! m
2 0 LOAD_CONST 7 ('xyz') # 直接给出结果,省略加法运算。3 k3 {4 o; z+ A
3 4 LOAD_CONST 8 ('aaaaaaaaaa') # 省略乘法运算。
. n2 p6 J1 Y0 w9 B至于多个动态字符串拼接,应优先选择 join 或 format 方式。: u( e1 G/ s( r1 y
相比多次加法运算和多次内存分配 (字符串是不可变对象),join 这类函数 (方法) 可预先计算出总长度,一次性分配内存,随后直接拷贝内存数据填充。另一方面,将固定内容与变量分离的模版化 format,更易阅读和维护。
3 G* z2 p/ ]7 e: V5 y) D/ k>>> username = "qyuhen"
! R  C/ B4 I) P+ {. i>>> datetime = "2017010") p; h' g: r" d  K/ {7 N% w
>>> "/data/" + username + "/message/" + datetime + ".txt"
# d2 a3 a$ E5 ]: w, [5 R/data/qyuhen/message/20170101.txt" n6 [; Y, t) j: S- P+ v* `
>>> "/data/{user}/message/{time}.txt".format(user = username, time = datetime)
% F$ L6 k7 v2 O: O1 W% c- F) H8 Q/data/qyuhen/message/20170101.txt. }/ s  _- z  W( S" J8 w4 t2 X
我们用 line_profiler 对比用加法和 join 拼接 26 个大写字母的性能差异。虽然该测试不具备代表性,但可以提供一个粗略的验证方法。& C; R- o4 p: ]6 R
#!/usr/bin/env python3
  u7 F3 Z' N- G: l3 K# z# ~' R/ ~import string. W( E+ g2 C7 U- Q
x = list(string.ascii_uppercase)
5 @6 y& T) |8 i4 |@profile# e0 }8 n4 `3 k( a& ^9 E
def test_add():1 T2 d) t' Y1 k; W+ ~# c& F
s = ""
! e& E% j4 e: x for c in x:
4 B$ q. h3 h* M8 ~) `2 c s += c
$ }% t1 A% j0 I$ J* e return s
: G% |/ ?) N9 Z: _+ d( p@profile' ^  ?; o3 s% s6 g: K' k% x* O
def test_join():
* \  W  Y' V0 w3 K3 s return "".join(x)* J5 Z6 c6 P% p! F* k: I
test_add()
( F) U7 L! s7 ?- [6 V: x7 }test_join()
& o$ g+ v0 q. S# y' i9 t输出:
. {/ P1 y4 k  P$ kernprof -l ./test.py && python -m line_profiler test.py.lprof1 E. ~1 {  h+ o9 {3 |
编写代码除保持简单外,还应具备良好的可阅读性。比如判断是否包含子串,in、not in 操作符就比 find 方法自然,更贴近日常阅读习惯。& {0 s' a- [( E: D9 A
>>> "py" in "python"$ \7 f# |9 A* g& X& C
True; {4 |2 J0 c! ?0 {
>>> "Py" not in "python"
* w+ a' ^- t* D) f2 Z( FTrue3 P* c: i% [, n3 A: |6 R
作为序列类型,可以使用索引序号访问字符串内容,单个字符或者某一个片段。支持负索引,也就是从尾部以 -1 开始(索引 0 表示正向第一个字符)。
* \8 ?4 @5 g& V% C# a>>> s = "0123456789"8 |, G2 L9 r9 o
>>> s[2]3 D) _: M5 `2 m- |9 M) h
2, m/ {9 _% }6 z
>>> s[-1]
* i( O" _1 E: [9 c4 [3 f/ b4 m91 y- X( n& F  n. O
>>> s[2:6]5 S5 M4 d! _7 z. f9 @6 s  T2 v
23454 M9 \& ~$ ?1 g& l
>>> s[2:-2]* F( s/ _# [4 ?
234567- m2 C1 K% m1 H$ q3 U: @
使用两个索引号表示一个序列片段的语法称作切片(slice),可以此返回字符串子串。但无论以哪种方式返回与原字符串内容不同的子串时,都会重新分配内存,并复制数据。不像某些语言那样,仍旧以指针引用原字符串内容缓冲区。" Q6 n: d* }& M( P6 s
先看相同或不同内容时,字符串对象构建情形。- G! }7 j* w+ E8 e
>>> s = "-" * 1024
/ T3 ~0 A2 A% k. u>>> s1 = s[10:100] # 片段,内容不同。5 W. \/ s6 t+ \7 e* i& g
>>> s2 = s[:] # 内容相同9 Q& J. h; r5 @
>>> s3 = s.split(",")[0] # 内容相同。
; P* U1 c/ T" q9 V% @>>> s1 is s # 内容不同,构建新对象。5 c% C( O+ I; ?* ?/ ]" z+ J: l" d
False
6 ^& s, d5 ]& j5 F3 N0 ^>>> s2 is s # 内容相同时,直接引用原字符串对象。; S6 T( U# W0 V2 j! a
True
* D# S' W9 t; a) K$ D- ~. ]>>> s3 is s" C8 e7 |- o8 x0 ?) |2 `# g
True0 X$ _; P. R9 {% i( @& K4 A
再进一步用 memory_profiler 观察内存分配情况。
: r  j6 ]) E: U8 J@profile6 v4 A: p# e* L2 z4 C* J% y
def test():, ?8 p/ H; q6 J% H7 L. o  I7 C
a = x[10:-10]% D& }2 F1 }* J- ~
b = x.split(",")
" g( P3 T. V' h) g0 n return a, b
6 {) e# A, t# h" T( c0 o; ex = "0," * (1 << 20)1 s9 w0 m+ R, i8 Q
test()
' d; k, ]* {0 M) e8 R! d输出
0 U- A2 [, ^7 A: V8 \$ python -m memory_profiler ./test.py
* R7 w* H1 Q; \! {  h; e$ H7 \
此类行为,与具体的 Python 实现版本有关,不能一概而论。
字符串类型内置丰富的处理方法,可满足大多数操作需要。对于更复杂的文本处理,还可使用正则表达式(re)或专业的第三方库,比如 NLTK、TextBlob 等。
% i! j8 D+ P/ o) W; e# n7 o' u转换& P2 w! {$ X3 n8 {( \
除去与数字、Unicode 码点的转换外,最常见的是在不同编码间进行转换。  ~+ _. v) ]& r  Z! J* X3 i
Python 3 使用 bytes、bytearray 存储字节数组,不再和 str 混用。
>>> s = "汉字"" U; I0 ?* `& `6 N, X$ i! Z
>>> b = s.encode("utf-16") # to bytes
: @% I! k1 p% W* s: }>>> b.decode("utf-16") # to unicode string
$ W. j1 |' K2 O' e/ E- T9 q汉字: o  D( v1 q5 t3 ^
如要处理 BOM 信息,可导入 codecs 模块。
5 v) c$ [+ U- `; E>>> s = "汉字"* @6 Q# D( B4 w# @
>>> s.encode("utf-16").hex()+ K  D5 l( C# y: V# N8 l* [; g2 g* }
fffe496c575b
9 |- Y3 b3 m7 t: b1 e* y>>> codecs.BOM_UTF16_LE.hex() # BOM 标志。7 o( V* z/ _( p, J2 E
fffe& e5 \; ]2 i  J; n3 j& f# ^
>>> codecs.encode(s, "utf-16be").hex() # 按指定 BOM 转换。
  W* Z  ], ]* [, N+ N5 n6c495b57
' P, K" Q+ k4 L/ Z% Q>>> codecs.encode(s, "utf-16le").hex()# ~, u! ^; D+ s! ~" ]
496c575b
9 Y/ i4 C3 \6 V  v还有,Python 3 默认编码不再是 ASCII,所以无需额外设置。
6 l" w7 d/ {" L2 I* {Python 3.6
+ o9 m; f) H' p+ q& g8 `/ Z/ m. E>>> sys.getdefaultencoding()
# S: D* m- f) r5 `0 o" Gutf-8
1 o3 F2 }0 ?" u$ `* g$ A7 {Python 2.7
; ^, V+ A( w1 f$ J$ A  v>>> import sys
6 f2 F3 ?( ]# |! }# D& T>>> reload(sys)" q$ H9 y7 l) h( |
>>> sys.setdefaultencoding("utf-8")+ k, L1 w$ h" E' k; B1 h
>>> b = s.encode("utf-16")
, i; ]) Z6 ?3 l# U; s>>> b.decode("utf-16")
3 c' f! M8 |) r$ ^* ?/ Pu'\u6c49\u5b57'0 D2 I9 |' Y# j2 B
>>> type(b)( A5 X' p" d0 ^# x. N/ K, ?: E
<type 'str'>
! n2 B* R4 d. z0 f9 \% r. M8 ~: @格式化$ p5 x+ @* s7 x& m3 }! }" r
长期发展下来,Python 累积了多种字符串格式化方式。相比古老的面孔,人们更喜欢或倾向于使用新的特征。  K  a- w1 b+ J  S( @% V' i6 y
Python 3.6 新增了 f-strings 支持,这在很多脚本语言里属于标配。$ c; r! m3 }7 |( @9 a) A
使用 f 前缀标志,解释器解析大括号内的字段或表达式,从上下文名字空间查找同名对象进行值替换。格式化控制依旧遵循 format 规范,但阅读体验上更加完整和简洁。
7 t  M) `1 H4 v5 X>>> x = 106 l/ q% \; F5 M4 H
>>> y = 204 Z1 F- ?- y( x- W2 C: z4 K
>>> f"{x} + {y} = {x + y}" # f-strings9 r+ D/ J4 |# n* ?% n
10 + 20 = 30
. c2 i: i+ ?, {6 g2 r5 v>>> "{} + {} = {}".format(x, y , x + y)
, l' ~9 k7 \9 d* `. `7 s10 + 20 = 30
7 K4 i% Q1 c$ q表达式除运算符外,还可以是函数调用。
1 \8 r7 d; {1 [4 F8 l. M3 e5 [>>> f"{type(x)}"$ Z; ]% t, {3 J$ U4 R
<class 'int'>2 f: N# X0 f, ?/ N7 }
完整 format 格式化以位置序号、字段名匹配替换值参数,允许对其施加包括对齐、填充、 精度等控制。从某种角度看,f-strings 有点像是 format 的增强语法糖。, @/ F7 F' P" {% Q. I! \/ j

1 `8 Y! t' f0 u. a* J6 t1 O将两者进行对比,f-strings 类模版方式更加灵活,一定程度上将输出样式与数据来源分离。 但其缺点是与上下文名字耦合,导致模版内容与代码必须保持同步修改。而 format 的序号与主键匹配方式可避开这点,只可惜它不支持表达式。3 n! L& H$ u* W' Y, H1 H' V  ]
另外,对于简短的格式化处理,format 拥有更好的性能。
1 J/ e/ @  }" ]- t) _+ |手工序号和自动序号
+ J" l  D9 R" ]! S. H+ u>>> "{0} {1} {0}".format("a", 10)
  i: ^' n( l) @0 n" Aa 10 a% Z1 P5 ~$ i( y! q4 o5 U" K+ ]# f& ~) V
>>> "{} {}".format(1, 2) # 自动序号,不能与手工序号混用。9 Z5 m; c* U8 e7 g
1 2
  E! s% J! o0 b6 ^6 Q主键
/ j2 J4 `  ~( k; c- ]" b" V>>> "{x} {y}".format(x = 100, y = [1,2,3])
/ z! i/ u! L, o! h100 [1, 2, 3]
& ?" K3 t: m% m4 d) ^# l! _( E属性和索引: u+ ?5 w" \4 x+ s8 L
>>> x.name = "jack"
  z8 T- o7 \* ~- C) }& o>>> "{0.name}".format(x) # 对象属性。) m- u( p, h( ]& b! U  _" u( \
jack
; @/ W3 s) S: R; ~" k>>> "{0[2]}".format([1,2,3,4]) # 索引。
* j" G% u+ ]7 p; ~  Q3
# r* q1 Q* {% U; d, u宽度、补位
- g, K8 h( O# H9 O' B3 p- ?3 J>>> "{0:#08b}".format(5)
/ R) C: O7 L6 i& H0 z8 r+ R7 S0b000101
: f& D$ h. T9 I9 Q  w数字2 g' h. G$ D. S4 g  s! w# l
>>> "{:06.2f}".format(1.234) # 保留 2 位小数。
! z. A8 Z: Q1 e) f001.235 ]; |2 ~/ Q5 Z: q0 e2 u
>>> "{:,}".format(123456789) # 千分位。6 X( C3 i4 u$ w. a" q$ T' o* g% p
123,456,789
: `, _. @  o, L- Q对齐8 H) c, F: G& @) x  @- n' ^
>>> "[{:^10}]".format("abc") # 居中
* @% D7 o3 n: e3 h8 R6 H[ abc ]! T- W$ z9 l( i  @! T: j
>>> "[{:.<10}]".format("abc") # 左对齐,以点填充。& m: h! l- E  u, d+ G
[abc.......]
8 h8 m7 a- l; Z" t5 N, L: x古老的 printf 百分号格式化方式已被官方标记为 “obsolete”,加上其自身固有的一些问题,可能会被后续版本抛弃,不建议使用。另外,标准库里 string.Template 功能弱,且性能也差,同样不建议使用。
3 A6 O/ D1 m) F4 p, p" d/ [池化
1 \" x! {; y5 ?! w9 ?字符串算是进程里实例数量较多的类型之一,因为无处不在的名字就是字符串实例。5 o* H. j: S5 q4 b. _) i
鉴于相同名字会重复出现在各种名字空间里,那么有必要让它们共享对象。内容相同,且不可变,共享不会导致任何问题。关键是可节约内存,且省去创建新实例的调用开销。
" I. y8 a7 ~* ]: ]! u' I对此,Python 的做法是实现一个字符串池(intern)。池负责管理实例,使用者只需引用即可。另一潜在好处是,从池返回的字符串,只需比较指针就可知道内容是否相同,无需额外计算。可用来提升哈希表等类似结构的查找性能。3 ?( z3 a& ~' ]) |9 f# I
>>> "__name__" is sys.intern("__name__")
0 G5 S7 @/ c; O. |True$ P" O# {) n( |4 @* ]( L/ ^
除了以常量方式出现的名字和字面量外,动态生成字符串一样可加入池中。如此可保证每 次都引用同一对象,不会有额外的创建和分配操作。
) W( M4 x# j/ j* O% v>>> a = "hello, world!"& z( l. q  W$ |7 m- \; ^
>>> b = "hello, world!"
$ b( ?0 v1 o5 B) H: ]( U) y>>> a is b # 不同实例。9 I, ~5 C: ]0 h* d
False
+ O  D" |7 k) W3 `/ j& L>>> sys.intern(a) is sys.intern("hello, world!") # 相同实例。8 ^( G% r6 W9 E7 V# {- g5 u
True
7 N0 a1 ^* h2 G) c当然,一旦失去所有外部引用,池内字符串对象会被回收。
; _' M2 L+ e: [" V0 T>>> a = sys.intern("hello, world!")+ w" M1 d$ `! @  d1 ^1 g* C
>>> id(a)
% u$ c% ^8 y0 {+ m" x5 d44018790249 L9 ?+ g; D( Q1 e  \  F2 n, z
>>> id(sys.intern("hello, world!")) # 有外部引用。
8 T, f/ i" e! k6 j44018790248 w4 A" l6 R/ Y/ d: C1 h6 q
>>> del a # 删除外部引用后被回收。% k1 B' U" A: |! p
>>> id(sys.intern("hello, world!")) # 从 id 值不同可以看到新建,入池。
7 C) Z  l( ]& q1 G" E$ X! T3 n8 Y4405219056* {9 Z# u. _: k: e& 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.|网站地图