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

[复制链接]
sosoyoyo 发表于 2017-12-31 09:40:20 | 显示全部楼层 |阅读模式 打印 上一主题 下一主题
字符串
* X4 p5 A2 z9 g% A; r; Y字符串 (str) 存储 Unicode 文本,是不可变序列类型。相比 Python 2 里的混乱,Python 3 总算顺应时代发展,将文本和二进制彻底分离。
* M* ]3 w- b, M# {
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 = "汉字"  O- }8 K8 }1 P/ g* j
>>> len(s)
' b- [6 S2 ?" [2( P8 h  ~0 \  U0 \8 \5 v
>>> hex(ord("汉")) # code point5 I, V( Q/ h1 I2 x7 w' [
0x6c49$ Z7 l+ `$ M8 b$ Z  U- ~
>>> chr(0x6c49): l3 [0 H' g) w  D
) i# S& k, r! n( ^9 F
>>> ascii("汉字") # 对 non-ASCII 进行转义。
, e/ n0 O0 d! J\u6c49\u5b57
+ [6 o% h5 q5 n$ J, Z8 a7 n5 X字符串字面量(literal)以成对单引号、双引号,或跨行三引号语法构成,自动合并相邻字面量。支持转义、八进制、十六进制,或 Unicode 格式字符。9 d# l, {+ c# U- L# x, E
用单引号还是双引号,并没有什么特殊限制。如果文本内引用文字使用双引号,那么外面用单引号可避免转义,更易阅读。通常情况下,建议遵循多数编程语言惯例,使用双引号标示。除去单引号在英文句法里的特殊用途外,它还常用来表示单个字符。
6 Q1 k* z# O, x$ X/ I>>> "h\x69, \u6C49\U00005B57"
3 ]6 j" r1 p' ihi, 汉字
5 \" g1 _5 p- J- b4 T
注意:Unicode 格式大小写分别表示 16 位和 32 位整数,不能混用。
>>> "It's my life" # 英文缩写。5 N) A6 x8 }, C; C. [0 q. T
>>> 'The report contained the "facts" of the case.' # 包含引文,避免使用 \" 转义。# v. y9 y+ C0 ~: T
>>> "hello" ", " "world" # 合并多个相邻字量。
+ n1 ?* ~9 j) p0 d4 }hello, world$ W' t% ]/ R5 z: v9 C( @- @; j
>>> """ # 换行符、前导空格、空行都是组成内容。/ ]2 x% q& `" b3 \/ J
The Zen of Python, by Tim Peters
: q" l: h2 h2 t+ M" c Beautiful is better than ugly.
# R; s5 s8 z. V0 O2 P" h9 r Explicit is better than implicit. , j  Q5 C' h$ ^: i8 e
Simple is better than complex.
7 L: I+ Y: b4 u+ ^"""
. s" F# ~' {9 O# p* }# w4 G' |! z可在字面量前添加标志,指示构建特定格式字符串。
# p! Q5 ^$ L6 W最常用的原始字符串(r, raw string),它将反斜线视作字符内容,而非转义标志。这在构建类似 Windows 路径、正则表达式匹配模式 (pattern) 之类的文法字符串时很有用。
$ t" A9 K, o8 R# H( s* X>>> open(r"c:\windows\readme.txt") # Windows 路径。5 A! z( i  z2 g% S$ w
>>> re.findall(r"\b\d+\b", "a10 100") # 正则表达式。
9 N0 K" R3 F+ L4 X# g7 ~['100']3 B6 O3 ^5 H. `' X
>>> type(u"abc") # 默认 str 就是 unicode, 无需添加 u 前缀。
5 I+ o2 ~' S: _" sstr
' Y  K: f, U/ b) k) j5 m>>> type(b"abc") # 构建字节数组。
& j: }) _) ?; f; wbytes
- m$ p5 D& ^4 ^操作
/ U5 J1 I/ o0 L7 d' b4 q支持用加法和乘法运算符拼接字符串。
% s0 T6 M, D8 v  R+ y$ M% p" T>>> s = "hello"* Q% T6 h9 M! e, F) Q
>>> s += ", world"9 w: a- R. ^+ f
>>> "-" * 10$ A" L. q6 Z, d" u9 z/ X, j/ W
----------
9 t  D: X. i$ g1 z编译器会尝试在编译期直接计算出字面量拼接结果,避免运行时开销。不过此类优化程度有限,并不总是有效。5 u4 k& J) G# A
>>> def test():
* M4 p, n3 A" W0 j! i  G% |! @ a = "x" + "y" + "z"
( H% a7 o3 N& s b = "a" * 10
# f7 h: u2 Z- T  U, W return a, b
3 i4 `5 G7 X, n+ N" \" c: g+ c>>> dis.dis(test)
! t; b3 ?9 M1 p9 [/ e% C2 m7 a4 ^( _ 2 0 LOAD_CONST 7 ('xyz') # 直接给出结果,省略加法运算。  ^2 ~  l/ K: A
3 4 LOAD_CONST 8 ('aaaaaaaaaa') # 省略乘法运算。
4 a8 k9 G( x4 B5 b9 @3 ~! N4 F至于多个动态字符串拼接,应优先选择 join 或 format 方式。' {4 K6 {6 P9 W# d+ d7 w
相比多次加法运算和多次内存分配 (字符串是不可变对象),join 这类函数 (方法) 可预先计算出总长度,一次性分配内存,随后直接拷贝内存数据填充。另一方面,将固定内容与变量分离的模版化 format,更易阅读和维护。
$ a: h' Z. Q3 I' t0 w/ z>>> username = "qyuhen"
0 D9 r( S% M- e& u>>> datetime = "2017010"
- F/ H7 u" A8 u, Q0 t>>> "/data/" + username + "/message/" + datetime + ".txt"" ^0 _6 g' X. @) r2 t+ j, h5 W5 `
/data/qyuhen/message/20170101.txt
9 K8 n: a! H- n5 b9 b" C6 I>>> "/data/{user}/message/{time}.txt".format(user = username, time = datetime)% B! L6 E; S" [; V  n" T" ^! O
/data/qyuhen/message/20170101.txt
- P* |6 r0 U; g8 r( J/ Y" P我们用 line_profiler 对比用加法和 join 拼接 26 个大写字母的性能差异。虽然该测试不具备代表性,但可以提供一个粗略的验证方法。
- G0 t  K4 Q/ b" U0 }+ j* d#!/usr/bin/env python39 M. W$ o# k7 F% T. b7 H7 Q$ m% s& Z
import string  E' m1 @  x! ~
x = list(string.ascii_uppercase)9 c5 k9 W8 d$ \
@profile3 V2 F2 z, J5 G, w: B2 ]/ q  c
def test_add():
. k; t; C" D* I3 o0 G* E' I! n3 M! h s = "", H. b( E7 P" ~8 c( D
for c in x:5 t( t4 l+ H! ^3 s6 D2 T; f/ \
s += c) J2 x, o" @, V5 a5 H6 I
return s7 k. s( M" a' Z/ e9 j
@profile
  V# }/ }0 r& x/ ~: y8 Qdef test_join():
. M$ r) O/ k4 z1 ^9 v' N+ r# `# E8 L return "".join(x)
) u+ G9 {- d4 E' ctest_add()
. x# w0 `, G3 J" {8 S/ y3 V+ Z" atest_join()3 J6 L+ G; \( J/ I
输出:- a# H7 f6 m) g( R
$ kernprof -l ./test.py && python -m line_profiler test.py.lprof
9 O- w( |# f, {( i1 {  z
编写代码除保持简单外,还应具备良好的可阅读性。比如判断是否包含子串,in、not in 操作符就比 find 方法自然,更贴近日常阅读习惯。
  I# O( }3 j* p  Z
>>> "py" in "python"8 ]; Z  Y  z; F! A( F, K1 d
True
3 ~) z5 {7 Y" b>>> "Py" not in "python": X& ]9 t7 s0 u2 r& W9 H/ g
True
. s: x* T2 ?$ k( `作为序列类型,可以使用索引序号访问字符串内容,单个字符或者某一个片段。支持负索引,也就是从尾部以 -1 开始(索引 0 表示正向第一个字符)。* o6 p. @, V" |' e& t; G5 |3 Y
>>> s = "0123456789"
) z. K  \3 a( u* [( {8 E, ]>>> s[2]- A0 [% b/ l: f8 f( G9 X* a/ ?, G6 p
2
/ z: c  a+ k/ w$ B! ?2 t  `>>> s[-1]# O& V2 d% O9 }% C3 [. X! h1 P
92 l5 x% C! C% F
>>> s[2:6]
# t% w7 e9 E& X8 V1 t; }7 C23458 S/ I- `% f- d5 ?) j4 B
>>> s[2:-2]% a5 L1 ^7 c- H, S. h' z
234567+ H+ B" ~- P- k, V3 \
使用两个索引号表示一个序列片段的语法称作切片(slice),可以此返回字符串子串。但无论以哪种方式返回与原字符串内容不同的子串时,都会重新分配内存,并复制数据。不像某些语言那样,仍旧以指针引用原字符串内容缓冲区。& i" N( z; S/ J' x  I
先看相同或不同内容时,字符串对象构建情形。
6 z% r) @2 h$ L! z>>> s = "-" * 1024
0 O) e+ U% r/ ?. m6 `>>> s1 = s[10:100] # 片段,内容不同。
; A2 I0 |# L) w. G% B>>> s2 = s[:] # 内容相同) Z7 S! ?+ f. i) l; G
>>> s3 = s.split(",")[0] # 内容相同。
7 c% P/ k- h7 I! x5 T: }>>> s1 is s # 内容不同,构建新对象。& A, d' ~; @& k: E
False
# y: x2 `7 M- h! f9 ]. O>>> s2 is s # 内容相同时,直接引用原字符串对象。/ I& _0 @5 O5 t5 K: C
True
5 M5 X5 S0 ~6 x  J4 q  q>>> s3 is s
# d* E& y# h2 b" s  m" vTrue
6 {+ o* g  G9 _* p再进一步用 memory_profiler 观察内存分配情况。% s% z+ W6 ~: v. u' Y
@profile
( s8 Z& v9 ], ]) C8 o# e5 |def test():' q( ]$ `* c. S5 |2 j
a = x[10:-10]
; w; \$ w2 Z) W4 E" i7 N b = x.split(",")
( V/ r& h4 N* I/ q; Z6 u9 g' m# ^7 | return a, b$ H! o2 m0 ?/ h3 ]- B
x = "0," * (1 << 20)
7 j  r( j4 z* {test()
7 y# F5 a0 Q& E8 z- i% y输出: g6 X6 b. ]( A: m) r) P
$ python -m memory_profiler ./test.py9 f5 n; d1 n& H3 L" }1 S
此类行为,与具体的 Python 实现版本有关,不能一概而论。
字符串类型内置丰富的处理方法,可满足大多数操作需要。对于更复杂的文本处理,还可使用正则表达式(re)或专业的第三方库,比如 NLTK、TextBlob 等。
) @# o4 |, K4 v8 }- b+ N! ~# @  f转换1 d: g  r+ E9 {
除去与数字、Unicode 码点的转换外,最常见的是在不同编码间进行转换。
0 m5 |6 J' @( J! A3 v2 h
Python 3 使用 bytes、bytearray 存储字节数组,不再和 str 混用。
>>> s = "汉字"
- [" u, s) }1 J  ^" Z! }8 _$ _>>> b = s.encode("utf-16") # to bytes
# }0 [  {6 M* K: b: y9 M% i0 C. I; v>>> b.decode("utf-16") # to unicode string5 g4 T( v/ O2 V5 l
汉字' e3 T" x8 A/ ?' G" ?/ J9 \
如要处理 BOM 信息,可导入 codecs 模块。
& K% i/ I3 }1 a>>> s = "汉字"
( P% h! a" ?9 ]. W>>> s.encode("utf-16").hex()
" k$ V/ E' n' {, q3 Dfffe496c575b
+ h, ^. r9 }2 K>>> codecs.BOM_UTF16_LE.hex() # BOM 标志。
- C7 t" m- c# r9 Z$ M0 B9 ~. `  nfffe
1 ^, }) k% H& Y+ `>>> codecs.encode(s, "utf-16be").hex() # 按指定 BOM 转换。
( W* n( O4 i6 Z3 J) f, W( s0 K2 _6c495b57
! ^( l7 Y* ^( A8 g  c  L3 ]4 c>>> codecs.encode(s, "utf-16le").hex()1 O7 N- D4 E6 S4 Z; e# E
496c575b
0 R2 |! K/ j( r1 d还有,Python 3 默认编码不再是 ASCII,所以无需额外设置。
) {5 k; h$ l# H" \Python 3.6( P0 [- X: p0 W8 {' [4 E  E' `/ F
>>> sys.getdefaultencoding()
# {9 I9 w8 a& h+ r; k* butf-8
& \# n( h5 Q8 ]# c2 VPython 2.7  F& o& u3 |1 _9 y  i
>>> import sys
: s' l5 l0 N3 l( b2 p7 ]: _>>> reload(sys)% z2 U" ^4 B$ F7 q5 u
>>> sys.setdefaultencoding("utf-8")
5 n4 S7 o- a1 I. N! G9 W% ?>>> b = s.encode("utf-16")
1 L, D2 w3 ~& [+ I0 I>>> b.decode("utf-16"), [4 ?: E0 G& \. |" g
u'\u6c49\u5b57'
0 I/ A' z& V* J" m>>> type(b)* p8 f8 |. `) H
<type 'str'>' x: C- h4 [. p3 F  E0 r* u7 P
格式化
  Q" T) m' a/ Y+ v' a长期发展下来,Python 累积了多种字符串格式化方式。相比古老的面孔,人们更喜欢或倾向于使用新的特征。9 j/ E7 E, \7 x4 G" i
Python 3.6 新增了 f-strings 支持,这在很多脚本语言里属于标配。5 m, g2 W+ u0 Q) V
使用 f 前缀标志,解释器解析大括号内的字段或表达式,从上下文名字空间查找同名对象进行值替换。格式化控制依旧遵循 format 规范,但阅读体验上更加完整和简洁。2 b* K6 X; w* Q8 j. q' G
>>> x = 10
  ^* n# k  e+ {/ K>>> y = 20! u$ x/ [0 u8 T) m1 w9 S0 P
>>> f"{x} + {y} = {x + y}" # f-strings
9 C" `- }! ^- U! q0 B2 x3 Y3 P0 m- G10 + 20 = 30
7 D  B% r3 |( l+ E* T. q>>> "{} + {} = {}".format(x, y , x + y): B; w6 N6 R) P' S# Q" x, A; L
10 + 20 = 30' t- ?  |8 Z( k* j* B
表达式除运算符外,还可以是函数调用。; k/ K, _: n0 p/ A9 x  k/ r
>>> f"{type(x)}", f, y, w7 f8 ^/ Z0 @. z# A
<class 'int'>
; M; B- ^* t, Z5 O; d4 B/ y. k: y1 t完整 format 格式化以位置序号、字段名匹配替换值参数,允许对其施加包括对齐、填充、 精度等控制。从某种角度看,f-strings 有点像是 format 的增强语法糖。' J7 N2 m+ @# Z, y$ b' i4 B

0 }: }* J8 ], H) y/ O将两者进行对比,f-strings 类模版方式更加灵活,一定程度上将输出样式与数据来源分离。 但其缺点是与上下文名字耦合,导致模版内容与代码必须保持同步修改。而 format 的序号与主键匹配方式可避开这点,只可惜它不支持表达式。
8 I0 r' W  ~$ a另外,对于简短的格式化处理,format 拥有更好的性能。
2 ~4 x6 r4 t0 ?6 c* M3 C手工序号和自动序号
! u( D2 C( g8 _% z. h8 A0 C, ?$ X>>> "{0} {1} {0}".format("a", 10)7 a- a; r& j3 t. @+ h3 M
a 10 a
6 |% Y- \( Q* U) E( `& v>>> "{} {}".format(1, 2) # 自动序号,不能与手工序号混用。6 W$ b# \1 ^9 F3 ?1 F( N
1 2) S" Z# ~  \6 [; l; W6 u
主键4 U# ~/ \! h3 O
>>> "{x} {y}".format(x = 100, y = [1,2,3])
: \* s! V( W8 ]100 [1, 2, 3]! u4 ~0 L/ u: {
属性和索引
/ F3 @" u, N+ ^$ m4 r>>> x.name = "jack"  b6 j3 m( K3 i4 ]* ?
>>> "{0.name}".format(x) # 对象属性。. V/ p* l' T4 [1 p" N- F
jack
% v. s- U' c! [$ r$ B8 t8 h( y>>> "{0[2]}".format([1,2,3,4]) # 索引。+ n6 T9 A* z0 p3 ?
3+ O* G5 r+ e$ N1 x6 E$ p$ j! k
宽度、补位: z% H  q% i0 D0 U
>>> "{0:#08b}".format(5); U, {$ f: r" S- I4 C; ^8 i
0b000101+ A; `) E. n. g/ E/ g2 K: x
数字
7 H$ ?6 F1 o0 o) y4 o; Y>>> "{:06.2f}".format(1.234) # 保留 2 位小数。
0 }  r" i$ Y7 K4 j. N6 x" R001.23
' ]  s# j8 j) g>>> "{:,}".format(123456789) # 千分位。
) X+ ^2 `( u: S) M123,456,7897 \5 B/ p9 L/ l7 ~  N
对齐
3 |' k- B& m0 ~# l>>> "[{:^10}]".format("abc") # 居中
+ [' E5 C& P' i/ l0 I4 \[ abc ]
" h2 l9 _+ w& ~: G: t5 n% ]4 D+ g>>> "[{:.<10}]".format("abc") # 左对齐,以点填充。* r8 s1 |6 t$ L9 J2 |- `
[abc.......]
+ w" V: \+ ], \. X! T2 I古老的 printf 百分号格式化方式已被官方标记为 “obsolete”,加上其自身固有的一些问题,可能会被后续版本抛弃,不建议使用。另外,标准库里 string.Template 功能弱,且性能也差,同样不建议使用。' u4 ?  W* O. D) z
池化
8 g  p! z- n8 @+ ]0 s: c字符串算是进程里实例数量较多的类型之一,因为无处不在的名字就是字符串实例。
# w4 g% {6 X& z: P( O" r$ P鉴于相同名字会重复出现在各种名字空间里,那么有必要让它们共享对象。内容相同,且不可变,共享不会导致任何问题。关键是可节约内存,且省去创建新实例的调用开销。
& @9 [+ O7 \( I) |. }" u' E. o对此,Python 的做法是实现一个字符串池(intern)。池负责管理实例,使用者只需引用即可。另一潜在好处是,从池返回的字符串,只需比较指针就可知道内容是否相同,无需额外计算。可用来提升哈希表等类似结构的查找性能。
4 b/ F! h8 e( m9 ?  ^6 a3 T, a>>> "__name__" is sys.intern("__name__")( J5 N) r" j. R
True; R5 d1 p/ Q4 M' F& `/ W) {
除了以常量方式出现的名字和字面量外,动态生成字符串一样可加入池中。如此可保证每 次都引用同一对象,不会有额外的创建和分配操作。
7 S. m' ~) `$ ^0 ?>>> a = "hello, world!"6 V8 W! E* Y" T: M
>>> b = "hello, world!"
9 X/ M2 S4 l+ M+ V>>> a is b # 不同实例。
' f, ?* x; _! p) v3 J* l( uFalse8 E- Q9 d+ j! U  W* }- P
>>> sys.intern(a) is sys.intern("hello, world!") # 相同实例。
  Q8 w) g9 h2 T+ RTrue
& m" K$ t4 t6 D& p4 H: W4 W当然,一旦失去所有外部引用,池内字符串对象会被回收。
. M4 G* ]2 A1 h, v) B& T' v>>> a = sys.intern("hello, world!")7 R: ~1 Z$ E, p, ^) J5 s5 l7 ?
>>> id(a), w7 Q2 D: Q7 P3 {8 X4 a& {
4401879024
8 Z4 M0 V8 _, |0 O>>> id(sys.intern("hello, world!")) # 有外部引用。
/ L1 o' z8 V. q" G8 R) `4401879024! D! `% y! k* [% W+ e2 [1 o  W
>>> del a # 删除外部引用后被回收。1 C9 U* c' J. O
>>> id(sys.intern("hello, world!")) # 从 id 值不同可以看到新建,入池。' h( i. Y5 c4 y6 o
4405219056! E7 p6 y5 p  s2 G: g6 Z2 W9 a
字符串池实现算法很简单,就是简单的字典结构。 详情参考 Objects/unicodeobject.c : PyUnicode_InternInPlace。
做大数据处理时,可能需创建海量主键,使用 intern 有助于减少对象数量,节约大量内存。

本帖子中包含更多资源

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

x
回复

使用道具 举报

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

本版积分规则

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

扫一扫关注我们

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