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

[复制链接]
sosoyoyo 发表于 2017-12-31 09:40:20 | 显示全部楼层 |阅读模式 打印 上一主题 下一主题
字符串
7 i; |- @8 y& ]! @  i字符串 (str) 存储 Unicode 文本,是不可变序列类型。相比 Python 2 里的混乱,Python 3 总算顺应时代发展,将文本和二进制彻底分离。8 L8 Y: v, }  n
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 = "汉字"
, r& u/ e& G. h" K( [>>> len(s)% b& @1 n% n' A' I0 i
2) {; D' J: ?) j0 ?  I
>>> hex(ord("汉")) # code point4 E( Q- P3 B3 X* g, i
0x6c49
, A( ^  L% S; |5 p9 r+ Y>>> chr(0x6c49)/ N! P, ?* m4 C" m( C, q! ?9 V( D: y
. N) Y- g- R! A3 s6 |
>>> ascii("汉字") # 对 non-ASCII 进行转义。
/ z- h+ `! F. ?3 X1 z' f' t\u6c49\u5b57
: s; d- e8 b; t; g: e( e8 a字符串字面量(literal)以成对单引号、双引号,或跨行三引号语法构成,自动合并相邻字面量。支持转义、八进制、十六进制,或 Unicode 格式字符。3 s. d" S/ i6 W! w" h+ `& A
用单引号还是双引号,并没有什么特殊限制。如果文本内引用文字使用双引号,那么外面用单引号可避免转义,更易阅读。通常情况下,建议遵循多数编程语言惯例,使用双引号标示。除去单引号在英文句法里的特殊用途外,它还常用来表示单个字符。
7 w& p. h: q6 q0 z>>> "h\x69, \u6C49\U00005B57"
- h0 _, b3 U$ ^) {+ Lhi, 汉字
- Z7 x* k; J: P# e8 u
注意:Unicode 格式大小写分别表示 16 位和 32 位整数,不能混用。
>>> "It's my life" # 英文缩写。1 t0 v8 |8 _$ P+ [& q$ N8 Q: C
>>> 'The report contained the "facts" of the case.' # 包含引文,避免使用 \" 转义。
. H7 P8 E9 a$ m6 l- A>>> "hello" ", " "world" # 合并多个相邻字量。/ U: F( P, H  P# Y
hello, world4 X4 c5 S: ~/ G' ^
>>> """ # 换行符、前导空格、空行都是组成内容。
& V" f9 F# t( |6 s3 b  W The Zen of Python, by Tim Peters 3 U/ N. ^: B  R' t. k6 f+ [% h
Beautiful is better than ugly.
7 y; a  g7 X5 d5 ^/ f& Q Explicit is better than implicit.
. E' U% B8 ]/ L9 q( ?6 G( H2 `! T  j5 [ Simple is better than complex.8 G( G3 ~/ [" U# g1 l- i
"""
* X% R+ U% f6 y, Q3 g* E可在字面量前添加标志,指示构建特定格式字符串。
$ _- t1 O; ^6 R' V最常用的原始字符串(r, raw string),它将反斜线视作字符内容,而非转义标志。这在构建类似 Windows 路径、正则表达式匹配模式 (pattern) 之类的文法字符串时很有用。3 ^2 a* m6 G0 z/ H* @, ]
>>> open(r"c:\windows\readme.txt") # Windows 路径。
! `( y- Z* v9 x4 \>>> re.findall(r"\b\d+\b", "a10 100") # 正则表达式。
; Z: B  P$ W) M- Y['100']* V8 @6 N) J- r8 {
>>> type(u"abc") # 默认 str 就是 unicode, 无需添加 u 前缀。; N6 g( p$ k$ `6 C1 h9 T1 K0 d- F
str
& Z! t) n$ U. G, j1 w  m/ T>>> type(b"abc") # 构建字节数组。2 n; i7 I, p2 ^/ n. `7 f
bytes9 ~# R* c* D5 _, G8 l- Q
操作
, \( T! h3 p/ G8 c) h; B# P支持用加法和乘法运算符拼接字符串。' g. G( v6 A( v$ j# f" {1 `7 h
>>> s = "hello"8 |: x/ B  q% u( D& f
>>> s += ", world"
& z6 t& I2 Y, h8 m( |) P>>> "-" * 10/ e+ d4 j5 x+ k: s' j
----------
9 S+ n5 T5 H. q- i编译器会尝试在编译期直接计算出字面量拼接结果,避免运行时开销。不过此类优化程度有限,并不总是有效。8 B+ g' |7 J2 Q' A. O0 d& ^/ W
>>> def test():
- g0 l4 o9 O4 W( _0 @; } a = "x" + "y" + "z"
) [4 z1 f" g! f, h b = "a" * 104 O+ X( k8 F7 `" p6 F
return a, b
; `5 L1 M% O. K. u>>> dis.dis(test)
; D! W2 Q# M8 s9 t 2 0 LOAD_CONST 7 ('xyz') # 直接给出结果,省略加法运算。0 Y% o& k4 V; j5 A1 C9 f% p, l+ F
3 4 LOAD_CONST 8 ('aaaaaaaaaa') # 省略乘法运算。
) l" @9 _3 x' U, v至于多个动态字符串拼接,应优先选择 join 或 format 方式。
1 ^# X9 q. T# K相比多次加法运算和多次内存分配 (字符串是不可变对象),join 这类函数 (方法) 可预先计算出总长度,一次性分配内存,随后直接拷贝内存数据填充。另一方面,将固定内容与变量分离的模版化 format,更易阅读和维护。# X( r* g; D2 L9 B; F; l
>>> username = "qyuhen"
3 g2 s  m7 a9 z0 K  r4 q+ B>>> datetime = "2017010"/ t. b0 Z4 o; ?
>>> "/data/" + username + "/message/" + datetime + ".txt"( m# V: }9 r6 n2 E  i! s9 a& _& T
/data/qyuhen/message/20170101.txt+ T" |+ M, q+ y5 I' ^$ e6 s
>>> "/data/{user}/message/{time}.txt".format(user = username, time = datetime)3 b  L3 N6 d: N. `# m1 S+ g1 \6 P% \( r
/data/qyuhen/message/20170101.txt
, {% O- P1 j! v8 O' ~; J我们用 line_profiler 对比用加法和 join 拼接 26 个大写字母的性能差异。虽然该测试不具备代表性,但可以提供一个粗略的验证方法。
4 {1 j' g% r/ [5 s#!/usr/bin/env python3+ J( I6 d( g2 y) _( g
import string; K1 t- q3 G* F
x = list(string.ascii_uppercase)
5 D/ p( J; }2 }/ y: C. w@profile
3 b1 x0 p! j. p0 J3 Hdef test_add():
3 j! C; O! O( L: F0 k8 A s = ""- k2 ]. ]; T% l
for c in x:
% H0 a( }, E1 Q3 M! G+ h- r6 R s += c
2 j/ l3 s# x8 z& C) j" s- S( j2 l return s
/ U9 G: {; f( i+ H7 i$ @/ ]9 G@profile/ @7 w7 Z- Y  j+ {7 v
def test_join():+ o6 h+ o* h7 C1 L7 x. M
return "".join(x)
8 w1 s% S. m; ltest_add()
2 q8 Y, o0 e7 a9 j0 Itest_join()
; Z3 O1 K7 c* r5 Z) e5 y' Y0 y输出:
# T$ Y4 J& a9 A" t5 n* l7 ~$ kernprof -l ./test.py && python -m line_profiler test.py.lprof
* H9 k# P! J3 ^
编写代码除保持简单外,还应具备良好的可阅读性。比如判断是否包含子串,in、not in 操作符就比 find 方法自然,更贴近日常阅读习惯。
- t9 K9 g8 D5 _. W* A
>>> "py" in "python"+ m. W; q) o" S6 C6 A1 x; N
True
& C" g; Z. J# l# R( I2 h% [>>> "Py" not in "python"
' C# i% r+ N) S, |; \True; `( x( h0 m, h9 ?4 J, W" p: c
作为序列类型,可以使用索引序号访问字符串内容,单个字符或者某一个片段。支持负索引,也就是从尾部以 -1 开始(索引 0 表示正向第一个字符)。2 f6 S, X8 D+ m  d7 O
>>> s = "0123456789"
7 C" R- C- Z: v, D6 ^1 i, l+ l0 y8 q' a>>> s[2]
* a9 O/ ~/ |! |! n% q- \0 C24 Y# U+ f$ G  q3 ~
>>> s[-1]7 }" T' m5 \" d2 a
96 ^; L/ x  P, k: M
>>> s[2:6]
; r5 h7 J0 E3 Y5 S9 O" v2345
& G) n' v) E  P>>> s[2:-2]& [% S$ p/ z: o+ V& q  P
234567
/ B* u' s' v' ~! g6 o使用两个索引号表示一个序列片段的语法称作切片(slice),可以此返回字符串子串。但无论以哪种方式返回与原字符串内容不同的子串时,都会重新分配内存,并复制数据。不像某些语言那样,仍旧以指针引用原字符串内容缓冲区。
3 B1 J% i- _. c& `1 m6 b- B先看相同或不同内容时,字符串对象构建情形。
/ z) _9 M6 C0 M; Y, _. s6 G! R) m4 k>>> s = "-" * 1024
2 P  s' H6 k! c5 x8 B>>> s1 = s[10:100] # 片段,内容不同。6 E+ A; B# m6 b2 l  n$ V6 G; A
>>> s2 = s[:] # 内容相同
& h7 ?, u! S$ I9 z7 g>>> s3 = s.split(",")[0] # 内容相同。) \1 O' ?/ I- \: C  g9 x5 [
>>> s1 is s # 内容不同,构建新对象。
. ?- g# c/ w. ]" |& wFalse9 @" N5 S) }9 ]' D
>>> s2 is s # 内容相同时,直接引用原字符串对象。" L# z% C" @5 m( G$ f" n) f& r
True/ X. U  u3 o+ l0 i
>>> s3 is s
9 F! a9 Q' u1 O9 c9 S0 e, GTrue
, L9 K5 W8 V' M- H4 x1 ^$ Y再进一步用 memory_profiler 观察内存分配情况。
3 W# f4 k: c* k1 M. W0 W# B@profile* e3 G( A. }: V0 n  c
def test():/ k% U* A) ]9 ^0 y9 w5 U) x
a = x[10:-10]
3 D7 ]$ C/ _% F b = x.split(",")
, K: m* f) d1 k# f9 n return a, b6 g1 x% y' @% ^* k: @5 M7 L( e! r$ K7 {$ P
x = "0," * (1 << 20)
6 @% q6 u3 J* Z1 F; |9 `3 `/ qtest()
+ j7 ?+ Y* V) n0 x输出
/ [, _8 v2 L. \4 `. V( H, ]$ python -m memory_profiler ./test.py
$ f6 [+ K0 P5 F/ E' W# w
此类行为,与具体的 Python 实现版本有关,不能一概而论。
字符串类型内置丰富的处理方法,可满足大多数操作需要。对于更复杂的文本处理,还可使用正则表达式(re)或专业的第三方库,比如 NLTK、TextBlob 等。
" p3 m3 f) Z, E  E) D转换
. l5 b& ~, p& s1 i, f除去与数字、Unicode 码点的转换外,最常见的是在不同编码间进行转换。
/ k9 }& ~" y9 ]: s) g4 J3 b
Python 3 使用 bytes、bytearray 存储字节数组,不再和 str 混用。
>>> s = "汉字"
" f( L+ Y( @  k5 g0 j4 O9 A) X>>> b = s.encode("utf-16") # to bytes
9 t  f: b  R6 G0 G>>> b.decode("utf-16") # to unicode string
* E! I$ U) E0 `1 s1 P; v3 M汉字
% s% k0 _: C' k2 Q- l如要处理 BOM 信息,可导入 codecs 模块。
9 I& R  _" K  K8 S>>> s = "汉字"# G' V! `; F" a3 O3 t
>>> s.encode("utf-16").hex()5 j2 _3 t0 L% u
fffe496c575b
7 D& R2 w' J. q>>> codecs.BOM_UTF16_LE.hex() # BOM 标志。
" M0 a0 F% a) B4 g$ Z( afffe
( e$ e/ D- a: E1 ~>>> codecs.encode(s, "utf-16be").hex() # 按指定 BOM 转换。
4 N% z# |3 H% W3 F) N6c495b57
+ c. [' a8 L  y3 r% o, q>>> codecs.encode(s, "utf-16le").hex()! H* {- C( y& [) i- B
496c575b
/ G. B3 x9 O# a6 w" V$ p4 H  ^( k( }# r还有,Python 3 默认编码不再是 ASCII,所以无需额外设置。- `4 [7 N3 E$ Z% t0 s+ x
Python 3.6
6 S- Y' P' J* X) v- L0 L' K$ P2 W/ H>>> sys.getdefaultencoding()
/ G+ w; c$ a9 {; t" z: {utf-8# b7 D1 ]/ h" {6 i3 P; d/ X
Python 2.7
! d9 V  i* j( a# m: E) [: y>>> import sys$ l: j0 m' a6 d
>>> reload(sys); X! [2 ^, t7 m# H0 K  a6 R
>>> sys.setdefaultencoding("utf-8")" |3 @2 V9 m& I4 ]- \( W
>>> b = s.encode("utf-16")
8 F$ }4 l" L7 `4 \# ^% y! e2 d>>> b.decode("utf-16")
3 ^3 k( I, e5 R3 d/ \' yu'\u6c49\u5b57'3 Y. W) y; b; j( \% f5 n
>>> type(b)
& i) b! a3 ~2 W$ v* _7 Q<type 'str'>
2 i% W! q8 ~. J) `6 }* W# ^格式化
8 J. E+ _4 O; c8 j* A( i长期发展下来,Python 累积了多种字符串格式化方式。相比古老的面孔,人们更喜欢或倾向于使用新的特征。$ G0 _! ]8 ]  d5 x3 i
Python 3.6 新增了 f-strings 支持,这在很多脚本语言里属于标配。
( t0 P# @  z' G. k, K( c% _! x# _使用 f 前缀标志,解释器解析大括号内的字段或表达式,从上下文名字空间查找同名对象进行值替换。格式化控制依旧遵循 format 规范,但阅读体验上更加完整和简洁。
9 G1 ~- ?# K9 N1 ^6 s" z>>> x = 10$ d+ a$ _$ E) I+ ~
>>> y = 20
6 j9 }+ b+ _. P4 y5 O>>> f"{x} + {y} = {x + y}" # f-strings* t7 n: w, H$ d+ q
10 + 20 = 303 Q, ^' o% h+ R
>>> "{} + {} = {}".format(x, y , x + y). @# [) A( l' }' b3 Y3 I
10 + 20 = 30
- Y9 G% E6 d- W8 P$ |# }( x表达式除运算符外,还可以是函数调用。0 x1 F. x- Y3 w) C3 q4 j
>>> f"{type(x)}"( f$ n( }' H  M* u; e5 o: \
<class 'int'>
$ k$ q, Q  m. M) w4 w- u完整 format 格式化以位置序号、字段名匹配替换值参数,允许对其施加包括对齐、填充、 精度等控制。从某种角度看,f-strings 有点像是 format 的增强语法糖。
! b; U. }& @$ v3 c" A
- l( n3 E/ g- D: `将两者进行对比,f-strings 类模版方式更加灵活,一定程度上将输出样式与数据来源分离。 但其缺点是与上下文名字耦合,导致模版内容与代码必须保持同步修改。而 format 的序号与主键匹配方式可避开这点,只可惜它不支持表达式。
0 A' g* f) L. B! Y$ ?( G* \- s另外,对于简短的格式化处理,format 拥有更好的性能。
% ]4 H, o) O' Z" e6 r* {手工序号和自动序号
6 e2 r+ B- x* H' a3 S; i" J>>> "{0} {1} {0}".format("a", 10); c- ], _5 k: r2 X$ O( N
a 10 a
( w, b7 C  w7 S  U3 T6 _>>> "{} {}".format(1, 2) # 自动序号,不能与手工序号混用。
' g9 H0 m* N5 w* q6 C9 s1 20 x- T: u% Y( P6 J/ F9 x6 Q& c
主键
) u: e4 N: m4 A+ O>>> "{x} {y}".format(x = 100, y = [1,2,3])8 t/ C- P8 T& N/ [/ J- R
100 [1, 2, 3]2 r/ Q4 M6 ~3 ^6 D  q( k! Z
属性和索引
! O0 P( _8 s! S! K7 Y( \* j8 W>>> x.name = "jack"1 Z  v  l( a* f/ E/ \
>>> "{0.name}".format(x) # 对象属性。
: F2 E9 `+ r# I7 {1 m* q6 ]8 _jack/ ]7 r% X/ X. N4 x6 l& `$ W' n
>>> "{0[2]}".format([1,2,3,4]) # 索引。7 u+ o* D! r) N$ v
3
0 Z  q% A/ e% Q) v2 q$ s宽度、补位
" r, f  p4 R0 n9 g>>> "{0:#08b}".format(5)
8 W, a3 E5 M; q% I  P$ z0b000101
' }* \) k2 c9 E" O) }" F! _数字
0 ]- j: v- C+ Z" @6 k: E% B6 r" f>>> "{:06.2f}".format(1.234) # 保留 2 位小数。
6 c: m9 D2 y5 Q; S; ?2 }8 ^3 X001.23% P# ^# |2 E; ~& p
>>> "{:,}".format(123456789) # 千分位。
+ `& ~8 H3 d2 p! i9 ^123,456,789
/ q$ M5 {! Q: ~6 g对齐% r" }7 _9 l& P4 S. v
>>> "[{:^10}]".format("abc") # 居中) l% ^$ |! t6 d; b
[ abc ]
, [: o, j2 D0 P/ B" j' y>>> "[{:.<10}]".format("abc") # 左对齐,以点填充。
( {' w; n; f7 I# G0 l! j& T[abc.......]4 D; Y/ H. S0 S+ [" p7 J( d- i9 M
古老的 printf 百分号格式化方式已被官方标记为 “obsolete”,加上其自身固有的一些问题,可能会被后续版本抛弃,不建议使用。另外,标准库里 string.Template 功能弱,且性能也差,同样不建议使用。
' F6 S7 O/ B9 J) Q9 s& t池化
" T0 l9 r( d% {字符串算是进程里实例数量较多的类型之一,因为无处不在的名字就是字符串实例。$ D9 J' R8 J' v+ W6 @7 B0 V  v
鉴于相同名字会重复出现在各种名字空间里,那么有必要让它们共享对象。内容相同,且不可变,共享不会导致任何问题。关键是可节约内存,且省去创建新实例的调用开销。
: d1 t0 G+ A, P: _对此,Python 的做法是实现一个字符串池(intern)。池负责管理实例,使用者只需引用即可。另一潜在好处是,从池返回的字符串,只需比较指针就可知道内容是否相同,无需额外计算。可用来提升哈希表等类似结构的查找性能。
7 H2 P  P1 r$ E; [: P! I0 `  j% r>>> "__name__" is sys.intern("__name__")# }9 L4 S4 ^  |" J
True8 k4 X3 H, E, f; c5 {2 C
除了以常量方式出现的名字和字面量外,动态生成字符串一样可加入池中。如此可保证每 次都引用同一对象,不会有额外的创建和分配操作。# C$ w- t& u+ i5 j
>>> a = "hello, world!"
7 ]# t7 N! h4 L>>> b = "hello, world!"
& N1 T3 v8 [, ]& P>>> a is b # 不同实例。
& _2 d; N* X8 g( `, C$ V! ~False
; n6 A* ^5 h% E; G0 N1 _+ m+ \>>> sys.intern(a) is sys.intern("hello, world!") # 相同实例。
- Y* [/ e& i6 u  Z5 t# F0 O5 kTrue; E' @# J: I; W" G0 R7 s
当然,一旦失去所有外部引用,池内字符串对象会被回收。
, ?, \3 L- ?) {5 D# W% p1 w: v>>> a = sys.intern("hello, world!")
( f3 o$ X3 _( i8 U  M>>> id(a)4 O2 U' }) z* Y+ Z7 N
4401879024: P2 o' `7 P' p, P: U7 T
>>> id(sys.intern("hello, world!")) # 有外部引用。# e- f6 s5 h. |- k4 z* B3 ^0 K# ?/ R
4401879024" Z5 s) _, t. ^- W2 p8 c: X
>>> del a # 删除外部引用后被回收。' A5 N  _  G0 f8 h
>>> id(sys.intern("hello, world!")) # 从 id 值不同可以看到新建,入池。
% h0 W- H  q0 y$ Q5 S4405219056
% g3 u4 N" W: `5 }4 T1 y
字符串池实现算法很简单,就是简单的字典结构。 详情参考 Objects/unicodeobject.c : PyUnicode_InternInPlace。
做大数据处理时,可能需创建海量主键,使用 intern 有助于减少对象数量,节约大量内存。

本帖子中包含更多资源

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

x
回复

使用道具 举报

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

本版积分规则

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

扫一扫关注我们

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