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

[复制链接]
sosoyoyo 发表于 2017-12-31 09:40:20 | 显示全部楼层 |阅读模式 打印 上一主题 下一主题
字符串7 C+ D- s) }* W9 b# \
字符串 (str) 存储 Unicode 文本,是不可变序列类型。相比 Python 2 里的混乱,Python 3 总算顺应时代发展,将文本和二进制彻底分离。
$ a. F( [( d6 L1 w% Y
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 = "汉字"
, J. T$ F1 j' j8 R# x' F: |>>> len(s)
7 Z+ H0 \5 u0 ^; P: O; m2
7 a7 T% t+ G( x8 k( c( ]+ s>>> hex(ord("汉")) # code point
0 j( W* t/ F/ @" b9 q0x6c490 G2 |0 u5 a3 C1 z! D
>>> chr(0x6c49)* C, v0 D. a" m4 K# w4 n8 t6 U+ E

* n: A0 `2 S1 |/ H>>> ascii("汉字") # 对 non-ASCII 进行转义。
  L: t& ~( p: p6 J- E: t\u6c49\u5b57$ J+ V' v, P9 S, P- X* c
字符串字面量(literal)以成对单引号、双引号,或跨行三引号语法构成,自动合并相邻字面量。支持转义、八进制、十六进制,或 Unicode 格式字符。: V0 S4 Q) Q' E, j
用单引号还是双引号,并没有什么特殊限制。如果文本内引用文字使用双引号,那么外面用单引号可避免转义,更易阅读。通常情况下,建议遵循多数编程语言惯例,使用双引号标示。除去单引号在英文句法里的特殊用途外,它还常用来表示单个字符。
0 g4 @! \% I. o$ y' |9 z>>> "h\x69, \u6C49\U00005B57"  D0 Y: @& C1 l
hi, 汉字- l, `. }/ C/ B+ S. [
注意:Unicode 格式大小写分别表示 16 位和 32 位整数,不能混用。
>>> "It's my life" # 英文缩写。
' u6 g0 @- g4 O; f) h* L>>> 'The report contained the "facts" of the case.' # 包含引文,避免使用 \" 转义。
# n  G) F' k) j( _- g: Q, l>>> "hello" ", " "world" # 合并多个相邻字量。
! H7 d# L& G. a# U$ \hello, world( |* q* [8 U. H3 j; P/ Q
>>> """ # 换行符、前导空格、空行都是组成内容。3 F1 a2 D: J* F( D2 `
The Zen of Python, by Tim Peters 5 M4 N/ z, n0 A; Z, k; V
Beautiful is better than ugly. 6 V; V* s) G, P8 m; H# `
Explicit is better than implicit. 9 H0 q3 t/ h, A2 }# r
Simple is better than complex.
1 _" W8 C- ~) T( Z7 L) t- T"""1 L$ ]& I+ H, Z. p. k6 \' J
可在字面量前添加标志,指示构建特定格式字符串。
% W. o0 A+ P( }最常用的原始字符串(r, raw string),它将反斜线视作字符内容,而非转义标志。这在构建类似 Windows 路径、正则表达式匹配模式 (pattern) 之类的文法字符串时很有用。
5 E7 H0 ^7 ^" j; m>>> open(r"c:\windows\readme.txt") # Windows 路径。
* ]% L4 v4 ^% \7 z4 Z% i! v>>> re.findall(r"\b\d+\b", "a10 100") # 正则表达式。
6 v) @" H% N" F; `['100']% M! W7 l3 K! l
>>> type(u"abc") # 默认 str 就是 unicode, 无需添加 u 前缀。
: i8 F( q+ f  L- estr+ ^  D& O3 t; i( @6 d) ^- ?$ y
>>> type(b"abc") # 构建字节数组。
1 ~# I4 i! x& P; V# k( k# Wbytes
$ X* s. g5 {4 p" p! B操作
8 G; l4 y  P* B4 K/ P; F支持用加法和乘法运算符拼接字符串。
0 Q1 X5 K" h5 @2 _5 i; ?>>> s = "hello"3 j& @9 x" @( {
>>> s += ", world"
- T/ h1 u! k" K+ J, X, N>>> "-" * 109 F, n0 H% G& p% Y% e
----------
/ ?& H' _* Q0 o/ M1 c! d4 y, I编译器会尝试在编译期直接计算出字面量拼接结果,避免运行时开销。不过此类优化程度有限,并不总是有效。
8 ?6 _. t5 K. f. k1 m>>> def test():
: x1 i" K. z  s6 {( T' v2 { a = "x" + "y" + "z", _* v! @  M( p/ ]0 V( P/ M$ K
b = "a" * 10
$ ]8 w' y# F; k" B7 C$ M" i1 k9 A1 V return a, b5 t. {# Q! s, |$ R) a+ x
>>> dis.dis(test)
& B' l6 H7 Y3 }1 B 2 0 LOAD_CONST 7 ('xyz') # 直接给出结果,省略加法运算。; Y% p3 n- w2 i
3 4 LOAD_CONST 8 ('aaaaaaaaaa') # 省略乘法运算。
, o+ |3 q9 J/ ~, u' a6 N. o至于多个动态字符串拼接,应优先选择 join 或 format 方式。( |* _( z+ K5 Y/ G6 e
相比多次加法运算和多次内存分配 (字符串是不可变对象),join 这类函数 (方法) 可预先计算出总长度,一次性分配内存,随后直接拷贝内存数据填充。另一方面,将固定内容与变量分离的模版化 format,更易阅读和维护。- n. s  r& `% y$ Z$ p. g
>>> username = "qyuhen"5 ~/ V5 U- z  `  a$ i. I
>>> datetime = "2017010"
: r; c) p1 N3 n$ ?  L5 c& n9 X' B5 j, s>>> "/data/" + username + "/message/" + datetime + ".txt"* f; t! o& c% `: Z8 @- D, k7 ^! c
/data/qyuhen/message/20170101.txt
7 |! [4 o5 x+ ^1 l% y* K>>> "/data/{user}/message/{time}.txt".format(user = username, time = datetime)
3 O; {) Z8 d, x/data/qyuhen/message/20170101.txt
+ e" w, x& c8 I9 z/ Y/ K+ W+ K我们用 line_profiler 对比用加法和 join 拼接 26 个大写字母的性能差异。虽然该测试不具备代表性,但可以提供一个粗略的验证方法。4 f! H) ~+ f  ^: |
#!/usr/bin/env python3
: z1 U% H1 F. f# i* oimport string0 @  M" w+ D! @' J
x = list(string.ascii_uppercase)
- _' S0 e4 e9 N7 ?@profile; }, `2 E% G) S3 f0 ]/ H/ J
def test_add():
/ O6 K2 \3 b- v& u/ v( _' N s = ""3 X( X9 x" G* X* ~: N; l* {8 i
for c in x:+ B  v0 {$ x- k' |+ L  c3 K  k
s += c; B1 i6 V* U) v3 n" y. \8 A! j# r
return s; F1 `/ D$ ]! k' v! l
@profile
" V7 x7 @. b' {* x9 S0 ndef test_join():7 m3 y, V( t' _% J1 C$ A
return "".join(x)
5 w! s0 J( _+ y0 Z# ^5 c. J9 _test_add()
  x9 G* W, o" K: f1 G5 xtest_join()
7 P( v* D  V7 q8 R5 D4 n输出:
' f6 B- s' {2 K: }' V$ kernprof -l ./test.py && python -m line_profiler test.py.lprof+ u/ W& z* Z" o! w. O
编写代码除保持简单外,还应具备良好的可阅读性。比如判断是否包含子串,in、not in 操作符就比 find 方法自然,更贴近日常阅读习惯。/ ^3 E7 X- @$ Q: j
>>> "py" in "python"0 h5 v) U$ Z+ R; N. O8 h
True
0 Y  p3 Z, g' v8 n! A) y) c" B" i1 S1 u>>> "Py" not in "python"; j& F( B9 ?" ]
True* G$ k3 o. [; F: n) }. g5 P9 U
作为序列类型,可以使用索引序号访问字符串内容,单个字符或者某一个片段。支持负索引,也就是从尾部以 -1 开始(索引 0 表示正向第一个字符)。
$ m) ?% w7 i6 s" @# {5 T1 p) M>>> s = "0123456789"
" {$ I7 U( h% _* E5 c. a7 j2 M>>> s[2]
# P/ ~5 c8 s! u2
- v: h8 m7 x' V>>> s[-1]  |7 K$ B5 {7 C7 o5 P: V* L1 O" `
9% h+ {+ a; u1 H5 Y, q, Y
>>> s[2:6]
5 y' {# j& `- i/ r9 Q0 o! D2345
4 _, ~4 @$ w5 p( K6 b$ j& y>>> s[2:-2]6 c1 m6 y0 t. i
234567) A& q) g5 G6 A* Y' A$ g+ S& U8 _7 K
使用两个索引号表示一个序列片段的语法称作切片(slice),可以此返回字符串子串。但无论以哪种方式返回与原字符串内容不同的子串时,都会重新分配内存,并复制数据。不像某些语言那样,仍旧以指针引用原字符串内容缓冲区。
" Y. O1 a: W+ W: i7 x" f先看相同或不同内容时,字符串对象构建情形。4 p- j" C( G8 z4 P" B
>>> s = "-" * 1024
5 y* G6 w6 C0 ?  X>>> s1 = s[10:100] # 片段,内容不同。
, S; A) z( ?- ?>>> s2 = s[:] # 内容相同6 a# E; z' x# I  i' P5 S
>>> s3 = s.split(",")[0] # 内容相同。+ F! U! u2 K! T( `2 d
>>> s1 is s # 内容不同,构建新对象。: z3 [. t! f+ g
False
, E) m5 P! \/ O; |/ Z>>> s2 is s # 内容相同时,直接引用原字符串对象。" S6 H& \+ w7 a. u
True
! Q; l9 ?0 d& b>>> s3 is s
" C% X& _5 Q- K4 ~( K* W+ FTrue
2 K" P0 q* ?- e  q: b; {3 k再进一步用 memory_profiler 观察内存分配情况。
, f  Y* d0 |8 s+ ?2 z) _) e5 S@profile& `7 H' E% M# v% W1 H8 ?
def test():3 x& L) t/ ?" S' {/ c/ L2 \" K6 U
a = x[10:-10]
% S6 F0 D' N! e9 y b = x.split(",")/ N5 a& l# t" h- K0 ~% [
return a, b! I- l1 G/ V4 p2 n9 o3 A) L" ~, o
x = "0," * (1 << 20)
8 G# d. H. ?6 u' h+ Ttest()
! u& H) Y' d: O3 M9 Q2 F; |输出
9 x/ a/ ^  p9 E" V% @# y5 @) y$ python -m memory_profiler ./test.py
& |3 ^( Y7 ^& I$ @% I" P: [: v
此类行为,与具体的 Python 实现版本有关,不能一概而论。
字符串类型内置丰富的处理方法,可满足大多数操作需要。对于更复杂的文本处理,还可使用正则表达式(re)或专业的第三方库,比如 NLTK、TextBlob 等。* q+ u' s& ?& b
转换
  O8 k3 T+ D5 K; y& ^" \除去与数字、Unicode 码点的转换外,最常见的是在不同编码间进行转换。& ?: S' _0 B" \9 \# F2 ]
Python 3 使用 bytes、bytearray 存储字节数组,不再和 str 混用。
>>> s = "汉字"8 I  ]% H2 d+ T( y% Z
>>> b = s.encode("utf-16") # to bytes
& C( _) _5 P" b, }  {6 g+ R5 H>>> b.decode("utf-16") # to unicode string
8 B  p( o% V$ U  _3 E汉字
2 }! b, c- l2 D0 N; t如要处理 BOM 信息,可导入 codecs 模块。3 ]0 J' r. Q  w' q, V
>>> s = "汉字"
4 h3 ?+ y: j# W3 X+ X>>> s.encode("utf-16").hex()
1 K3 u. F& v  J* q  J+ dfffe496c575b, E0 R4 r0 Z/ O
>>> codecs.BOM_UTF16_LE.hex() # BOM 标志。; U7 z* p. J2 ~- x" M" i
fffe
5 b' P4 F) m' u5 W. p7 q! U>>> codecs.encode(s, "utf-16be").hex() # 按指定 BOM 转换。4 l% g7 b& f( p! _
6c495b57  Z( \  ^' F2 U4 f6 {. c# Q
>>> codecs.encode(s, "utf-16le").hex()9 `: ~, H6 a8 n. r, z, K+ }
496c575b
7 V4 e  _1 v3 o还有,Python 3 默认编码不再是 ASCII,所以无需额外设置。2 u& g) w9 }0 I9 `0 k7 Z
Python 3.6! j1 ]* U% W+ U; Y% v/ L. E
>>> sys.getdefaultencoding()
+ F1 X+ |- v% X4 A  ?6 n: S9 ^8 dutf-87 B9 M9 C0 Y0 I
Python 2.7
- b  t# n; i  z3 R  K1 P>>> import sys& O! W) G, f5 }, A
>>> reload(sys)
4 A6 q4 ?9 X  {1 S>>> sys.setdefaultencoding("utf-8")/ z7 D/ w& m# Y# O1 ?4 m/ C, w$ Y" Y
>>> b = s.encode("utf-16")
+ |# p! j$ p1 F1 H8 p5 O>>> b.decode("utf-16")
% f* y* w7 D/ D% r, nu'\u6c49\u5b57'# \- W/ D# T- b$ ^2 }
>>> type(b)0 c. q% i& |# ~5 _4 z" q0 K
<type 'str'>; p7 b3 H# T+ `# l  t4 j9 j
格式化
9 x5 q9 s, q2 i" x5 m  \4 n长期发展下来,Python 累积了多种字符串格式化方式。相比古老的面孔,人们更喜欢或倾向于使用新的特征。. B$ U" M. w$ M! R* W9 |
Python 3.6 新增了 f-strings 支持,这在很多脚本语言里属于标配。
: D, x4 P* b+ ?0 y使用 f 前缀标志,解释器解析大括号内的字段或表达式,从上下文名字空间查找同名对象进行值替换。格式化控制依旧遵循 format 规范,但阅读体验上更加完整和简洁。: s( t) q! u) G7 ^: s; B- i& z
>>> x = 10
2 B+ D3 k3 L* A' L9 ]>>> y = 200 }6 F! N* ~7 }3 C
>>> f"{x} + {y} = {x + y}" # f-strings
. E0 D0 C+ Q, R2 |! _* c10 + 20 = 30; P0 L, n+ d! ^/ _* u
>>> "{} + {} = {}".format(x, y , x + y); I5 e$ W8 F4 c" v
10 + 20 = 30
9 V% i) \0 ]; w6 y表达式除运算符外,还可以是函数调用。
+ {  t, C5 N  F; @* |' j; f( r" Z>>> f"{type(x)}"1 w) Z6 H" k# M5 ~
<class 'int'>$ R) h' C0 }4 v# t3 H5 [
完整 format 格式化以位置序号、字段名匹配替换值参数,允许对其施加包括对齐、填充、 精度等控制。从某种角度看,f-strings 有点像是 format 的增强语法糖。
) w- D- G, Z2 R$ u; J% D
7 q$ r8 m  \' v4 n将两者进行对比,f-strings 类模版方式更加灵活,一定程度上将输出样式与数据来源分离。 但其缺点是与上下文名字耦合,导致模版内容与代码必须保持同步修改。而 format 的序号与主键匹配方式可避开这点,只可惜它不支持表达式。( h0 L2 M, ?. C  @% e% I
另外,对于简短的格式化处理,format 拥有更好的性能。
0 t, W0 P" ?1 n( ]手工序号和自动序号" d* t. ]- V" t3 h! d/ I4 n+ O  @
>>> "{0} {1} {0}".format("a", 10)" N1 b' z6 y1 j: t% m" l" |  G7 f5 V
a 10 a
5 t( i  z2 b" ?, J- h  ^>>> "{} {}".format(1, 2) # 自动序号,不能与手工序号混用。$ H" @& k3 ~4 u; P9 |4 I/ O$ c
1 2
( a6 x, c& X8 L1 Q2 _7 M5 k主键( N  I. ^% p5 C5 y: K/ q, j
>>> "{x} {y}".format(x = 100, y = [1,2,3])% I' P; y# ]5 s
100 [1, 2, 3]4 o  a( i) @  H# B$ v7 w
属性和索引
* o6 a: p! {" v- v" G/ r! O>>> x.name = "jack"
, I8 {8 ]+ ]% U  e' ^>>> "{0.name}".format(x) # 对象属性。
, m4 M. E% P: ?" Z1 n( vjack
. w, R) n8 ^3 W2 Q( d+ D+ x>>> "{0[2]}".format([1,2,3,4]) # 索引。
9 ~! R  _) l  G0 p0 }3
4 v- l3 f0 X! }( e- ]" J宽度、补位
  M/ y3 L8 s8 v4 k4 i5 {" R% M% m/ J>>> "{0:#08b}".format(5)
' R- C" \; m5 D6 H7 o7 t1 r0b000101% g0 H. F7 ~# t4 {; [
数字/ P& ^5 k) Q; M% j/ M3 z6 g
>>> "{:06.2f}".format(1.234) # 保留 2 位小数。
" [4 M! @0 z  d: [001.23
" a! u; U- Y( \1 d) {' J>>> "{:,}".format(123456789) # 千分位。
  C6 l7 b: v# E/ S2 g- f  q3 k% P123,456,789, [* B( N/ E  m" T$ J
对齐
! i2 @- e* H$ x8 m: ^! D>>> "[{:^10}]".format("abc") # 居中0 G  j2 A! |+ Q. S3 U' F
[ abc ]% E: b* \& c+ v4 V
>>> "[{:.<10}]".format("abc") # 左对齐,以点填充。
/ y2 P6 M0 L* l( I3 _6 L[abc.......]
) V/ U( i/ C) ?8 ~1 x古老的 printf 百分号格式化方式已被官方标记为 “obsolete”,加上其自身固有的一些问题,可能会被后续版本抛弃,不建议使用。另外,标准库里 string.Template 功能弱,且性能也差,同样不建议使用。3 u# B" F  w7 z6 e
池化
1 ~% j; O6 T# g5 o- y5 ~1 z字符串算是进程里实例数量较多的类型之一,因为无处不在的名字就是字符串实例。
7 F, W* G' N/ J  K' ]" h8 M$ w  s- h鉴于相同名字会重复出现在各种名字空间里,那么有必要让它们共享对象。内容相同,且不可变,共享不会导致任何问题。关键是可节约内存,且省去创建新实例的调用开销。& [& o* s# P' \) |
对此,Python 的做法是实现一个字符串池(intern)。池负责管理实例,使用者只需引用即可。另一潜在好处是,从池返回的字符串,只需比较指针就可知道内容是否相同,无需额外计算。可用来提升哈希表等类似结构的查找性能。, b1 n4 a4 X* Y1 X7 _7 P: o
>>> "__name__" is sys.intern("__name__")
$ o# s) X/ m& Z) D$ ^% v* O6 ETrue
. x3 s: g8 F. `4 Y! [除了以常量方式出现的名字和字面量外,动态生成字符串一样可加入池中。如此可保证每 次都引用同一对象,不会有额外的创建和分配操作。
" t! L9 g" m; u6 E* @) O* [>>> a = "hello, world!"
  }" l/ g1 g$ N8 f>>> b = "hello, world!"4 F) o& g- ]* H- W4 T3 X
>>> a is b # 不同实例。6 H8 v7 K4 ]3 ^
False
9 m: i# k3 h8 v- ^. t>>> sys.intern(a) is sys.intern("hello, world!") # 相同实例。1 F! i+ f5 W! x: y! \/ E) n
True. l+ k3 U6 S# u- h& D5 R; ]' }5 p) A
当然,一旦失去所有外部引用,池内字符串对象会被回收。
- P4 m: I. w( o7 O; H/ _/ d>>> a = sys.intern("hello, world!")
4 o( K" X# Z- B. T>>> id(a). `( d8 B0 i/ h- r8 V9 I" s9 F
4401879024* }$ @! I( L3 z  m
>>> id(sys.intern("hello, world!")) # 有外部引用。. X' O% m5 u4 v' G8 y9 D6 d# T
4401879024
3 C+ @9 ]' b$ U# P" g>>> del a # 删除外部引用后被回收。( V( Q+ q& W7 {  V$ _0 }# T/ @/ l- e
>>> id(sys.intern("hello, world!")) # 从 id 值不同可以看到新建,入池。
, D! D+ @6 p$ w/ N/ T8 g* k44052190567 \  ^8 H" g/ `
字符串池实现算法很简单,就是简单的字典结构。 详情参考 Objects/unicodeobject.c : PyUnicode_InternInPlace。
做大数据处理时,可能需创建海量主键,使用 intern 有助于减少对象数量,节约大量内存。

本帖子中包含更多资源

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

x
回复

使用道具 举报

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

本版积分规则

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

扫一扫关注我们

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