2.5 __bytes__()方法
只有很少的情景需要我们把对象转换为字节。在第2部分“持久化和序列化”中,我们会详细探讨这个主题。
通常,应用程序会创建一个字符串,然后使用Python的IO类内置的编码方法将字符串转换为字节。对于大多数情况,这种方法就足够了。只有当我们自定义一种新的字符串时,我们会需要定义这个字符串的编码方法。
依据不同的参数,bytes()函数的行为也不同。
- bytes(integer):返回一个不可变的字节对象,这个对象包含了给定数量的0x00值。
- bytes(string):这个版本会将字符串编码为字节。其他的编码和异常处理的参数会定义编码的具体过程。
- bytes(something):这个版本会调用something.__bytes__()创建字节对象。这里不用编码或者错误处理参数。
基本的object对象没有定义__bytes__()。这意味着所有的类在默认情况下都没有提供__bytes__()方法。
在一些特殊情况下,在写入文件之前,我们需要将一个对象直接编码成字节。通常使用字符串并且使用str类型为我们提供字符串的字节表示会更简单。要注意,当操作字节时,没有什么快捷方式可以解码文件或者接口中的字节。内置的bytes类只能解码字符串,对于我们的自定义对象,是无法解码的。在这种情况下,我们需要解析从字节解码出来的字符串,或者我们可以显式地调用struct模块解析字节,然后基于解析出来的值创建我们的自定义对象。
下面我们来看看如何把Card编码和解码为字节。由于Card只有52个可能的值,所以每一张牌都应该作为一个单独的字节。但是,我们已经决定用一个字符表示suit,用另外一个字符表示rank。此外,我们还需要适当地重构Card的子类,所以我们必须对下面这些项目进行编码。
- Card的子类(AceCard、NumberCard、FaceCard)。
-
子类的__init__()参数。
注意,我们有一些__init__()方法会将一个数值类型的rank转换为一个字符串,导致丢失了原始的数值。为了使字节编码可逆,我们需要重新创建rank的原始数值。
下面是__bytes__()的一种实现,返回了Card、rank和suit的UTF-8编码。
def __bytes__( self ):
class_code= self.__class__.__name__[0]
rank_number_str = {'A': '1', 'J': '11', 'Q': '12', 'K': '13'}.get( self.rank, self.rank )
string= "("+" ".join([class_code, rank_number_str, self.suit,] ) + ")"
return bytes(string,encoding="utf8")
这种实现首先用字符串表示Card对象,然后将字符串编码为字节。这通常是最简单也是最灵活的方法。
当我们拿到一串字节时,我们可以将这串字节解码为一个字符串,然后将字符串转换为一个新的Card对象。下面是基于字节创建Card对象的方法。
def card_from_bytes( buffer ):
string = buffer.decode("utf8")
assert string[0 ]=="(" and string[-1] == ")"
code, rank_number, suit = string[1:-1].split()
class_ = { 'A': AceCard, 'N': NumberCard, 'F': FaceCard }[code]
return class_( int(rank_number), suit )
在上面的代码中,我们将字节解码为一个字符串。然后我们将字符串解析为数值。基于这些值,现在我们可以重建原始的Card对象。
我们可以像下面这样生成一个Card对象的字节表示。
b= bytes(someCard)
然后我们可以用生成的字节重新创建Card对象。
someCard = card_from_bytes(b)
需要特别注意的是,通常自己定义字节表示是非常有挑战性的,因为我们试图表示一个对象的状态。Python中已经内置了很多字节表示的方式,通常这些方法足够我们使用了。
如果需要定义一个对象底层的字节表示方式,最好使用pickle或者json模块。在第9章“序列化和保存——JSON、YAML、Pickle、CSV和XML”中,我们会详细探讨这个主题。