Python回顾与整理4:序列1—字符串

0.说明

        

        序列其实是Python的某几类数据类型的统称,如字符串,列表和元组,将它们统称为序列,是因为:它们的成员有序排列,并且可以通过下标偏移量访问到它的一个或者几个成员

        总结的思路为:先介绍适用于所有序列类型的操作符和内建函数,然后再分别对这几种序列类型进行介绍




1.序列


        序列类型都有相同的访问模式:它的每一个元素都可以通过指定一个偏移量的方式得到,多个元素通过切片操作的方式得到。而在Python序列中,偏移量的规则如下(假设序列长度为N):

wKiom1bq3yyQV70pAAAP0Jc09p0317.png


(1)标准类型操作符

        在《Python回顾与整理2:Python对象》中有介绍标准类型操作符,这些操作符一般都是可以适用于所有的序列类型的。


(2)序列类型操作符

        分别介绍如下:

  • 成员关系操作符:in,not in

        对于字符串,就是判断字符是否在字符串中(其实这个字符也是一个字符串对象);对于列表和元组,就是判断对象是否属于该对象序列,语法为:obj [not] in 序列

  • 连接操作符:+

  • 从节约内存的角度去考虑,对于字符串,建议使用join,对于列表,建议使用extend().

  • 重复操作符:*

        语法为:sequence * copies_int,即数字必须为整型,不能是长整型。

  • 切片操作符:[ ],[:],[::]

        假设有列表:L = [0, 1, 2, 3, 4],常见的有下面的几种形式操作:

        


操作结果
L或L[:][1, 2, 3, 4, 5]
L[0:3]或L[:3][0, 1, 2]
L[2:5]或L[2:][2, 3, 4]
L[1:3][1, 2]
L[3][3]

        通过上表容易知道:如果没有指定起始索引值(即为None),将以第一个索引值作为默认值;同样地,如果没有指定结尾索引值,将以最后一个索引值作为默认值;如果都没有指定,则返回整个序列。


  • 切片操作扩展:使用步长索引

        使用第3个参数作为步长,如下:

>>> s = 'abcdefgh'    
>>> s[::-1]       #可以视作翻转
'hgfedcba'
>>> s[::2]        #隔一个取一个
'aceg'
  • 切片索引的更多内容

        主要看下面一个例子:

>>> s = 'abcde'
>>> for i in [None] + range(-1, -len(s), -1):
...   print s[:i]
... 
abcde
abcd
abc
ab
a

        上面的方法主要是希望在第一次迭代时就能显示完整的字符串,然后依次看去掉末尾的每一个字符串,而使用了None的方法,是因为对于s[:]的切片操作,其实开头和末尾就是None。当然,如果你想用下面的方法,是行不通的:

>>> for i in [None].extend(range(-1, -len(s), -1)):
...   print s[:i]
... 
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'NoneType' object is not iterable

        那是因为列表的extend方法是没有返回值的(返回None),所以不能迭代。

        另外一点时,开始和结束索引值可以超过字符串的长度,如下:

>>> s = 'xpleaf'
>>> s[-100:100]
'xpleaf'
>>> s[-100:-50]
''


(3)序列类型内建函数

(a)类型转换

        主要是下面这些用于序列类型的转换函数:

  • list(iter)

  • str(obj)

  • unicode(obj)

  • basestring()

  • tuple(iter)

        但需要注意的是,实际上并不存在真正的类型转换,因为在《Python回顾与整理2:Python对象》中已有提及,Python对象的类型和身份是不可变的,所谓转换,其实它们只是把对象作为参数,并将其内容(浅)拷贝到新生成的对象中。

        而所谓浅拷贝就是只拷贝了对象的索引,而不是重新建立了一个对象。

(b)可操作

        主要是下面的这些序列类型可用的内建函数:

  • enumerate(iter)

  • len(seq)

  • max(iter, key=None) or max(arg0, arg1, key=None)

  • min(iter, key=None) or min(arg0, arg1, key=None)

  • reversed(seq)

  • sorted(iter, func=None, key=None, reverse=False)

  • sum(seq, init=0)

  • zip([it0, it1,... itN])

        需要注意的是,序列类型是可以迭代的,但可以迭代的则不一定是序列类型,比如迭代器以及其它可迭代的非序列对象,所以对于上面函数的参数类型iter和seq要注意区分。后面将会在特定序列类型中对这些函数进行介绍。        




2.字符串


        字符串是一种直接量或标量,这意味着Python解释器在处理字符串时是把它作为单一值并且不会包含其他Python类型的数据类型来进行处理。

        Python实际上有三种字符串:

  • str:通常意义的字符串

  • unicode:Unicode字符串

  • basestring:抽象类,str和unicode都是其子类,不可被实例化

>>> basestring('foo')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: The basestring type cannot be instantiated

        基本的操作如下:

  • 字符串创建和赋值:直接赋值或使用str()转换

  • 访问字符串或其字符:通过切片操作

  • 改变字符串:生成新的字符串

  • 删除字符或字符串:可以使用del或赋值空字符串

        上面的4个内容都比较简单,但有一点需要注意的是,因为字符串是不可变类型,所以无论是改变字符串或者是删除其中的字符,其实都是需要重新生成一个新的字符串对象。对于删除字符串,可以显式地使用del,但一般不需要这么做。




3.字符串和操作符


(1)标准类型操作符

        在《Python回顾与整理2:Python对象》中已经有介绍过标准类型操作符,它们对于字符串类型肯定也是适用的(字符串就是标准类型或者说是内建类型),举例如下:

>>> str1 = 'abc'
>>> str2 = 'lmn'
>>> str3 = 'xyz'
>>> str1 < str2
True
>>> str2 != str3
True

        其中,字符串大小的比较是按照ASCII值的大小来进行比较的。


(2)序列类型操作符

 (a)切片操作

        主要是考虑下面的几种操作:

  • 正向索引:使用正索引值

  • 反向索引:使用负索引值

  • 默认索引:使用正索引值

    通过前面对序列的了解,这些都比较简单,这里就不作总结了。

(b)成员操作符(in, not in)

        用于判断一个字符或者一个字符串是否出现在另一个字符串中,但需要注意的是,成员操作符不是用来判断一个字符串是否包含另一个字符串的(这样的操作应该由find(), index(), rfind()或rindex()函数来完成)。举例如下:

>>> 'bc' in 'abcd'
True
>>> 'n' in 'abcd'
False
>>> 'nm' not in 'abcd'
True

        有时需要经常用到string模块里的一些属性:

>>> import string
>>> string.uppercase
'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
>>> string.lowercase
'abcdefghijklmnopqrstuvwxyz'
>>> string.letters
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'
>>> string.digits
'0123456789'

        下面是一个简单判断Python有效标识符的例子,非常有参考价值:

import string

alphas = string.letters + '_'
nums = string.digits

print 'Welcome to the Identifier Checker v1.0'
print 'Testees must be at least 2 chars long.'
myInput = raw_input('Identifier to test? ')

if len(myInput) > 1:

    if myInput[0] not in alphas:
        print 'invalid: first symbol must be alphabetic'
    else:
        for otherChar in myInput[1:]:

            if otherChar not in alphas + nums:
                print 'invalid: remaining symbols mustt be alphanumeric'
                break
        else:
            print 'okay as an identifier'

        执行时如下:

Welcome to the Identifier Checker v1.0
Testees must be at least 2 chars long.
Identifier to test? .123
invalid: first symbol must be alphabetic

Welcome to the Identifier Checker v1.0
Testees must be at least 2 chars long.
Identifier to test? xpleaf
okay as an identifier

        几个注意的地方:

  • for-else语句:else语句块在for循环完整执行时才会执行

  • 性能优化:把重复操作作为参数放到循环里面进行是非常低效的

        如while i < len(myString),因为每执行一次循环,迭代都需要运行一次这个函数,因为我们可以先把len(myString)保存起来再进行使用,所以对于上面的例子,可以先把alphas + nums先保存起来:

alphnums = alphas + nums

(c)连接符(+)

  •  运行时刻字符串连接

    举例如下:

>>> 'xpleaf' + ' ' + 'clyyh'
'xpleaf clyyh'
>>> import string
>>> string.upper('xpleaf' + ' ' + 'clyyh')
'XPLEAF CLYYH'

        但不建议像上面的方法来使用,因为Python必须为每一个参加连接操作的字符串分配新的内在,包括新产生的字符串,另外,也不建议使用string模块,因为字符串对象本身已经有这些方法,所以将上面的代码修改如下:

>>> ''.join(('xpleaf', ' ', 'clyyh')).upper()
'XPLEAF CLYYH'
  • 编译时字符串连接

    在Python的语法中,允许我们在源码中把几个字符串连在一起来构成新的字符串:

>>> foo = 'Hello' ' ' 'World!'
>>> foo
'Hello World!'

        这样的好处时,可以把字符串分成几部分来写,而不用加反斜杠:

>>> string.upper('xpleaf/'
... 'yonghaoye'
... '/clyyh')
'XPLEAF/YONGHAOYE/CLYYH'

        当然,你也可以在每一个字符串后面写注释。

(d)普通字符串转化为Unicode字符串

        普通字符串和Unicode字符串连接时就会做自动转换:

>>> 'Hello' + u' ' + 'World' + u'!'
u'Hello World!'

(e)重复操作符(*)

        举例如下:

>>> '=' * 20
'===================='




4.只适用于字符串的操作符


(1)格式化操作符(%)

        这与C语言的printf()函数十分类似,下面给出字符串格式化符号列表以及对应的简单例子:

字符串格式化符号
格式化字符串转换方式简单例子
%c转换成字符(ASCII码值,或者长度为一的字符串)

>>> print '%c' % 65

A

>>> print '%c' % 'L'

L

%r优先使用repr()函数进行字符串转换

>>> print '%r' % 'xpleaf'

'xpleaf'

%s优先使用str()函数进行字符串转换

>>> print '%s' % 'xpleaf'

xpleaf

%d/%i转成有符号十进制数

>>> print '%d' % 16  #十进制

16

>>> print '%d' % 020  #八进制

16

>>> print '%d' % 0x10  #十六进制

16

%u转成无符号十进制数
%o
转成无符号八进制数

>>> print '%o' % 16

20

>>> print '%o' % -16

-20

%x/%X转成无符号十六进制数

>>> print '%x' % 16

10

>>> print '%x' % -16

-10

%e/%E转成科学计数法>>> print '%e' % 16

1.600000e+01

%f/%F转成浮点型(小数部分自然处理)

>>> print '%f' % 16

16.000000

%g/%G%e和%f/%E和%F的简写,指数小于-4或更高精度时使用%e或%E,否则使用%f
%%输出%

>>> print '%d%%' % 100

100%

        需要注意的是,%u/%o/%x/%X在遇到负数时会返回一个有符号字符串,如上面的例子所示。另外需要注意的是,Python支持两种格式的输入参数:

  • 元组形式:上面的例子即是采用这种方式

  • 字典形式:后面在总结字典时会提及(下面也有提及)

        有时候需要控制字符串的输出格式,这时可以使用一些辅助指令,如下:

格式化操作符辅助命令
符号作用简单例子
*定义宽度或者小数点精度

>>> print '%10d' % 16

        16

-用作左对齐

>>> print '%-10d' % 16

16 

+在正数前面显示加号(+)

>>> print '%+d' % 16

+16

<sp>在正数前面显示空格

>>> print '% d' % 16

 16

>>> print '% d' % -16

-16

#在八进制数前显示零('0'),在十六进制数前显示'0x'或'0X'

>>> print '%#o' % 16

020

>>> print '%#x' % 16

0x10

(var)映射变量(字典参数)

>>> print 'name:%(name)s' % {'name': 'xpleaf'}

name:xpleaf

m.nm是显示的最小总宽度,n是小数点后的位数(如果可用的话)

>>> print '%10.2f' % 16

     16.00

>>> print '%10.6f' % 16

 16.000000

        当然在进行字符串输出的时候也可以加入转义字符:

>>> print 'Host: %s\tPort: %d' % ('xpleaf', 80)
Host: xpleaf	Port: 80

        事实上,所有的Python对象都有一个字符串表示形式(通过repr()函数,或str()函数来展现),print语句会自动为每个对象调用str()函数。而在定义自己的对象时,可以定义对象的字符串表示,这样,repr(),str()或者print被调用时,就可以返回一个自定义的字符串表示了,这一点,在使用Flask定义Models时,作用就非常大,如下是一个典型的例子:

class Menu(db.Model):
    __tablename__ = 'menus'
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(64), unique=True)
    types = db.relationship('ArticleType', backref='menu', lazy='dynamic')
    order = db.Column(db.Integer, default=0, nullable=False)
    
def __repr__(self):
        return '<Menu %r>' % self.name

        这样在查询一个Model时,根据返回的字符串,就可以知道是哪一个Model,因为正常情况下(没有定义字符串钩子),返回的将是一些无法区分哪一个Model的字符串形式,如<Model 0xff389923>这样的形式,可以参考下面的例子:

>>> class Test():
...   def __unicode__(self):
...     return 'This is an unicode string'
... 
>>> c = Test()
>>> unicode(c)
u'This is an unicode string'
>>> str(c)
'<__main__.Test instance at 0x7fd83b762bd8>'
>>> repr(c)
'<__main__.Test instance at 0x7fd83b762bd8>'

        当然,如果定义了__repr__()方法,那么使用unicode(c)或str(c)时都会有显示,因为repr()对Python本身的支持是比较好的(repr()为Python‘官方’字符串表示),所以一般我们都会定义__repr__()方法。

        其实,在定义了__repr__()方法之后,在解释器中转储对象时,就可以不是原来的<object ai id>的形式了:

# 没有定义__repr__()方法
>>> class Test(object):
...     pass
... 
>>> t = Test()
>>> t
<__main__.Test object at 0x7f57ec793050>

# 定义了__repr__()方法
>>> class Test(object):
...     def __repr__(self):
...             return "This is a test!"
... 
>>> t = Test()
>>> t
This is a test!

        从中就可以看出__repr__()方法的作用了,说它是官方字符串的表示方法,是因为,默认情况下在解释器中输入一个对象,其实它返回的就是__repr__()方法中定义的返回对象,当然,如果没有定义该方法,返回的就是<object at id>这样的形式了。

        事实上,如果只定义了__repr__()方法,而没有定义__str__()方法,那么在执行print语句时,还是会调用__repr__()方法的,这在前面有提到过,如果定义了__repr__()方法,那么使用unicode(c)或str(c)时都会有显示。


(2)字符串模板

        可以使用string模块中的Template对象,这样在格式化输出字符串时就可以不用去记住前面的字符串格式化符号,主要有下面的两个方法:

  • substitue()

  • safe_substitue()

        第一个更为严谨,因为在key缺少的情况下会报错,第二个则不会,举例如下:

>>> from string import Template
>>> s = Template('My name is ${name}, and my girlfriend is ${girlfriend}.')
>>> print s.substitute(name='xpleaf', girlfriend='cl')
My name is xpleaf, and my girlfriend is cl.
>>> print s.substitute(name='xpleaf')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/usr/lib/python2.7/string.py", line 176, in substitute
    return self.pattern.sub(convert, self.template)
  File "/usr/lib/python2.7/string.py", line 166, in convert
    val = mapping[named]
KeyError: 'girlfriend'
>>> print s.safe_substitute(name='xpleaf')
My name is xpleaf, and my girlfriend is ${girlfriend}.


(3)原始字符串操作符(r/R)

        原始字符串,即所有的字符都是按照原来的意思进行处理,不会对转义字符或特殊字符进行处理,这在使用正则表达式时作用非常大。要使用原始字符串,只需要在字符串前加'r'或'R'即可,举例如下:

>>> '\n'
'\n'
>>> print '\n',

>>> r'\n'
'\\n'
>>> print r'\n'
\n

        在Windows操作系统中,会有一个典型的例子:

>>> f = open('C:\windows\temp\readme.txt', 'r')
Traceback (most recent call last):
File "<stdin>", line 1, in ?
f = open('C:\windows\temp\readme.txt', 'r')
IOError: [Errno 2] No such file or directory: 'C:\\windows\\temp\readme.txt'

        显然是因为'\t'和'\r'被当成了转义字符处理,所以要想正确执行操作,应该使用原始字符串:

>>> f = open(r'C:\windows\temp\readme.txt', 'r')


(4)Unicode字符串操作符(u/U)

        在字符串前加上'u'或'U'即可将原来的字符串转换为Unicode字符串,如下:

>>> u'xpleaf'
u'xpleaf'

        当然也可以在原始字符串中使用Unicode字符串,只是'u'必须在'r'前面,如下:

>>> ur'Hello\nWorld!'
u'Hello\\nWorld!'
>>> ru'Hello\nWorld!'
  File "<stdin>", line 1
    ru'Hello\nWorld!'
                    ^
SyntaxError: invalid syntax




5.内建函数


(1)标准类型函数

        同比较操作符一样,cmp()函数也字符串的ASCII码值进行比较:

>>> str1 = 'abc'
>>> str2 = 'lmn'
>>> str3 = 'xyz'
>>> cmp(str1, str2)
-1
>>> cmp(str3, str1)
1
>>> cmp(str2, 'lmn')
0


(2)序列类型操作符

  • len()

>>> len('xpleaf')
6
  • max()和min():返回ASCII值最大/最小的字符

>>> max('xpleaf')
'x'
>>> min('xpleaf')
'a'
  • enumerate(iter):接受一个可迭代对象作为参数,返回一个enumerate对象(也是可迭代对象),该对象生成由iter每个元素的index值和item值组成的元组

>>> s = 'xpleaf'
>>> enumerate(s)
<enumerate object at 0x7fd83b7e1eb0>
>>> list(enumerate(s))
[(0, 'x'), (1, 'p'), (2, 'l'), (3, 'e'), (4, 'a'), (5, 'f')]
>>> for index, value in enumerate(s):
...   print index, value
... 
0 x
1 p
2 l
3 e
4 a
5 f
  • zip(seq1[, seq2[...]):返回一个包含元组的列表,即zip(seq1 [, seq2 [...]]) -> [(seq1[0], seq2[0] ...), (...)]

>>> zip('123')
[('1',), ('2',), ('3',)]
>>> zip('123', 'abc')
[('1', 'a'), ('2', 'b'), ('3', 'c')]
>>> zip('123', 'abc', 'def')
[('1', 'a', 'd'), ('2', 'b', 'e'), ('3', 'c', 'f')]


(3)字符串类型函数

  • raw_input():读取用户输入的字符串

>>> user_input = raw_input('Enter your name:')
Enter your name:xpleaf
>>> len(user_input)
6

        不过需要注意的是,Python的字符串没有所谓的结束字符,所以输入了多少个字符,长度就是多少。

  • str()和unicode()

        前面已经有提及,Python有三种类型的字符串:str  unicode和basestring,可以使用type进行验证:

>>> type('xpleaf')
<type 'str'>
>>> type(u'xpleaf')
<type 'unicode'>

        因此str()和unicode()都是工厂函数,生成对应类型的对象:

>>> unicode('xpleaf')
u'xpleaf'
>>> str(u'xpleaf')
'xpleaf'

        由于它们都是不同的类型,显然可以使用标准内建函数isinstance进行类型的判断:

>>> isinstance('xpleaf', str)
True
>>> isinstance('xpleaf', unicode)
False
>>> isinstance('xpleaf', basestring)
True
>>> isinstance(u'xpleaf', basestring)
True

        从上面的例子也可以验证,str和unicode虽然是两种不同的类型,但它们都是basestring的子类,这点在前面已经有提及。

  • chr()  unichr()ord()

        关于这个函数,在`《Python回顾与整理3:数字》6(3)(b)ASCII转换函数`中已有提及,这里不再重复说明。




6.字符串内建函数


        这里所谓字符串内建函数,指的是可以通过`myString.method()`方式执行的方法。

        字符串方法实现了string模块中的大部分方法,目前字符串内建支持的方法(包含了对Unicode的支持)可以参考书中的表格,由于太多,就不一一列出,需要时查看就可以了。

        需要使用这些内建函数时,其实也不一定要找书,如果在IPython上,可以使用下面的方法:

In [15]: quest
Out[15]: 'what is you favorite color?'

In [16]: quest.
quest.capitalize  quest.isalnum     quest.lstrip      quest.splitlines
quest.center      quest.isalpha     quest.partition   quest.startswith
quest.count       quest.isdigit     quest.replace     quest.strip
quest.decode      quest.islower     quest.rfind       quest.swapcase
quest.encode      quest.isspace     quest.rindex      quest.title
quest.endswith    quest.istitle     quest.rjust       quest.translate
quest.expandtabs  quest.isupper     quest.rpartition  quest.upper
quest.find        quest.join        quest.rsplit      quest.zfill
quest.format      quest.ljust       quest.rstrip      
quest.index       quest.lower       quest.split       

In [16]: help(quest.split)

        如果只是在普通的Python交互器中,也可以使用下面的方法:

>>> s = 'xpleaf'
>>> dir(s)
['__add__', '__class__', '__contains__', '__delattr__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__getitem__', '__getnewargs__', '__getslice__', '__gt__', '__hash__', '__init__', '__le__', '__len__', '__lt__', '__mod__', '__mul__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__rmod__', '__rmul__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '_formatter_field_name_split', '_formatter_parser', 'capitalize', 'center', 'count', 'decode', 'encode', 'endswith', 'expandtabs', 'find', 'format', 'index', 'isalnum', 'isalpha', 'isdigit', 'islower', 'isspace', 'istitle', 'isupper', 'join', 'ljust', 'lower', 'lstrip', 'partition', 'replace', 'rfind', 'rindex', 'rjust', 'rpartition', 'rsplit', 'rstrip', 'split', 'splitlines', 'startswith', 'strip', 'swapcase', 'title', 'translate', 'upper', 'zfill']
>>> help(s.split)

        这样的话,就可以很清楚地知道有多少个内建函数,同时每一个内建函数的作用是什么。




7.字符串的独特特性


(1)特殊字符串和控制字符

        一个反斜杠加一个单一字符就表示一个特殊字符,通常是不可打印的字符,因此可以考虑用这样的字符来作数据库中某一数据项的定界符。但是当这些特殊字符用在原始字符串中时,它就失去了转义的功能。下面是Python支持的转义字符:

反斜杠开头的转义字符
标识八进制十进制十六进制字符说明
\000000x00NULL空字符NULL
\a00770x07BEL响铃字符
\b01080x08BS退格
\t01190x09HT横向制表符
\n012100x0ALF换行
\v013110x0BVT纵向制表符
\f014120x0CFF换页
\r015130x0DCR回车
\e033270x1BESC转义
\"042340x22"双引号
\'047390x27'单引号
\\134920x5C\反斜杠

        因为合法的ASCII值范围是0~255,所以对应的八进制和十六进制也有一个范围:

  • 八进制:000~0177

  • 十六进制:0x00~0xff

        另外除了可以使用标识符来表示转义字符本身,也可以使用相对应的八进制、十进制和十六进制来表示:

>>> print 'xpleaf\tclyyh'                #标识符
xpleaf	clyyh
>>> print 'xpleaf%cclyyh' % 011   #八进制
xpleaf	clyyh
>>> print 'xpleaf%cclyyh' % 9        #十进制
xpleaf	clyyh
>>> print 'xpleaf%cclyyh' % 0x09  #十六进制
xpleaf	clyyh


(2)三引号

        通过使用三引号,就可以允许一个字符串跨多行,字符串中可以包含换行符、制表符、以及其他特殊字符,这样的话就可以不用使用转义字符那么麻烦了,如下:

>>> hi = '''I am xpleaf,
... and I love cl.'''
>>> hi            #rper()形式输出
'I am xpleaf,\nand I love cl.'
>>> print hi    #str()形式输出
I am xpleaf,
and I love cl.


(3)字符串不变性

        这点在前面已经有所提及,即字符串本身是不可变的数据类型,要对修改字符串,其实都是通过生成一个新的字符串对象来实现,这个过程中对于内存的控制,由Python替我们管理,因此可以不用太过于纠结这中间发生了什么。

        下面是一个典型的例子:

>>> s = 'xpleaf'
>>> id(s)
140566687161200
>>> s = s + '\tclyyh'
>>> s
'xpleaf\tclyyh'
>>> id(s)
140566652843432

        如果尝试修改字符串的其中的一个字符,就会出错:

>>> s = 'xpleaf'
>>> s[2] = 'L'
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'str' object does not support item assignment

        即对于像字符串这样的不可变对象,在赋值操作中,左值必须是一个完整的对象,比如是一个字符串对象,而不能是字符串的一部分,当然右值就没有这样的限制了。所以对于上面的例子,如果真的要修改字符串中的某一个字符,一般采用下面的方法来“修改”:

>>> s[2]
'l'
>>> s = '%sL%s' % (s[0:2], s[3:])
>>> s
'xpLeaf'




8.Unicode


(1)术语

        下面是一些在字符编码中常会见到的术语:

Unicode术语
名词含义
ASCII美国标准信息交换码
BMP基本多文种平面(第零平面)
BOM字节顺序标记(标识字节顺序的字符)
CJK/CJKV中文-日文-韩文(和越南语)的缩写
Code point类似于ASCII值,代表Unicode字符的值,范围在range(114112)或者说从0x000000到0x10FFFFFF
Octet八位二进制数的位组
UCS通用字符集
UCS2UCS的双字节编码方式(见UTF-16)
UCS4UCS的四字节编码方式
UTFUnicode或者UCS的转换格式
UTF-88位UTF转换格式(无符号字节序列,长度为1~4个字节)
UTF-1616位UTF转换格式(无符号字节序列,通过是16位长[两个字节],见UCS2)

        当然,其实对于上面提及的这些术语,如果想要有比较透彻的理解,即从原理上去理解,建议是参考计算机组成原理的相关理论知识。


(2)什么是Unicode

        这里的总结将根据计算机组成原理的相关理论知识来进行总结。

        在Unicode之前,使用的是ASCII,ASCII非常简单,每个字符以7位二进制表示(第8位有其它用途,比如可以用来作扩展字符集使用,或者用来区分中文和英文字符),这样的话就可以表示2^7=128种字符,而实际上可打印的字符却只有95个,即使扩展到了8位(可打印字符增加到223个),也无法表示世界上成千上万种的语言。所以,必须要有一种可行的字符编码方案,能够表示全世界所有的语言。这样的话,Unicode就应运而生了。

        Unicode是国际组织制定的可以容纳世界上所有文字和字符的字符编码方案,它依随着通用字符集(后面会有总结)的标准而发展,至今仍在不断增修,第一个版本Unicode 1.0于1991年10月发布,到目前为止已经发布了近20个版本。

        Unicode可分为编码方式和实现方式两个层次:

  • 编码方式

        Unicode的编码方式与国际标准ISO/IEC 10646的通用字符集(Universal Character Set,UCS)概念相对应,两者的码表兼容,并共同调整任何未来的扩展。

  • 实现方式:Unicode编码格式

        Unicode的实现方式不同于编码方式。一个字符的Unicode编码是确定的,但是在实际传输过程中,由于不同系统平台的设计不一定一致,以及出于节省空间的考虑,Unicode编码的实现方式会有所不同。Unicode的实现方式称为Unicode转换格式(Unicode Translation Format,UTF),目前存在的UTF格式有UTF-7、UTF-7.5、UTF-8、UTF-16以及UTF-32.

        例如,UTF-8编码是一种变长编码,用1~6个字节编码Unicode字符。它将基本7位ASCII字符仍用7位编码表示,占用1个字节(首位补0)。而遇到与其他Unicode字符混合的情况,将按一定算法转换,UCS-2转换成UTF-8很可能需要3个字节,UCS-4转换成UTF-8很可能需要6个字节。这样以7位ASCII字符为主的西方文档就大大节省了编码长度。

        UTF-16比起UTF-8,好处在于大部分字符都以固定长度的字节(2字节)存储,但UTF-16却无法兼容ASCII编码。

        为了方便理解,后面会把UTF-8、UTF-16等的Unicode编码的实现方式称作为Unicode编码格式

  • 关系

        解析的时候以Unicode编码的标准来解析不同的字符,储存的时候将按照一定的算法将Unicode字符(兼容UCS字符集)以UTF的方式来进行存储。

        

(3)怎样使用Unicode

        将从下面的几个方面来进行讨论:

  • 尽量不要使用string模块

        string模块已经停止了更新,只保留了ASCII码的支持,string模块已经不推荐使用,在任何需要跟Unicode兼容的代码里都不要再用该模块,Python保留该模块仅仅是为了向后兼容。

  • Python把硬编码的字符串叫做字面上的字符串,默认所有字面上的字符串都用ASCII编码

        即默认显示出来的字符串使用的编码方式为ASCII,所以在使用中文字符时,一般都需要在中文字符前面加'u',以指定该中文的编码方式为Unicode编码,也许会有疑问,怎么可能会又有ASCII编码,又有Unicode编码,在同一个文件中,这可以实现吗?当然可以实现,这时候我们只需要指定Unicode编码的实现方式为UTF-8,就可以完成这样一个需求,即原来的英文字符还是用ASCII来实现,中文字符就用Unicode来实现,这就是UTF-8的强大之处,所以一般我们写代码时,都会指定Unicode编码的实现方式为UTF-8(Unicode编码是确定的,只是实现方式不确定而已,可以是UTF-8、UTF-16等,这都是为了节省存储空间)。

  • str()chr()unicode()unichar()

        内建的str()函数和chr()函数并没有升级成可以处理Unicode,它们只能处理常规的ASCII编码字符串,如果一个Unicode字符串被作为参数传给了str()函数,它会首先被转换成ASCII字符串然后再交给str()函数。所以如果Unicode字符串中包含不被ASCII字符串支持的字符,会导致str()函数报异常。同样地,chr()函数只能以0~255作为参数工作,如果接收的参数超出这个范围,也会报错。

        而新的内建函数unicode()和unichar()可以看成是Unicode版本的str()和chr()。unicode()函数可以把任何Python的数据类型转换成一个Unicode字符串,如果是对象,并且该对象定义了__unicode__()方法(即定义了一个钩子),它还可以把该对象转换成相应的Unicode字符串:

>>> class uniTest:
...   def __unicode__(self):
...     return 'xpleaf'
...   def __repr__(self):
...     return 'hello'
... 
>>> c = uniTest()
>>> str(c)
'hello'
>>> unicode(c)
u'xpleaf'

        当然,如果定义了__repr__()方法,那么使用unicode(c)或str(c)时都会有显示,因为repr()对Python本身的支持是比较好的(repr()为Python‘官方’字符串表示),如下面的例子:

>>> class Test():
...   def __repr__(self):
...     return 'OK'
... 
>>> c = Test()
>>> repr(c)
'OK'
>>> str(c)
'OK'
>>> unicode(c)
u'OK'

        相关内容可以参考本次总结的4(1)。


(4)Codec

        codec是Coder/DECoder的首字母组合,它定义了文本跟二进制值的转换方式。其实从计算机组成原理的角度去理解,codec就是Unicode的实现方式,即UTF-8、UTF-16等这些实现方式,也就是前面所说的Unicode编码格式(后面将其简称为编码),其实也就是字符串在磁盘或数据库中的储存方式


(5)编码解码

        先要理解“编码”与解码这两个术语的意思(提及的相关理论知识建议参考计算机组成原理):

  • 编码:encode

        即当将字符串保存在磁盘中时,应该以一种什么样的方式去保存这些字符串,即每个字符串应该要用多少个字节保存,现在常用的编码(格式)为UTF-8,因为UTF-8是一种变长编码,这样就使得编码具有很大的灵活性,从而可以大大节省存储空间。

  • 解码:decode

        即当从磁盘或文件中读取字符串时(其实保存起来的实体就是0101的二进制序列),应该以什么样的方式去解析这些字符,即一个字符应该占用了多少个字节(通过编码格式可以知道,这中间有一定的算法)。不过需要注意的是,要想正确地将字符串显示出来,还需要将这些UTF编码通过一定算法转换为UCS编码(UCS为字符集,里面有字符的二进制编码与这些字符的一一对应关系),然后才能正确的显示出来,这也就是为什么说,Unicode编码是固定的(每一个字符对应的Unicode编码是一定的,即在UCS字符集中,每一个字符都有唯一对应的二进制数值),而Unicode编码的实现方式(编码格式)是不确定的(每一个字符到底要用多少个字节存储起来),这两者之间存在一定的转换关系,而最终要想将字符显示出来,必须要参阅Unicode编码,即要转换为Unicode编码后才能知道该字符的形状(因为Unicode与UCS是兼容的,所以讲两者的其中之一都是没有问题的)

        所以每次我们写一个Unicode字符串到磁盘上我们都要用指定的编码器给他“编码一下”(Python默认是以ASCII进行编码),相应地,当我们从这个文件读取数据时,我们必须“解码”该文件,使之成为相应的Unicode字符串对象(另外,Pycharm默认以UTF-8进编解码,所以即使是以ASCII进行编码的文档也是可以正常打开的)。

        可以看下面的一个例子,这里使用UTF-8作为编码格式:

#!/usr/bin/env python

CODEC = 'utf-8'
FILE = 'unicode.txt'

hello_out = u'Hello world!\n'
bytes_out = hello_out.encode(CODEC)
f = open(FILE, 'w')
f.write(bytes_out)
f.close()

f = open(FILE, 'r')
bytes_in = f.read()
f.close()
hello_in = bytes_in.decode(CODEC)
print hello_in,

        执行结果如下:

$/usr/bin/python2.7 /home/xpleaf/PycharmProjects/Python_book/6/uniFile.py
Hello world!

        当去查看unicode.txt文件的大小时,发现该文件大小为13字节(刚好有13个字符),这也就验证了UTF-8为一种变长编码格式。


(6)把Unicode应用到实际应用中

        在实际使用Unicode时,应遵循下面的规则:

  • 程序中出现字符串是加个前缀'u'

  • 不要用str()函数,用unicode()代替

  • 不要用string模块——如果传给它的是非ASCII字符,它会把一切搞砸

  • 不到必须时不要在你的程序里面编码Unicode字符,只在你要写入文件或数据库或网络时,才调用encode()函数;相应地,只在你需要把数据读回来的时候才调用decode()函数


(7)一个非常现实的问题

        理论知识依然按照上面的去理解,然后再理解一下下面这段文字就可以了:

编码与解码

        首先,明确一点,计算机中存储的信息都是二进制的

        编码/解码本质上是一种映射(对应关系),比如‘a’用ascii编码则是65,计算机中存储的就是00110101,但是显示的时候不能显示00110101,还是要显示'a',但计算机怎么知道00110101是'a'呢,这就需要解码,当选择用ascii解码时,当计算机读到00110101时就到对应的ascii表里一查发现是'a',就显示为'a'

编码:真实字符与二进制串的对应关系,真实字符→二进制串

解码:二进制串与真实字符的对应关系,二进制串→真实字符

ASCII & UTF-8

        大家熟知的ASCII以1字节8个bit位表示一个字符,首位全是0,表示的字符集明显不够

        unicode编码系统是为表达任意语言而设计的,为了防止存储上的冗余(比如,对应ascii码的部分),其采用了变长编码,但变长编码给解码带来了困难,无法判断是几个字节表示一个字符

UTF-8是针对unicode变长编码设计的一种前缀码,根据前缀可判断是几个字节表示一个字符

150515095270251.png


        如果一个字节的第一位是0,则这个字节单独就是一个字符;如果第一位是1,则连续有多少个1,就表示当前字符占用多少个字节。

        比如"严"的unicode是4E25(100111000100101),4E25处在第三行的范围内(0000 0800-0000 FFFF),因此"严"的UTF-8编码需要三个字节,即格式是"1110xxxx 10xxxxxx 10xxxxxx"。然后,从"严"的最后一个二进制位开始,依次从后向前填入格式中的x,高位补0,得到"严"的UTF-8编码是"11100100 10111000 10100101"。


        有了上面的理论知识,看下面的一个例子:

>>> t = '严'
>>> print t
严
>>> t
'\xe4\xb8\xa5'
>>> t.encode('utf-8')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
UnicodeDecodeError: 'ascii' codec can't decode byte 0xe4 in position 0: ordinal not in range(128)

        为什么会这样?这与Python无关,给t赋值,实际就是给t赋二进制数值'\xe4\xb8\xa5',这是因为我们的系统显示默认使用的编码方式是utf-8(Python内部看到的只是普通的二进制'\xe4\xb8\xa5');而输出的时候,也是取决于系统显示默认使用的解码方式(Python内部看到的只是普通的二进制'\xe4\xb8\xa5')。但无论如何,此时t还只是一个str类型的字符串:

>>> type(t)
<type 'str'>

        所以在Python看来,t不过是一些二进制数的组合而已,即'\xe4\xb8\xa5',只是这些二进制数都超出了ASCII值的范围0~127,所以当执行t.encode('utf-8')编码时就报错了。正确应该是像下面这样:

>>> t = u'严'
>>> t
u'\u4e25'
>>> t.encode('utf-8')
'\xe4\xb8\xa5'

        当把类似的内容保存在一个文件中再执行时,现象如下:

xpleaf@leaf:~$ cat test.py 
u = '严'
xpleaf@leaf:~$ python test.py 
  File "test.py", line 1
SyntaxError: Non-ASCII character '\xe4' in file test.py on line 1, but no encoding declared; see http://python.org/dev/peps/pep-0263/ for details

        这里我们是以字符串类型(就是ASCII值)存储'严',即实际上是存储了二进制数值'\xe4\xb8\xa5',这还是跟我们的系统有关,我们输入'严'时,表面解析出来的是'严'(仍然是与我们的操作系统有关,即输入'严'时,操作系统帮我们将'严'转换成了utf-8码,然后再把它显示出来,但是Python是不认识它的,Python内部默认以ASCII编码解码),但计算机处理的则是二进制数值'\xe4\xb8\xa5',而这些二进制数值显然是超出了ASCII的范围,Python当然是无法识别,所以才会报错。

        当我们指定编码解码方式时,就可以正确识别了:

xpleaf@leaf:~$ cat test.py 
# coding: utf-8
u = '严'
xpleaf@leaf:~$ python test.py

         这里即使不指定u为unicode也是没有问题的,因为这里我并不需要编码,只是希望让Python可以识别'严'而已。当然你会想到把它变成下面的形式:

xpleaf@leaf:~$ python test.py 
Traceback (most recent call last):
  File "test.py", line 3, in <module>
    u.encode('utf-8')
UnicodeDecodeError: 'ascii' codec can't decode byte 0xe4 in position 0: ordinal not in range(128)

        肯定会报错,编码为utf-8,意味着u应该为unicode字符串,但这里不是。所以在执行encode时,encode发现它不是一个unicode字符(unicode字符串的标识类型是以u开头),于是认为它是ASCII字符串,所以就进行相应的处理(根据变长编码的规则,依然是执行ASCII编码),但很显然,这些二进制数值已经超出了ASCII的范围,所以肯定会报错了!


        通过前面所有的理论知识和这一小节的这些例子,相信对Python的Unicode总算是有了一个比较清楚的理解,当然,读者就不一定了,建议还是要自己亲自去尝试。


(8)Python的Unicode支持

  • 内建的unicode()函数:Unicode字符串工厂函数,接受string做参数,返回一个Unicode字符串

  • 内建的decode()/encode()方法

  • Unicode类型:Python数据类型,是basestring的子类

  • Unicode序数:指的是unichr()函数

  • 强制类型字符转换:混合类型字符串操作需要把普通字符串转换成Unicode对象

  • 字符串格式化操作符

        %s把Python字符串中的Unicode对象执行了str(u)操作,所以,输出的应该是u.encode(默认编码),如果格式化字符串是Unicode对象,所有的参数都将首先强制转换成Unicode,然后根据对应的格式串一起进行格式转换:数字首先被转换成普通字符串,然后再转换成Unicode;Python字符串通过默认编码格式转换成Unicode;Unicode对象不变。所以其他格式字符串都需要像上面这样转换,举例如下:

>>> u'%s %s' % (u'abc', 'abc')
u'abc abc'




9.相关模块


        Python标准库中与字符串有关的主要模块如下:

  • string

  • re

  • struct

  • c/StringIO

  • base64

  • codecs

  • crypt

  • difflib

  • hashlib

  • hma

  • md5

  • rotor

  • sha

  • stringprep

  • textwrap

  • unicodedata

        详细用法可以参考《Python标准库》、《Python参考手册》或Python官方文档。




10.字符串关键点总结


        没有太大的问题,主要是知道Python中没有字符数据类型,字符串是字符存储操作的最基本的单位,字符应该视为长度为1的字符串。



评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值