得益于Python数据模型,自定义类型的行为可以像内置类型那样自然。实现如此自然的行为,靠的不是继承,而是鸭子模型(duck typing)。
对象表示形式
获取对象的字符串表示形式的标准方式,Python提供了两种。
- repr() :返回便于开发者理解的方式返回对象的字符串,用__repr__实现 Python控制台变量打印就是调用的__repr__
- str():返回便于用户理解的方式返回对象的字符串,用__str__实现
其他的表示形式,还有:
- __bytes__:类似于__str__方法,bytes()函数调用它获取对象的字节序列表示形式。
- __format__:会被内置的format()函数和str.format()方法调用,适应特殊的格式代码显示对象字符串表示形式。
ps:在Python3中,__repr__/__str__和__format__都必须返回Unicode字符串(str类型)。只有__bytes__方法返回字节序列(bytes类型)
实现Pythonic的向量类
使用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
以上的知识点比较密集:
- 实例支持拆包,要实现__iter__方法,这样实例变成可迭代对象,才能拆包。直接调用生成器表达式实现。
- format中的占位符{!r}和%r 一样,返回对象本体。比如字符串'abc'就返回'abc' 而不是abc
- 内部方法获取当前类名:class_name = self.__class__.__name__ 或者是 class_name = type(self).__name__
- tuple函数接收的参数是iterable,由于前面已经实现了__iter__,self是可迭代的,所以可以使用tuple(self)
- array数组的第一个参数是类型,这里d表示双精度浮点数,将向量转换为数组,然后再把数组转换为字节序列。
- ord()函数的参数是一个字符(长度为1的字符串),返回对应字符的ASCII数值。
- 和上面 对应的函数是chr(), 参数是ASCII数值,返回对应的字符串。
- memoryview.cast能用不同的方式读写同一块内存数据,这里使用bytes第一位保存的类型作为参数
- cls指代的是本身类Vector2d,括号内拆包了memoryview内存视图:3.0,4.0 最终结果就是 Vector2d(3.0,4.0) 产生了一个新的实例。
classmethod与staticmethod
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
内置的format()函数和str.format()方法把各个类型的格式化方法委托给.__format__(format_sepc)方法,format_spec是格式说明符。
- format(obj, format_spec) 第二个参数是格式说明符。
- str.format()在字符串中,{}里代换字段中冒号后面的部分。
>>> 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'结果,那么在展示向量<r, θ>,其中的r是模,θ(读西塔)是弧度。'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
目前实现的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)表达式
知识点:
- 使用两个前导下划线__把属性标记为私有的。
- 使用@property装饰器把读值方法标记
- 后续在调用self.x和self.y时,去到了def x和def y函数中的行为,也就是返回了私有属性,不影响读取。但是不能修改了
让这些向量不可变,接下来才能实现__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的私有属性和受保护的属性
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这种写法强制导入
使用__slots__类属性节省空间
在默认情况下,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__就是让对象支持弱引用,用于自定义的类中默认就有。
总结:
- 每个子类都要定义__slots__属性,因为解释器会忽略继承__slots__
- 实例只能有__slots__列出的属性。
- 如果不把'__weakref__'加到__slots__中, 实例就不能作为弱引用的目标。
类属性
类属性可以为实例属性提供默认值。
比如在上述代码中,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__获取,这样在子类继承后也能继续使用。