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

[复制链接]
sosoyoyo 发表于 2017-12-31 09:40:20 | 显示全部楼层 |阅读模式 打印 上一主题 下一主题
字符串/ r4 P3 n2 J) ^) s, j/ Z
字符串 (str) 存储 Unicode 文本,是不可变序列类型。相比 Python 2 里的混乱,Python 3 总算顺应时代发展,将文本和二进制彻底分离。
8 X+ b$ J7 x0 \  E% Q) h2 {; x
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) Q0 ~2 V( l7 r) R5 @7 P9 }2 G
>>> len(s)# r% ]# ]' j) E" p& V
2
: b- I3 a3 |8 j* ]( N>>> hex(ord("汉")) # code point
0 N% N0 i0 G, T7 l( H4 K' O0x6c49. e. e6 q3 J0 h' U: S
>>> chr(0x6c49)
$ q4 z: p  j6 m4 i7 b
& y: t- z2 `* k( q# E2 c1 W: H>>> ascii("汉字") # 对 non-ASCII 进行转义。* `# w& t: ]0 t4 ^9 `9 w
\u6c49\u5b57
( h$ u( e$ b$ B: q+ T字符串字面量(literal)以成对单引号、双引号,或跨行三引号语法构成,自动合并相邻字面量。支持转义、八进制、十六进制,或 Unicode 格式字符。+ U9 v5 |. L' q0 V8 B( z
用单引号还是双引号,并没有什么特殊限制。如果文本内引用文字使用双引号,那么外面用单引号可避免转义,更易阅读。通常情况下,建议遵循多数编程语言惯例,使用双引号标示。除去单引号在英文句法里的特殊用途外,它还常用来表示单个字符。  e4 \1 P6 Y; J* u6 Q
>>> "h\x69, \u6C49\U00005B57"; r; r) n2 p3 S3 j
hi, 汉字
( L0 o1 `, x" A2 T
注意:Unicode 格式大小写分别表示 16 位和 32 位整数,不能混用。
>>> "It's my life" # 英文缩写。
( w/ _, D) k+ S& l! f>>> 'The report contained the "facts" of the case.' # 包含引文,避免使用 \" 转义。
$ d  _' D+ w* m2 z4 t8 p>>> "hello" ", " "world" # 合并多个相邻字量。
) b/ @) l) m: zhello, world6 y! M3 q. B% h( j6 v# G
>>> """ # 换行符、前导空格、空行都是组成内容。
- s3 e! Q, |0 N The Zen of Python, by Tim Peters
7 l5 A/ }: i. H* a Beautiful is better than ugly. " Z, n' T5 f- F
Explicit is better than implicit.
* r/ E8 k0 U0 e3 C: c Simple is better than complex.6 e7 u$ l1 v* X1 A" Y# s
"""
0 f0 \; m& n) `1 c$ q3 S, q可在字面量前添加标志,指示构建特定格式字符串。: Y& }1 z, {0 ?8 i& Q
最常用的原始字符串(r, raw string),它将反斜线视作字符内容,而非转义标志。这在构建类似 Windows 路径、正则表达式匹配模式 (pattern) 之类的文法字符串时很有用。, O( G  J1 H) v/ p7 q0 u* w
>>> open(r"c:\windows\readme.txt") # Windows 路径。5 x5 s. {8 W/ v$ v2 T' C; w( M
>>> re.findall(r"\b\d+\b", "a10 100") # 正则表达式。
0 H/ p( t2 n+ s: G['100']# ]  x8 J" c9 O' ~& P' @0 a
>>> type(u"abc") # 默认 str 就是 unicode, 无需添加 u 前缀。! r; W/ n8 q7 t# _. d. I/ [
str
! X5 q) J/ N9 a2 x" m>>> type(b"abc") # 构建字节数组。4 G/ s2 ^  }9 N% G/ c6 |1 v) N
bytes
1 x0 W2 O6 P' x' R* x+ D! E+ m操作- ~' m! }& `; P
支持用加法和乘法运算符拼接字符串。+ f; D+ D( V4 Z! ?& Z
>>> s = "hello"
# R3 x  X. Y6 a. L>>> s += ", world"1 ?0 t" m' o- _3 T$ Y% Y5 D  O
>>> "-" * 10; Z) {3 e  r$ A. ^8 \
----------
& @2 f. T# O+ H& }编译器会尝试在编译期直接计算出字面量拼接结果,避免运行时开销。不过此类优化程度有限,并不总是有效。
" Y! G( V7 c, S! r0 j/ s" F) w>>> def test():
: H- i: W5 C$ d. Y" S6 u. K- \4 u; t a = "x" + "y" + "z"+ P) i0 P$ _+ b6 G6 M/ Y' P' }) g
b = "a" * 10
; G3 U% @5 [- c- K2 o# B# J7 a return a, b/ X' R. x' q5 Y$ T9 q- g* q
>>> dis.dis(test)& \7 _1 M/ v3 I1 G7 k! l
2 0 LOAD_CONST 7 ('xyz') # 直接给出结果,省略加法运算。
4 T( s/ i! \, t2 f! G; i( m 3 4 LOAD_CONST 8 ('aaaaaaaaaa') # 省略乘法运算。. |  E7 v% B+ O  I& b. r
至于多个动态字符串拼接,应优先选择 join 或 format 方式。
: p) i' c- Y! I0 G. M9 x相比多次加法运算和多次内存分配 (字符串是不可变对象),join 这类函数 (方法) 可预先计算出总长度,一次性分配内存,随后直接拷贝内存数据填充。另一方面,将固定内容与变量分离的模版化 format,更易阅读和维护。
* t8 ^: H! t5 r' y) i, ]>>> username = "qyuhen"
7 \3 J5 B( o5 f4 l; D- @5 J>>> datetime = "2017010": [3 z: r4 G5 U3 U* X
>>> "/data/" + username + "/message/" + datetime + ".txt"7 Y/ |" f* r+ h6 s- J9 M' z
/data/qyuhen/message/20170101.txt8 n7 }+ I0 k7 W5 U% J! f% w' X
>>> "/data/{user}/message/{time}.txt".format(user = username, time = datetime)
: o% ]8 X) Q# \/data/qyuhen/message/20170101.txt
$ `$ C1 G9 l' d8 J$ v我们用 line_profiler 对比用加法和 join 拼接 26 个大写字母的性能差异。虽然该测试不具备代表性,但可以提供一个粗略的验证方法。
4 D; p' M5 }: r0 P! P, b% S' }#!/usr/bin/env python35 W/ e, O2 V; G& u9 X: u4 `. i
import string0 N3 G. H3 I5 K  i
x = list(string.ascii_uppercase)
* F0 A, A  h3 k4 M5 _. _$ }% P9 v@profile
: U/ P- d3 c' @def test_add():# X+ m$ X2 Q: m+ I4 m" b& C5 M! Z
s = "". t. z& i3 N. u: h0 [8 s& h
for c in x:
" @/ S* ~/ H3 H s += c
( z- C  o  X5 o  m$ R" G return s
0 h$ r# q4 @$ b2 i% y( c@profile
% b9 J% L" S' f" V( F9 }3 jdef test_join():( p8 W+ o) e; q0 |
return "".join(x)2 V( h9 q0 X& a% x" J6 f
test_add()
0 f' @' |8 f. }0 w, @5 Qtest_join()$ f  {& j( s( Q3 |3 y
输出:
# w2 u$ F& L- j$ kernprof -l ./test.py && python -m line_profiler test.py.lprof
' M: b. I, X2 [: ]
编写代码除保持简单外,还应具备良好的可阅读性。比如判断是否包含子串,in、not in 操作符就比 find 方法自然,更贴近日常阅读习惯。, C9 ~* G& [8 B+ u' W
>>> "py" in "python"' E1 M: i9 O6 I& s
True
! P: ^1 l- F. s5 e8 s: S>>> "Py" not in "python"$ W5 h8 V0 Q% h- `4 ~9 A( l
True3 v+ i: M6 q' r
作为序列类型,可以使用索引序号访问字符串内容,单个字符或者某一个片段。支持负索引,也就是从尾部以 -1 开始(索引 0 表示正向第一个字符)。7 e7 p% H0 O% @
>>> s = "0123456789"
6 e5 L( G5 W/ k" o) @>>> s[2]
0 b' ^1 M0 p4 N, n& m( P2+ b* G6 }# N9 {4 V
>>> s[-1]' {! I8 }5 z! q! H2 E: L
9( F, S1 d! ^# {+ ?: O, }2 A7 ]
>>> s[2:6]
! N& L4 l* K5 `8 A/ v6 m2345- f2 Y$ u$ P4 B% ^+ r* l
>>> s[2:-2]
7 W; X# Y& t! y% i$ `1 U( y/ t234567
1 w, d' o0 i$ h$ s; V使用两个索引号表示一个序列片段的语法称作切片(slice),可以此返回字符串子串。但无论以哪种方式返回与原字符串内容不同的子串时,都会重新分配内存,并复制数据。不像某些语言那样,仍旧以指针引用原字符串内容缓冲区。; `* S: S$ F5 [
先看相同或不同内容时,字符串对象构建情形。/ a) K0 r3 g7 O3 v
>>> s = "-" * 10242 t% ?3 D6 ^1 T/ F! f) D
>>> s1 = s[10:100] # 片段,内容不同。
$ ?+ a) _& P1 U% e0 e% M3 j. ^: S>>> s2 = s[:] # 内容相同% N0 {5 X, k% p9 a/ G
>>> s3 = s.split(",")[0] # 内容相同。
/ ], V$ d6 x7 J0 g4 L>>> s1 is s # 内容不同,构建新对象。  c( c. u1 G# D! C. v
False$ ~" v7 G: A. v% _4 @' g
>>> s2 is s # 内容相同时,直接引用原字符串对象。9 \7 v9 ^$ j; N0 A2 x) d6 {+ y6 V
True( J$ W" H6 f8 Q% n9 C: h5 }
>>> s3 is s% _' j8 C: i, u2 T+ U
True% _! f, b: a. M0 n  j& d* N8 t2 i
再进一步用 memory_profiler 观察内存分配情况。6 \* m  w6 L, y6 q
@profile" r  I& a, l  z" [5 J) P
def test():
! o6 N2 B2 x; x7 m a = x[10:-10]* D6 P, h9 a$ d& n$ g1 W% _
b = x.split(",")
" T3 {" w1 I2 |: [ return a, b# u' L. @5 E+ t# x  |7 L) l) O
x = "0," * (1 << 20)5 [9 f) ~9 ?5 o' j' }
test()
3 J: h: ~; G) _: f2 M. y! ~6 r4 e* D输出6 M% ^9 {( c( ~# o
$ python -m memory_profiler ./test.py4 ~, }, O0 C( _' z. s
此类行为,与具体的 Python 实现版本有关,不能一概而论。
字符串类型内置丰富的处理方法,可满足大多数操作需要。对于更复杂的文本处理,还可使用正则表达式(re)或专业的第三方库,比如 NLTK、TextBlob 等。% D* G& a' C& v  J3 _& x# z$ `6 {$ T
转换3 |  k/ s. Z* L% [! t
除去与数字、Unicode 码点的转换外,最常见的是在不同编码间进行转换。
1 {' C6 u  ~- b
Python 3 使用 bytes、bytearray 存储字节数组,不再和 str 混用。
>>> s = "汉字"
8 D! C3 \7 k' j* Q% N- ], h( V>>> b = s.encode("utf-16") # to bytes5 C; u& X0 a- s1 ?3 u
>>> b.decode("utf-16") # to unicode string
- j( w: ^9 _6 k7 `) }+ y: r汉字, L* g/ |. `& _$ L, C
如要处理 BOM 信息,可导入 codecs 模块。
! Q3 I7 B/ s" a& {9 x) @. e>>> s = "汉字"
4 s- U2 {3 m8 z>>> s.encode("utf-16").hex()0 }$ s' N  ]8 c
fffe496c575b. ?7 }6 a* p+ c( j& O
>>> codecs.BOM_UTF16_LE.hex() # BOM 标志。8 s5 c* a! O2 C. ~/ J
fffe
" `/ s: j/ W# [9 s>>> codecs.encode(s, "utf-16be").hex() # 按指定 BOM 转换。
' b& |1 H3 w3 G6 {, W6c495b57
1 @! v# N2 d; X/ Q>>> codecs.encode(s, "utf-16le").hex()
0 K, r, U# t% q) ^4 H0 N496c575b* t& S6 @, V' S" q; ~( y$ ?
还有,Python 3 默认编码不再是 ASCII,所以无需额外设置。
9 r) T% C+ f% EPython 3.6
+ \* h6 `3 _1 g! K0 M>>> sys.getdefaultencoding()4 X/ [1 _* y% e8 A" P' Z/ D
utf-8% b- {+ n5 G2 t0 v" t9 I. Z
Python 2.7
. m- o# P% r! z8 v5 v+ s>>> import sys
6 e4 a2 ]1 n. Q& J' w# Z>>> reload(sys)9 K4 f; z) g3 G" I4 a
>>> sys.setdefaultencoding("utf-8")
* u% R" t) a' ^$ [>>> b = s.encode("utf-16")
" B* _2 b1 X3 K) o% _9 j# S>>> b.decode("utf-16")
  q+ C/ Y' D/ Vu'\u6c49\u5b57'( _: T: m) A+ U/ D) X! O
>>> type(b): @" P5 m6 J  R% ~/ a* U( y
<type 'str'>- o4 b8 a3 |; }
格式化
: z1 w3 c8 w; H9 E8 E& g  K; [/ t长期发展下来,Python 累积了多种字符串格式化方式。相比古老的面孔,人们更喜欢或倾向于使用新的特征。' f' I) X- d* K* G+ i6 b
Python 3.6 新增了 f-strings 支持,这在很多脚本语言里属于标配。: [9 v$ Z- {* O/ @- Z
使用 f 前缀标志,解释器解析大括号内的字段或表达式,从上下文名字空间查找同名对象进行值替换。格式化控制依旧遵循 format 规范,但阅读体验上更加完整和简洁。
; C/ J( E$ l) Z1 U4 F9 C; ]>>> x = 10' ~. B. w; K6 o7 o$ A! ^; N
>>> y = 20, T# ]1 @3 P$ Z; A
>>> f"{x} + {y} = {x + y}" # f-strings
- C. U; b$ d8 O8 W& r10 + 20 = 305 t' ~. }5 H9 c% v* m" r3 t
>>> "{} + {} = {}".format(x, y , x + y)
% N! D5 j( q6 r10 + 20 = 30
  v$ M1 h+ G8 W  p# q$ U表达式除运算符外,还可以是函数调用。
0 o' n8 f* C7 [" o- s>>> f"{type(x)}"
9 ]  y+ o, L: e  S0 e, s<class 'int'>8 F2 w: R; _! ]" R; J) G
完整 format 格式化以位置序号、字段名匹配替换值参数,允许对其施加包括对齐、填充、 精度等控制。从某种角度看,f-strings 有点像是 format 的增强语法糖。) E3 N& v; s" w: Q# f8 b

9 ^2 E; W) |- k& U$ ?2 V将两者进行对比,f-strings 类模版方式更加灵活,一定程度上将输出样式与数据来源分离。 但其缺点是与上下文名字耦合,导致模版内容与代码必须保持同步修改。而 format 的序号与主键匹配方式可避开这点,只可惜它不支持表达式。% l" m7 V( V9 i2 b
另外,对于简短的格式化处理,format 拥有更好的性能。+ j4 L* m, f8 q. S
手工序号和自动序号
# T3 W' M# b+ a8 u& e>>> "{0} {1} {0}".format("a", 10)
9 D& c" O& I. V6 l4 Fa 10 a
! d* _3 |! @5 t, J# m4 @" N  r>>> "{} {}".format(1, 2) # 自动序号,不能与手工序号混用。! M+ ^* R# r5 Z5 ~" U8 {
1 2
% A# Q$ e% D: z4 `主键4 N3 Z: u5 Q0 i
>>> "{x} {y}".format(x = 100, y = [1,2,3])6 {2 n" D1 a* b
100 [1, 2, 3]. M! g$ N. a; K" F
属性和索引/ k* o: J8 t9 d+ `; A2 l8 b+ C2 Q
>>> x.name = "jack"
% n9 w# q# C; T! e% k) A' N>>> "{0.name}".format(x) # 对象属性。
! H, d4 V0 P' D4 r$ Rjack
) F& g+ y3 L) Q, V, \) m>>> "{0[2]}".format([1,2,3,4]) # 索引。
8 M& n0 M1 n7 Z4 p& i* H3
# N! y# p) F+ W& x/ j# W; p+ w8 k宽度、补位- O4 X$ V2 Q3 {- \7 A# x
>>> "{0:#08b}".format(5)$ W7 \* t6 E8 y, g/ Q$ P1 L
0b000101& i9 I8 l6 i' b  }* h0 c; C
数字' ~% e" }: ~: {: _1 R7 F0 w- o
>>> "{:06.2f}".format(1.234) # 保留 2 位小数。
' r  a9 A- S3 V001.23. z9 `5 P) J  U
>>> "{:,}".format(123456789) # 千分位。$ `( V7 j* _. R3 H; Z
123,456,789
3 A0 i0 h+ k& {! z$ d! q0 D. l对齐
# z' Q  U, W% i6 x$ u" |>>> "[{:^10}]".format("abc") # 居中
. @) m6 ~& {' W5 I: l3 [+ R[ abc ]; N# Q2 i6 t. A/ W, o  n+ f
>>> "[{:.<10}]".format("abc") # 左对齐,以点填充。
' f. `" g1 x4 V6 [[abc.......]# n. L1 h( {1 V' [+ S9 F0 {
古老的 printf 百分号格式化方式已被官方标记为 “obsolete”,加上其自身固有的一些问题,可能会被后续版本抛弃,不建议使用。另外,标准库里 string.Template 功能弱,且性能也差,同样不建议使用。" I) n2 B. E: _7 {, t
池化# X7 c3 j& N' u4 ~8 d
字符串算是进程里实例数量较多的类型之一,因为无处不在的名字就是字符串实例。$ l- {2 N7 v) W* ?0 i! a2 p
鉴于相同名字会重复出现在各种名字空间里,那么有必要让它们共享对象。内容相同,且不可变,共享不会导致任何问题。关键是可节约内存,且省去创建新实例的调用开销。
. V' E7 f  d/ u) X1 h, n9 |对此,Python 的做法是实现一个字符串池(intern)。池负责管理实例,使用者只需引用即可。另一潜在好处是,从池返回的字符串,只需比较指针就可知道内容是否相同,无需额外计算。可用来提升哈希表等类似结构的查找性能。
  \  ^: f, N! n+ U  ^) b9 p, q2 `>>> "__name__" is sys.intern("__name__")$ l1 S7 E* f  S$ ?5 ?7 A
True
; l2 \$ C" O$ G5 H9 n除了以常量方式出现的名字和字面量外,动态生成字符串一样可加入池中。如此可保证每 次都引用同一对象,不会有额外的创建和分配操作。/ ]7 H" x& x: J- N' ~  E
>>> a = "hello, world!"% Z" h* j- ]! J+ I4 M- q: {0 f
>>> b = "hello, world!"# v5 J7 Z* N3 A' ^1 S0 r9 f/ P
>>> a is b # 不同实例。1 Q  }3 t; `7 v
False0 G  u; ]. }- w- a2 O+ g
>>> sys.intern(a) is sys.intern("hello, world!") # 相同实例。6 X) h+ w) T% T3 ~: b8 o: f6 Q
True! ?6 n9 l5 A% F" o9 n( |  A
当然,一旦失去所有外部引用,池内字符串对象会被回收。/ ~( f9 E0 _7 |2 y
>>> a = sys.intern("hello, world!")4 K  J5 f$ f. l/ S9 `
>>> id(a)
0 ?8 S; O5 W: ~0 A% N4401879024
* x6 z* b, {5 P  v+ \- h>>> id(sys.intern("hello, world!")) # 有外部引用。, B7 k7 A2 k  ?
44018790245 `" |. v. @" {' B- ?  ~
>>> del a # 删除外部引用后被回收。
$ B6 f7 Y! n6 x2 g. s. G7 g, [1 B>>> id(sys.intern("hello, world!")) # 从 id 值不同可以看到新建,入池。- t4 ]6 D' n# F5 d3 a0 ?) g8 l* h
44052190565 Z. x+ G$ ^- l: A9 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.|网站地图