对象表示形式
所谓对象表示形式,就是将对象转换为一种可读的形式。
Python提供了两种方式:
repr()
:以便于开发者理解的方式返回对象的字符串表示形式str()
:以便于用户理解的方式返回对象的字符串表示形式
我们只要实现 __repr__
和__str__
特殊方法,就可以为repr()
和str()
提供支持。
为了给对象提供其他的表示形式,还会用到另外两个特殊方
法:__bytes__
和 __format__
。__bytes__
方法与 __str__
方
法类似:bytes()
函数调用它获取对象的字节序列表示形式。而
__format__
方法会被内置的format()
函数和 str.format()
方
法调用。
向量类
我们使用Vector2d
类来说明对象表示形式的众多用法。
#vector2d_v0.py
from array import array
import math
class Vector2d:
typecode = 'd' #类属性,在实例和字节序之间转换时使用
def __init__(self,x,y):
self.x = float(x) #把x和y转换成浮点数,尽早捕获错误,防止传入不当参数
self.y = float(y)
def __iter__(self):
return (i for i in (self.x, self.y)) #把Vector2d实例变成可迭代的对象,这样才能拆包
def __repr__(self):
class_name = type(self).__name__
return '{}({!r}, {!r})'.format(class_name, *self) #因为 Vector2d 实例是可迭代的对象,所以 *self 会
#把 x 和 y 分量提供给 format 函数。
def __str__(self):
return str(tuple(self))
def __bytes__(self):
return (bytes([ord(self.typecode)]) + #把 typecode 转换成字节序列
bytes(array(self.typecode, self))) #迭代 Vector2d 实例,得到一个数组,再把数组转换成字节序列。
def __eq__(self, other):
return tuple(self) == tuple(other)
def __abs__(self):
return math.hypot(self.x,self.y) #模是 x 和 y 分量构成的直角三角形的斜边长
def __bool__(self):
return bool(abs(self))
在控制台调用如下:
>>> from vector2d_v0 import Vector2d
>>> v1 = Vector2d(3,4)
>>> print(v1.x,v1.y)
3.0 4.0
>>> x,y = v1 #Vecctor2d实例可以拆包成变量元组
>>> x,y
(3.0, 4.0)
>>> v1
Vector2d(3.0, 4.0)
>>> repr(v1)
'Vector2d(3.0, 4.0)'
>>> str(v1)
'(3.0, 4.0)'
>>> v1_clone = eval(repr(v1))
>>> v1 == v1_clone #支持==比较
True
>>> print(v1)
(3.0, 4.0)
>>> octets = bytes(v1)
>>> octets
b'd\x00\x00\x00\x00\x00\x00\x08@\x00\x00\x00\x00\x00\x00\x10@'
>>> abs(v1)
5.0
>>> bool(v1),bool(Vector2d(0,0))
(True, False)
备选构造方法
我们可以把 Vector2d
实例转换成字节序列了;同理,也应该能从字
节序列转换成 Vector2d
实例。
主要在上面的类中增加如下类方法:
@classmethod #类方法使用classmethod装饰器修饰
def frombytes(cls,octets): #通过cls传入类本身
typecode = chr(octets[0]) #从第一个字节中读取typecode
memv = memoryview(octets[1:]).cast(typecode) #通过传入的字节序列创建一个memoryview,然后使用typecode转换
return cls(*memv) #拆包转换后的memoryview,得到构造方法所需的一对参数
classmethod与staticmethod
classmethod
定义操作类的方法,该方法的第一个参数是类本身。其最常见的用途是备选构造方法。
staticmethod
装饰器也会改变方法的调用方式,但是第一个参数不是特殊的值。其实,静态方法就是普通的函数。
In [1]: class Demo:
...: @classmethod
...: def klassmeth(*args):
...: return args
...: @staticmethod
...: def statmeth(*args):
...: return args
...:
In [2]: Demo.klassmeth()#不管怎么调用该方法,第一个参数始终是Demo
Out[2]: (__main__.Demo,)
In [3]: Demo.klassmeth('spam')
Out[3]: (__main__.Demo, 'spam')
In [4]: Demo.statmeth('spam') #它的行为与普通函数类似
Out[4]: ('spam',)
格式化显示
内置的 format()
函数和str.format()
方法把各个类型的格式化方
式委托给相应的 .__format__(format_spec)
方
法。format_spec
是格式说明符,它是:
format(my_obj, format_spec)
的第二个参数,或者str.format()
方法的格式字符串,{}
里代换字段中冒号后面的部分
In [5]: brl = 1/2.43
In [6]: brl
Out[6]: 0.4115226337448559
In [7]: format(brl,'0.4f')
Out[7]: '0.4115'
In [8]: '1 BRL = {rate:0.2f} USD'.format(rate=brl) #rate被brl替换
Out[8]: '1 BRL = 0.41 USD'
{rate:0.2f}'
这样的格式字符串其实包含两部分,
冒号左边的 rate
在代换字段句法中是字段名,冒号后面的 0.2f
是格式说明符。格式说明符使用的表示法叫格式规范微语言。
格式规范微语言是可扩展的,因为各个类可以自行决定如何解释
format_spec
参数。例如,datetime
模块中的类,它们的
__format__
方法使用的格式代码与strftime()
函数一样。下面是
内置的format()
函数和str.format()
方法的几个示例:
>>> from datetime import datetime
>>> now = datetime.now()
>>> format(now,'%H:%M:%S')
'16:20:38'
>>> "It's now {:%I:%M %p}".format(now)
"It's now 04:20 PM"
如果类没有定义 __format__
方法,从 object 继承的方法会返回
str(my_object)
。我们为Vector2d
类定义了 __str__
方法,因
此可以这样做:
>>> v1 = Vector2d(3,4)
>>> format(v1)
'(3.0, 4.0)'
接下来将实现自己的微语言。假设用户提供的格式说明符是用于格式化向量中各个浮点数分量的。我们想达到这样的效果:
>>> v1 = Vector2d(3, 4)
>>> format(v1)
'(3.0, 4.0)'
>>> format(v1, '.2f')
'(3.00, 4.00)'
>>> format(v1, '.3e')
'(3.000e+00, 4.000e+00)'
增加的方法如下:
def __format__(self, format_spec = ''):
components = (format(c,format_spec) for c in self) #使用内置的 format 函数把 fmt_spec 应用到向量的各个分量上
return '({}, {})'.format(*components)
可散列的Vector2d
为了让Vector2d
实例变成可散列的,必须实现__hash__
方法(还有__eq__
方法),还需让向量不可变。
#修改了构造方法
def __init__(self,x,y):
self.__x = float(x) #使用两个下划线把属性变为私有的
self.__y = float(y)
@property #把读值方法标记为特性
def x(self):
return self.__x
@property
def y(self):
return self.__y
def __hash__(self):
return hash(self.x) ^ hash(self.y)
下面对改造后的向量进行测试:
>>> from vector2d_v2 import Vector2d
>>> v1 = Vector2d(3,4)
>>> v2 = Vector2d(3.1,4.2)
>>> hash(v1),hash(v2)
(7, 384307168202284039)
>>> set([v1,v2])
{Vector2d(3.1, 4.2), Vector2d(3.0, 4.0)}
Python的私有属性和受保护的属性
Python 不能像 Java 那样使用 private
修饰符创建私有属性,但是
Python 有个简单的机制,能避免子类意外覆盖“私有”属性。
我们上面在属性x
名称前加了两个下划线变成了__x
,对于该类来说,__x
会变成_Vector2d__x
,这个语言特性叫名称改写。Python 会把这种属性名存入实例的__dict__
属性中,而且会在前面加上一个下划线和类名:
>>> v1.__dict__
{'_Vector2d__x': 3.0, '_Vector2d__y': 4.0}
>>> v1._Vector2d__x #如果知道了如何改写名称,还是可以访问到
3.0
但是有些人不喜欢这种句法,他们约定使用
一个下划线前缀编写“受保护”的属性(如self._x
)
Python 解释器不会对使用单个下划线的属性名做特殊处理,不过这是很
多 Python 程序员严格遵守的约定,他们不会在类外部访问这种属性。
使用__slots__
类属性节省空间
默认情况下,Python 在各个实例中名为 __dict__
的字典里存储实例属
性。为了使用底层的散列表提升访问速度,字典会消
耗大量内存。如果要处理数百万个属性不多的实例,通过 __slots__
类属性,能节省大量内存,方法是让解释器在元组中存储实例属性,而
不用字典。
定义 __slots__
的方式是,创建一个类属性,使用 __slots__
这个
名字,并把它的值设为一个字符串构成的可迭代对象,其中各个元素表
示各个实例属性。我喜欢使用元组,因为这样定义的 __slots__
中所
含的信息不会变化,如示例所示:
class Vector2d:
__slots__ = ('__x','__y')
在类中定义 __slots__
属性的目的是告诉解释器:“这个类中的所有
实例属性都在这儿了!”
覆盖类属性
Python 有个很独特的特性:类属性可用于为实例属性提供默认
值。Vector2d
中有个 typecode
类属性,__bytes__
方法两次用到
了它,而且都故意使用 self.typecode
读取它的值。因为
Vector2d
实例本身没有typecode
属性,所以 self.typecode
默
认获取的是 Vector2d.typecode
类属性的值。
但是,如果为不存在的实例属性赋值,会新建实例属性。假如我们为
typecode
实例属性赋值,那么同名类属性不受影响。然而,自此之
后,实例读取的self.typecode
是实例属性typecode
,也就是把
同名类属性遮盖了。借助这一特性,可以为各个实例的typecode
属
性定制不同的值。
Vector2d.typecode
属性的默认值是 ‘d’,即转换成字节序列时使
用 8 字节双精度浮点数表示向量的各个分量。如果在转换之前把
Vector2d
实例的typecode
属性设为 ‘f’,那么使用 4 字节单精度
浮点数表示各个分量:
>>> v1 = Vector2d(1.1,2.2)
>>> dumpd = bytes(v1)
>>> dumpd
b'd\x9a\x99\x99\x99\x99\x99\xf1?\x9a\x99\x99\x99\x99\x99\x01@'
>>> len(dumpd)
17
>>> v1.typecode = 'f' #修改v1实例的typecode属性
>>> dumpf = bytes(v1)
>>> dumpf
b'f\xcd\xcc\x8c?\xcd\xcc\x0c@'
>>> len(dumpf)
9
>>> Vector2d.typecode # 而类属性不变
'd'