得益于Python数据模型,自定义类型的行为可以像内置类型那样自然。实现如此自然的行为,靠的不是继承,而是鸭子模型(duck typing)。
获取对象的字符串表示形式的标准方式,Python提供了两种。
其他的表示形式,还有:
ps:在Python3中,__repr__/__str__和__format__都必须返回Unicode字符串(str类型)。只有__bytes__方法返回字节序列(bytes类型)
使用Python的特殊方法,实现一个Pythonic的向量类
- import math
- from array import array
-
-
- class Vector2d:
- type_code = 'd' # 类属性。在于实例和字节序列转换时使用。
-
- def __init__(self, x, y):
- self.x = float(x)
- self.y = float(y)
-
- def __iter__(self):
- return (i for i in (self.x, self.y)) # 使用迭代器(iterator)表达式
-
- def __repr__(self):
- class_name = type(self).__name__ # 自省类名
- return '{}({!r},{!r})'.format(class_name, *self) # *self拆包,用到了自己的__iter__方法。 !r 和%r一样 返回对象本体
-
- def __str__(self):
- # return '(%r,%r)' % (self.x, self.y)
- return str(tuple(self)) # 从可迭代对象可以轻松得到一个元组,然后转成字符串。tuple( iterable ),所以会调用__iter__
-
- def __eq__(self, other):
- return tuple(self) == tuple(other) # 构建成元组,可以快速比较所有分量。
-
- def __bytes__(self):
- """向量类转换为bytes"""
- return bytes([ord(self.type_code)]) + bytes(array(self.type_code, self)) # b'd' + b'\x00...'
-
- def __abs__(self):
- return math.hypot(self.x, self.y) # hypot()函数就是计算三角形的斜边长。在向量中就是模
-
- def __bool__(self):
- return bool(abs(self)) # 判断模是否为0
-
- @classmethod
- def from_bytes(cls, bytes_):
- """bytes转换为向量类"""
- type_code = chr(bytes_[0])
- memy = memoryview(bytes_[1:]).cast(type_code)
- return cls(*memy)
-
-
- v1 = Vector2d(3, 4)
-
- x, y = v1
- print('x,y:', x, y) # 支持拆包:__iter__
-
- print('v1:', v1) # 支持打印:先调用__str__,如果找不到再继续调用__repr__
- print('v1.__repr__():', v1.__repr__())
-
- v1_clone = eval(repr(v1))
- print(v1_clone == v1) # 支持比较相等:__eq__
-
- print(bytes(v1)) # 二进制表示形式:__bytes__
-
- print(abs(v1)) # 返回实例的模:__abs__
-
- print(bool(v1), bool(Vector2d(0, 0))) # 如果实例的模为0,返回FALSE;否者返回True
-
- 打印结果
- x,y: 3.0 4.0
- v1: (3.0, 4.0)
- v1.__repr__(): Vector2d(3.0,4.0)
- True
- b'd\x00\x00\x00\x00\x00\x00\x08@\x00\x00\x00\x00\x00\x00\x10@'
- 5.0
- True False
以上的知识点比较密集:
classmethod用法:定义操作类,而不是操作实例的方法。这种方法的第一个参数是类本身,而不是实例。最常见的用途是定义备选构造方法,就像上面示例中的from_bytes方法,返回的是一个新实例。
staticmethod用法:不需要实例作为参数。其实静态方法就是一个普通的函数,只是定义的类的定义体中,可能用于归类展示。
示例,简单比较
- class Demo:
-
- @classmethod
- def class_meth(*args):
- print(args)
-
- @staticmethod
- def static_meth(*args):
- print(args)
-
-
- d = Demo()
- d.class_meth()
- d.static_meth()
- 打印结果
- (<class '__main__.Demo'>,)
- ()
内置的format()函数和str.format()方法把各个类型的格式化方法委托给.__format__(format_sepc)方法,format_spec是格式说明符。
>>> brl = 1/2.43
>>> brl
0.4115226337448559
>>> format(brl, '0.4f')
'0.4115'
>>> '1 BRL = {rate:.4f} USD'.format(rate=brl)
'1 BRL = 0.4115 USD'
上面的{}中的rate是字段名,与格式说明符无关,用来决定把.format()的哪个参数传给代码字段。
冒号后面的'.4f'是格式说明符。
格式说明符使用的表示语法叫格式规范微语言。
格式规范微语言为一些内置类型提供了专用的表示代码。比如b和x分别表示二进制和十六进制的int类型,f表示小数形式的float类型,而%表示百分数形式。
>>> format(42,'b')
'101010'
>>> format(2/3,'.1%')
'66.7%'
格式规范微语言是可扩展的,因为各个类可以自行决定如何解释format_spec参数。例如,datatime模块中的类,它的__format__方法和strftime()函数一样。
>>> from datetime import datetime
>>> now = datetime.now()
>>> format(now, '%H:%M:%S')
'14:54:10'
>>> 'now {:%H:%M %p}'.format(now)
'now 14:54 PM'
如果类没有定义__format__方法,那么从object集成的方法会返回str(my_object)。
示例,让向量类支持格式说明符。
- ···省略其他代码
- def __format__(self, format_spec):
- return str(tuple(format(x, format_spec) for x in self))
-
-
- v1 = Vector2d(3, 4)
-
- print(format(v1))
- print(format(v1, '.2f')) # 保留2位小数
- print(format(v1, '.3e')) # 使用科学计数法,保留3位小数
- 打印:
- ('3.0', '4.0')
- ('3.00', '4.00')
- ('3.000e+00', '4.000e+00')
下面将再次扩展,让向量类支持自定义的格式代码。
格式说明符如果以'p'结果,那么在展示向量
- ···省略其他代码
- def __format__(self, format_spec=''):
- if format_spec.endswith('p'):
- # 自定义格式说明符 <模, 角度>
- format_spec = format_spec[:-1]
- coords = (abs(self), self.angle())
- outer_fmt = '<{}, {}>'
- else:
- coords = self
- outer_fmt = '({}, {})'
-
- components = (format(c, format_spec) for c in coords) # 'p'之外的格式说明符,应用到向量的各个分量上。
- return outer_fmt.format(*components)
-
-
- v1 = Vector2d(1, 1)
-
- print(format(v1, 'p'))
- print(format(v1, '.5fp'))
- print(format(v1, '.3ep'))
- 打印
- <1.4142135623730951, 0.7853981633974483>
- <1.41421, 0.78540>
- <1.414e+00, 7.854e-01>
目前实现的Vector2d实例是不可散列的,使用hash(v1)函数会报错.。
要实现可散列的对象,必须实现__hash__方法和__eq__方法,前面的代码中__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 __iter__(self):
- return (i for i in (self.x, self.y)) # 使用迭代器(iterator)表达式
知识点:
让这些向量不可变,接下来才能实现__hash__方法,这个方法返回一个整数。
示例2,实现__hash__方法
def __hash__(self): return hash(self.x) ^ hash(self.y)
使用^(位运算符 异或)来混合各分量的散列值。
ps:理想情况下还要考虑对象属性的散列值(__eq__方法也要使用,比较属性的散列值),因为相等的对象应该具有相同的散列值。
- import math
- from array import array
-
-
- class Vector2d:
- type_code = 'd' # 类属性。在于实例和字节序列转换时使用。
-
- 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) # 位运算符 异或^
-
- def __iter__(self):
- return (i for i in (self.x, self.y)) # 使用迭代器(iterator)表达式
-
- def __repr__(self):
- class_name = type(self).__name__ # 自省类名
- # class_name = self.__class__.__name__ # 自省类名,另一种写法
- return '{}({!r},{!r})'.format(class_name, *self) # *self拆包,用到了自己的__iter__方法。 !r 和%r一样 返回对象本体
-
- def __str__(self):
- # return '(%r,%r)' % (self.x, self.y)
- return str(tuple(self)) # 从可迭代对象可以轻松得到一个元组,然后转成字符串。tuple( iterable ),所以会调用__iter__
-
- def __eq__(self, other):
- return tuple(self) == tuple(other) # 构建成元组,可以快速比较所有分量。
-
- def __bytes__(self):
- """向量类转换为bytes"""
- return bytes([ord(self.type_code)]) + bytes(array(self.type_code, self)) # b'd' + b'\x00...'
-
- def __abs__(self):
- return math.hypot(self.x, self.y) # hypot()函数就是计算三角形的斜边长。在向量中就是模
-
- def __bool__(self):
- return bool(abs(self)) # 判断模是否为0
-
- @classmethod
- def from_bytes(cls, bytes_):
- """bytes转换为向量类"""
- type_code = chr(bytes_[0])
- memy = memoryview(bytes_[1:]).cast(type_code)
- return cls(*memy)
-
- # def __format__(self, format_spec):
- # return str(tuple(format(x, format_spec) for x in self))
-
- def angle(self):
- """计算角度"""
- return math.atan2(self.x, self.y)
-
- def __format__(self, format_spec=''):
- if format_spec.endswith('p'):
- # 自定义格式说明符 <模, 角度>
- format_spec = format_spec[:-1]
- coords = (abs(self), self.angle())
- outer_fmt = '<{}, {}>'
- else:
- coords = self
- outer_fmt = '({}, {})'
-
- components = (format(c, format_spec) for c in coords) # 'p'之外的格式说明符,应用到向量的各个分量上。
- return outer_fmt.format(*components)
Python不像Java那样有private修饰符创建私有属性,但是Python有个简单的机制,避免子类意外覆盖“私有”属性。
如果以__name这种形式(两个前导下划线)命名实例属性,Python会把属性名以特殊形式存入__dict__属性中,会在前面加上一个下划线和类名。这种语言特征叫做名称改写(name mangling)
示例,打印Vector2d类实例的__dict__
- v1 = Vector2d(1, 1)
- print(v1.__dict__)
- 打印结果
- {'_Vector2d__x': 1.0, '_Vector2d__y': 1.0}
名称改写是一种安全措施,目的是避免意外访问,不能防止故意修改,比如v1._Vector2d__x = 7 这种代码是可以正常给私有属性赋值的。
Python的另一个种属性是单个下划线,比如_name,这种一般称为“受保护的”属性,程序员们自我约定在类的外部不能访问这种属性。
在模块中的变量,如果使用一个单导线划线的话,会对导入有影响,比如from mymod import * 这种写法,就不会导入_name这种变量。
但是可以使用from mymod import _name这种写法强制导入
在默认情况下,Python在各个实例中名为__dict__的字典里面存储实例属性。但是字典的底层使用了散列表来提升访问速度,字典会消耗大量内存。
如果要处理数量为百万个属性,通过__slots__类属性,可以节省大量的内存,方法是让解释器在元组中存储实例属性,而不是字典。实际Python会在各个实例中使用类似元组的结构结构存储实例变量,避免使用__dict__属性消耗太多内存。
在类中定义__slots__就是告诉Python解释器:这个类的所有实例属性都在这了!
ps: 继承超类的__slots__并没有效果,Python只会使用各个类中定义的__slots_属性。
示例, 类属性__slots__的使用方法
- class Vector2d:
- __slots__ = ('__x', '__y')
- type_code = 'd' # 类属性。在于实例和字节序列转换时使用。
-
- def __init__(self, x, y):
- self.__x = float(x)
- self.__y = float(y)
在类中定义了__slots__属性之后,实例不能再有__slots__中之外的其他属性,也就是这会约束用于新增实例属性,算是一个副作用,所以不能滥用__slots__,推荐当实例可能产生数百万个时才建议使用。
如果想把实例作为弱引用的目标,要把'__weakref__'加到__slots__中。__weakref__就是让对象支持弱引用,用于自定义的类中默认就有。
总结:
类属性可以为实例属性提供默认值。
比如在上述代码中,Vector2d.typecode属性的默认值是'd',即转换为字节序列时使用8字节双浮点数。如果想使用'f',即4字节单精度浮点数,可以修改实例属性
v1.typecode = 'f' 然后再执行函数bytes(v1)
如果想要修改类属性,那么必须在类上修改:
Vector2d.typecode = 'f'
但是使用继承更符合Python的风格,而且效果更持久,也更有针对性。
class NewVector2d(Vector2d):
type_code = 'f'
这也说明了Vect2d.__repr__方法为何没有硬编码class_name的值,而是使用type(self).__name__获取,这样在子类继承后也能继续使用。