bytes实例包含原始数据,即8位无符号值(通常按照ASCII编码标准显示)。a=b'h\x65llo'print(list(a))print(a)>>>[104,101,108,108,111]b'hello'str实例包含Unicode码点(codepoint,也叫码点),这些码点对应人类语言中的文本字符。a='a\u0300propos'print(list(a))print(a)>>>['a','`','','p','r','o','p','o','s']àpropos大家一定要记住:str实例不一定非要按照某种固定方案编码成二进制数据,bytes实例也不一定非要按照某种固定方案解码成字符串。要将Unicode数据转换成二进制数据,必须调用str的encode方法。要将二进制数据转换成Unicode数据,必须调用bytes的decode方法。在调用这些方法的时候,可以明确指明要使用的编码方案,也可以使用系统默认的方案,通常指的是UTF-8(但有时也不一定,这个问题下面会讲到)。在编写Python程序时,必须将解码和编码操作放在接口的最外层,这样程序的核心部分才能使用Unicode数据进行操作。这种方法通常称为Unicode三明治(Unicodesandwich)。程序的核心部分应该使用str类型来表示Unicode数据,不要锁定某种字符编码。这使得程序可以接受多种文本编码(如Latin-1、ShiftJIS和Big5)并将它们转换成Unicode,同时也保证输出的文本信息使用相同的标准(最好是UTF-8)编码。这两种不同的字符类型对应于Python中的两种常见用例:开发人员需要操作原始8位值的序列,这些值共同表示一个字符,该字符应以UTF-8或其他一些标准字符串进行编码。开发人员需要对通用Unicode字符串进行操作,而不是对特定编码的字符串进行操作。我们通常需要编写两个辅助函数(helperfunctions)来在这两种情况之间进行转换,以确保输入的值类型是开发者所期望的形式。第一个辅助函数接受bytes或str实例并返回str:defto_str(bytes_or_str):ifisinstance(bytes_or_str,bytes):value=bytes_or_str.decode('utf-8')else:value=bytes_or_strreturnvalue#Instanceofstrprint(repr(to_str)(b'foo')))print(repr(to_str('bar')))>>>'foo''bar'第二个辅助函数也接受bytes或str实例,但它返回字节:defto_bytes(bytes_or_str):ifisinstance(bytes_or_str,str):value=bytes_or_str.encode('utf-8')else:value=bytes_or_str返回值#Instanceofbytesprint(repr(to_bytes(b'foo')))print(repr(to_bytes('bar')))在Python中使用原始8位值与Unicode字符串时需要注意两个问题。第一个问题是bytes和str这两个类型看起来工作方式相同,但是它们的实例彼此不兼容,因此在传递字符序列时必须考虑它们的类型。您可以使用+运算符将字节添加到字节,str也可以。print(b'one'+b'two')print('one'+'two')>>>b'onetwo'onetwo但不能将str实例添加到bytes实例:b'one'+'two'>>>Traceback...TypeError:can'tconcatstrtobytesandcannotaddbytesinstancetostrinstance:'one'+b'two'>>>Traceback...TypeError:canonlyconcatenatestr(not"bytes")到strbytes和bytes使用二元运算符(binaryoperator)比较大小,str和str之间也是可以的:assertb'red'>b'blue'assert'red'>'blue'但是str实例不能和bytes实例比较:assert'red'>b'blue'反过来也是一样的,也就是说bytes实例不能和str实例比较:assertb'blue'<'red'判断bytes和str实例是否相等,一直求值为假(False),即使这两个实例表示完全相同的字符,也不相等。例如,在下面的例子中,它们所代表的字符串相当于ASCII编码中的foo。print(b'foo'=='foo')>>>False这两种类型的实例都可以出现在%运算符的右侧,以替换左侧格式字符串中的%s。print(b'red%s'%b'blue')print('red%s'%'blue')>>>b'redblue'redblue如果格式串是bytes类型,那么不能用a代替strinstance%s,因为Python不知道这个str应该编码成什么scheme类型,你可以使用bytes实例来替换其中的%s,问题是,这可能与你想要的结果不同。print('red%s'%b'blue')>>>redb'blue'将使系统调用bytes实例上的__repr__方法,然后用调用%s的结果替换格式字符串,因此程序会直接输出b'blue'而不是你想的输出blue本身。第二个问题出现在操作文件句柄时,这里的句柄指的是内置open函数返回的句柄。此类句柄默认需要使用Unicode字符串操作,不能使用原始字节。习惯了Python2的开发者特别容易出现这个问题,会导致程序出现奇怪的错误。比如在向文件写入二进制数据时,下面的写法其实是错误的。withopen('data.bin','w')asf:f.write(b'\xf1\xf2\xf3\xf4\xf5')>>>Traceback...TypeError:write()argumentmustbestr,notbytes程序异常发生是因为在调用open函数的时候指定了'w'模式,所以系统要求必须写成文本模式。如果你想使用二进制模式,你应该指定'wb'。在文本模式下,write方法接受包含Unicode数据的str实例,而不是包含二进制数据的bytes实例。所以,我们必须将模式更改为'wb'才能解决问题。withopen('data.bin','wb')asf:f.write(b'\xf1\xf2\xf3\xf4\xf5')在读取文件时也有类似的问题。比如要读出刚刚写入的二进制文件,就不能使用下面的写法。withopen('data.bin','r')asf:data=f.read()程序报错是因为调用open函数时指定了'r'模式,所以系统要求必须以文本方式读取模式。要以二进制格式读取,应指定“rb”。在文本模式下操作句柄时,系统使用二进制数据的默认文本编码方案。所以上面的写法会导致系统通过bytes.decode把这个数据解码成一个str字符串,然后再用str.encode把这个字符串编码成二进制值。但是,对于大多数系统来说,默认的文本编码方案是UTF-8,所以系统很可能会将b'\xf1\xf2\xf3\xf4\xf5'解码为UTF-8格式的字符串,所以会出现类似上面一个会出现。要修复错误,需要将模式更改为“rb”。withopen('data.bin','rb')asf:data=f.read()assertdata==b'\xf1\xf2\xf3\xf4\xf5'另一种修改是调用open函数时,显式指定编码standard通过encoding参数保证一些平台特有的行为不会干扰代码的运行效果。例如,假设刚刚写入文件的二进制数据表示使用'cp1252'标准编码的字符串(cp1252是一种旧的Windows编码方案),你可以这样写:withopen('data.bin','r',encoding='cp1252')asf:data=f.read()assertdata=='?òó??'这样程序就不会出现异常,但是返回的字符串也和读取原始字节数据返回的一样有很大的不同。通过这个例子,我们需要提醒自己注意当前操作系统的默认编码标准(可以执行python3-c'importlocale;print(locale.getpreferredencoding())'命令查看)看看是否它与您的期望一致。如果不确定,请在调用open时明确指定编码参数。关键点bytes包含一个8位值序列,而str包含一个Unicode代码点序列。我们可以编写辅助函数来保证程序接收到的字符序列确实是期望操作的类型(需要知道要操作的是Unicode码位还是原始8位值。将字符串编码为UTF-8标准,结果是一系列像这样的8位值)。bytes和str这两个实例不能与某些运算符(如>、==、+、%运算符)混合使用。从文件中读取二进制数据(或将二进制数据写入文件)时,应使用二进制模式打开文件,例如'rb'('wb')。如果要从文件中读取(或写入)Unicode数据,必须注意系统默认的文本编码方案。如果不确定,可以通过encoding参数明确指定。
