符合Python风格的对象
1.1 前言
本文是《流畅的Python》——符合Python风格的对象的读后笔记与总结。
在其他面向语言中构建自定义类型通常是通过继承和重载来实现,然而在Python中只要构建鸭子类型就可以实现自定义类型:只需按照预定行为实现对象所需的方法即可。
Python中有很多的魔法方法
,这是Python语言的魅力之一,通过实现所需的魔法方法,就可以实现功能强大的自定义类型。
通过学习实现一个简单的二维向量该有的方法来收获以下几点知识:
- 如何及何时使用
@classmethod
和@staticmethod
装饰器 - 如何自定义格式化说明符来实现自定义格式化
1.2 实现向量类
对象表示形式
在Python中获取对象字符串的表示形式有两种方法:
-
repr()
:以便于开发者理解的方式返回对象的字符串表示形式。内部实现了
__repr__
方法 -
str()
:以便于用户理解的方式返回对象的字符串表示形式。内部实现了
__str__
方法 -
bytes()
:获取对象的字节序列表示形式。内部实现了
__bytes__
方法 -
format()
:格式化字符串的表示形式。内部实现了
__format__
方法
from array import array
import math
class Vector2d:
"""自定义二维向量类"""
typecode = '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))
def __repr__(self):
# 通过元对象获取向量实例名称,返回便于开发者理解的格式化后的名称
class_name = type(self).__name__
return '{}({!r}, {!r})'.format(class_name, *self)
def __str__(self):
# 使用元组返回有序对的便于用户理解的对象字符串
return str(tuple(self))
def __bytes__(self):
# 返回向量对象的字节序列
return (bytes([ord(self.typecode)]) +
bytes(array(self.typecode, self)))
def __eq__(self, other):
# 通过构建元组判断两个向量是否相等
return tuple(self) == tuple(other)
def __abs__(self):
# 返回向量的模
return math.hypot(self.x, self.y)
def __bool__(self):
# 判断是否是一个向量
return bool(abs(self))
@classmethod
def frombytes(cls, octets):
"""从字节序列转换为向量对象"""
typecode = chr(octets[0])
memv = memoryview(octets[1:]).cast(typecode)
return cls(*memv)
运行结果:
>>> v1 =Vector2d(3, 4)
>>> v1
Vector2d(3.0, 4.0)
>>> x, y = v1
>>> x, y
(3.0, 4.0)
>>> v1_clone = eval(repr(v1))
>>> v1 == v1_clone
True
>>> bytes_v1 = bytes(v1)
>>> bytes_v1
b'd\x00\x00\x00\x00\x00\x00\x08@\x00\x00\x00\x00\x00\x00\x10@'
>>> Vector2d.frombytes(bytes_v1)
Vector2d(3.0, 4.0)
>>> abs(v1)
5.0
>>> bool(v1), bool(Vector2d(0, 0))
(True, False)
>>>
上边的实现方法中,__eq__
方法有个缺陷:具有相同数值的可迭代对象相比结果也为true。
>>> Vector2d(3, 4) == [3, 4]
True
1.3 classmethod与staticmethod
Python中的类方法就是只有类本身才能调用的方法,类的实例化对象是不能调用的。
类方法的第一个参数一定是代表类本身,一般使用cls
,但是也可以叫别的名字。
静态方法就是普通的函数,只是在类中定义而不是在模块层定义而已。
>>> class Demo:
... @classmethod
... def klassmeth(*args):
... return args
... @staticmethod
... def statmeth(*args):
... return args
...
>>> Demo.klassmeth()
(<class '__main__.Demo'>,)
>>> Demo.klassmeth('spam')
(<class '__main__.Demo'>, 'spam')
>>> Demo.statmeth()
()
>>> Demo.statmeth('spam')
('spam',)
1.4 格式化显示
使用内置的format()
函数和str.format()
方法格式化前边向量类。
格式说明符
对格式化对象的格式化要求:
format(my_obj, format_spec)
的第二个参数str.format()
方法的格式字符串,{}里代换字段中冒号后面的部分,冒号左边表示代换字段句法中的字段名。
>>> brl = 1/2.43 # 需要格式化的字段
>>> brl
0.4115226337448559
>>> format(brl, '0.4f') # brl代表格式化字段对象,'0.4f'代表格式化说明符
'0.4115'
# rate 代表格式化字段对象, 0.2f 代表格式化说明符
>>> '1 BRL = {rate:0.2f} USD'.format(rate=brl)
'1 BRL = 0.41 USD'
支持格式化说明符
使自定义向量类支持格式化说明符:
def __format__(self, fmt_spec=''):
components = (format(c, fmt_spec) for c in self)
return '({}, {})'.format(*components)
自定义格式化说明符
在Python的格式规范微语言中,整数使用的代码有bcdoxXn
, 浮点数使用的代码有eEfFGn%
,字符串使用的代码有s
。
我们在自定义格式化规范的时候不要使用这些已经被定义了的代码,比如说自定义向量类的格式化代码定位没有出现过的p
:格式为 <r, θ >
代表向量的模和角度。
# 返回向量的角度
def angle(self):
return math.atan2(self.y, self.x)
# 自定义格式化说明符,返回自定义格式化后的字段对象
def __format__(self, fmt_spec=''): # fmt_spec为传入的格式化说明符,默认为空
if fmt_spec.endswith('p'): # 如果格式化说明符为自定义的p就构造自定义格式化
fmt_spec = fmt_spec[:-1] # 删除p后缀
coords = (abs(self), self.angle()) # 构建需要格式化字段对象
outer_fmt = '<{}, {}>' # 构造格式化格式
else:
coords = self # 使用默认的格式化字段对象
outer_fmt = '({}, {})' # 构造格式化格式
# 接下来两步很经典,先根据官方定义的格式化规范格式化coords对象
# 然后在利用生成器表达式构造为可迭代对象
# 在将这个可迭代对象以Python中经典的拆包方式作为格式化字段对象来构造我们自定义的outer_fmt格式
components = (format(c, fmt_spec) for c in coords)
return outer_fmt.format(*components)
自定义格式化说明符步骤大概如下:
-
传入自定义格式化说明符
-
根据自定义格式化说明符来构建格式化字段对象和格式化后的格式。
这一步需要注意的是要将自定义格式化说明符的后缀(自定义格式化代码)删掉,因为在最后一步的格式化的时候使用的还是官方的格式化代码。
-
根据前边构造的格式化字段对象和格式化格式化说明符来初次格式化。
这次格式化主要是根据官方定义的格式化规范来构造格式化后的字段对象。
-
最后在按照我们定义的格式化后的格式和上步得到的字段对象来最终格式化。
>>> format(Vector2d(1, 1), 'p')
'<1.4142135623730951, 0.7853981633974483>'
>>> format(Vector2d(1, 1), '.3ep')
'<1.414e+00, 7.854e-01>'
>>> format(Vector2d(1, 1), '0.5fp')
'<1.41421, 0.78540>'
1.5 实现自定义类的可散列
首先要知道可散列对象一定是不可变的,不可变对象不一定是可散列的。
要想自定义对象是可散列的必须实现两个特殊方法:
-
__hash__
:返回一个hash值,表示为散列值,但是只有不可变对象才能求hash值,所以我们自定义的向量类要把属性设为不可变。class Vector2d: typecode = '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 __iter__(self): return (i for i in (self.x, self.y))
- 利用
property
把属性装饰为只读 __iter__
:需要读取x和y分量的方法可以保持不变,通过 self.x 和 self.y 读取公开特性,而不必读取私有属性 。
- 利用
-
__eq__
:相等对象应该有相同的散列值。
此时的向量是可散列的:
>>> 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)}
目前完整的自定义向量类的代码如下:
from array import array
import math
class Vector2d:
typecode = '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 __iter__(self):
return (i for i in (self.x, self.y))
def __repr__(self):
class_name = type(self).__name__
return '{}({!r}, {!r})'.format(class_name, *self)
def __str__(self):
return str(tuple(self))
def __bytes__(self):
return (bytes([ord(self.typecode)]) +
bytes(array(self.typecode, self)))
def __eq__(self, other):
return tuple(self) == tuple(other)
def __hash__(self):
return hash(self.x) ^ hash(self.y)
def __abs__(self):
return math.hypot(self.x, self.y)
def __bool__(self):
return bool(abs(self))
def angle(self):
return math.atan2(self.y, self.x)
def __format__(self, fmt_spec=''):
if fmt_spec.endswith('p'):
fmt_spec = fmt_spec[:-1]
coords = (abs(self), self.angle())
outer_fmt = '<{}, {}>'
else:
coords = self
outer_fmt = '({}, {})'
components = (format(c, fmt_spec) for c in coords)
return outer_fmt.format(*components)
@classmethod
def frombytes(cls, octets):
typecode = chr(octets[0])
memv = memoryview(octets[1:]).cast(typecode)
return cls(*memv)