本文主要是 Fluent Python 第 9 章的学习笔记。这部分主要是介绍了如何实现一个符合 Python 风格的类,包括常见的特殊方法、
__slots__
、@classmethod
和@staticmethod
装饰器、Python 私有属性和受保护属性的用法、约定和局限等。
每门面向对象的语言至少都有一种获取对象的字符串表示形式的标准方式。 Python 中提供了两种:
repr()
:以便于开发者理解的方式返回对象的字符串表示形式。
str()
:以便于用户理解的方式返回对象的字符串表示形式。
需要实现 __repr__
和 __str__
特殊方法为 repr()
和 str()
提供支持。此外还有 __bytes__
对应 bytes()
,__format__
对应内置的 format()
函数和 str.format()
方法。
实现一个向量类,并在里面重写一些特殊方法。
# vector2d_v0.py: 目前定义的都是特殊方法
from array import array
import math
class Vector2d:
typecode = 'd' # 类属性,在Vector2dd实例和字节序列之间转换时使用
def __init__(self, x, y):
self.x = float(x) # 转为浮点数
self.y = float(y)
def __iter__(self):
# 定义 __iter__ 方法把Vector2d实例变成可迭代对象,这样才能拆包
return (i for i in (self.x, self.y)) # 直接用生成器表达式就可以实现
def __repr__(self):
class_name = type(self).__name__
return '{}({!r}, {!r})'.format(class_name, *self) # {!r}获取各分量的表示形式,然后插值,*self 会把x和y提供给format函数
# return f'{class_name}({repr(self.x)}, {self.y})'
def __str__(self):
return str(tuple(self)) # 从可迭代的 Vector2d 轻松得到一个元组,显示为一个有序对
def __bytes__(self):
return (bytes([ord(self.typecode)]) + # 把 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) # 模式 x 和 y 分量构成的直接三角形斜边
def __bool__(self):
return bool(abs(self)) # 使用 abs(self) 计算模,把结果转为布尔值,0.0 为 False,非零值是 True
v1 = Vector2d(3, 4)
print(v1.x, v1.y)
x, y = v1
print(x, y)
print(v1)
v1_clone = eval(repr(v1))
print(v1 == v1_clone)
octets = bytes(v1)
print(octets)
print(abs(v1))
print(bool(v1), bool(Vector2d(0, 0)))
3.0 4.0
3.0 4.0
(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
实现把字节序列转换成 Vector2d 实例,需要为 Vector2d 定义一个同名 frombytes 类方法:
@classmethod # 装饰器
def frombytes(cls, octets): # 类方法第一个参数是 cls
typecode = chr(octets[0]) # 从第一个字节读取 typecode
memv = memoryview(octets[1:]).cast(typecode)
return cls(*memv) # 拆包转换后的 memoryview,得到构造方法所需的一对参数
classmethod
:类方法,即定义操作类的方法,而不是操作实例的方法。classmethod 改变了方法的调用方式,因此类方法第一个参数是 cls,而不是实例(self)。classmethod 最常见的用途是定义备选构造方法。按照约定,类方法第一个参数名为 cls。(Python 不强制要求)
staticmethod
:也会改变方法的调用方式,但是第一个参数不是特殊的值。其实静态方法就是普通函数,只是碰巧在类的定义体重,而不是在模块层定义。
下面的例子对比了 classmethod 和 staticmethod。
# 比较 classmethod 和 staticmethod 的行为
class Demo:
@classmethod
def klassmeth(*args):
return args # 返回全部位置参数
@staticmethod
def statmeth(*args):
return args # statmeth 也是
print(Demo.klassmeth()) # 不管怎么调用 Demo.klassmeth,它的第一个参数始终是 Demo 类
print(Demo.klassmeth('Spam'))
print(Demo.statmeth())
(,)
(, 'Spam')
()
classmethod 装饰器非常有用,不过 staticmethod 装饰器确可以替代,因为可以在模块中直接定义函数,而不用在类中定义。因此 staticmethod 不是特别有用。
为用户自定义的类型扩展格式规范微语言。如下:
# Vector2d.__format__ 方法,能计算极坐标
def angle(self):
return math.atan2(self.y, self.x)
def __format__(self, fmt_spec=''):
if fmt_spec.endswith('p'): # 如果格式代码以‘p’ 结尾,使用极坐标
fmt_spec = fmt_spec[:-1] # 从 fmt_spec 删除 'p' 后缀
coords = (abs(self), self.angle()) # 构建(magnitude, angle) 表示极坐标
outer_fmt = '<{}, {}>' # 把外层格式设为一对尖括号
else:
coords = self
outer_fmt = '({}, {})' # 外层格式设为一对圆括号
components = (format(c, fmt_spec) for c in coords) # 把各个分量生成可迭代对象,构成格式化字符串
return outer_fmt.format(*components)
为了实现 Vector2d 可散列,必须使用 __hash__
方法,还需要 __eq__
方法,以及让向量不可变。
给 Vector2d 添加 __hash__
方法:
def __hash__(self):
return hash(self.x) ^ hash(self.y) # 最好使用位运算符异或(^)混合各分量的散列值
实现让向量不可变:
def __init__(self, x, y):
self.__x = float(x)
self.__y = float(y)
@property
def y(self):
return self.__y
@property
def x(self):
return self.__x
# Vector2d 类完整版
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 y(self):
return self.__y
@property
def x(self):
return self.__x
def __iter__(self):
# 定义 __iter__ 方法把Vector2d实例变成可迭代对象,这样才能拆包
return (i for i in (self.x, self.y)) # 直接用生成器表达式就可以实现
def __repr__(self):
class_name = type(self).__name__
return '{}({!r}, {!r})'.format(class_name, *self) # {!r}获取各分量的表示形式,然后插值,*self 会把x和y提供给format函数
# return f'{class_name}({repr(self.x)}, {self.y})'
def __str__(self):
return str(tuple(self)) # 从可迭代的 Vector2d 轻松得到一个元组,显示为一个有序对
def __bytes__(self):
return (bytes([ord(self.typecode)]) + # 把 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) # 模式 x 和 y 分量构成的直接三角形斜边
def __bool__(self):
return bool(abs(self)) # 使用 abs(self) 计算模,把结果转为布尔值,0.0 为 False,非零值是 True
def angle(self):
return math.atan2(self.y, self.x)
def __format__(self, fmt_spec=''):
if fmt_spec.endswith('p'): # 如果格式代码以‘p’ 结尾,使用极坐标
fmt_spec = fmt_spec[:-1] # 从 fmt_spec 删除 'p' 后缀
coords = (abs(self), self.angle()) # 构建(magnitude, 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): # 类方法第一个参数是 cls
typecode = chr(octets[0]) # 从第一个字节读取 typecode
memv = memoryview(octets[1:]).cast(typecode)
return cls(*memv) # 拆包转换后的 memoryview,得到构造方法所需的一对参数
print('测试 hashing')
v1 = Vector2d(3, 4)
v2 = Vector2d(3.1, 4.2)
print(hash(v1), hash(v2))
print(len(set([v1, v2])))
print('测试 x 和 y 只读性')
print(v1.x, v1.y)
# v1.x = 123 # 报错 ttributeError: can't set attribute
print('测试 format()')
print(format(Vector2d(1, 1), 'p'))
print(format(Vector2d(1, 1), '0.5fp'))
测试 hashing
7 384307168202284039
2
测试 x 和 y 只读性
3.0 4.0
测试 format()
<1.4142135623730951, 0.7853981633974483>
<1.41421, 0.78540>
Python 中用 __XXX
表示私有属性,Python 会把私有属性名存入实例的 __dict__
属性中,而且会在前面加上一个下划线和类名,即 _classname__XXX
,这个语言特性叫名称改写(name mangling)。私有属性是一种保护措施,实际上我们也可以访问到,通常我们在调试或序列化才这么做。
不过由于有些 Python 程序员不喜欢名称改写功能,所以约定使用一个下划线前缀编写“受保护”的属性(如 self._x
)。
Python 解释器不会对使用单个下划线的属性名做特殊处理,不过这是很多 Python 程序员遵守的约定,他们不会再类外部访问这种属性。
__slots__
类属性节省空间在类中定义 __slots__
属性的目的是告诉解释器:这个类中的所有实例属性都在这里。这样 Python 会在各个实例中使用类似元组的结构存储实例变量,从而避免使用消耗内存的 __dict__
属性,从而节省内存。
定义 __slots__
属性的方式是,创建一个类属性,使用 __slots__
这个名字,并把它的值设为一个字符串构成的可迭代对象,其中各个元素表示各个实例属性。作者推荐元组,因为这样定义的 __slots__
中所含的信息不会变化。
__slots__
的问题:
__slots__
属性,因为解释器会忽略继承的 __slots__
属性。__slots__
列出的属性,除非把 __dict__
加入 __slots__
中(这样做就是去了节省内存的功效)。__weakref__
加入 __slots__
,实例就不能成为弱引用的目标。如果你的程序不用处理数百万个实例,一般情况下都不会使用 __slots__
属性。权衡需求证明有必要才使用它。
Python 有个很独特的特性:类属性可以为实例属性提供默认值。
实例属性如果和类属性同名,那么实例属性会被优先访问,类属性不受影响。
如果想修改类属性的值,必须直接在类上修改,不能通过实例修改。如:Vector2d.typecode = 'f'
。
更符合 Python 风格,且效果持久,更有针对性的修改方法是,创建一个子类,只用于定制类的数据属性。Django 基于类的视图大量用了这个技术。如:
# ShortVector2d 是 Vector2d 的子类,只用于覆盖 typecode 的默认值
class ShortVector2d(Vector2d):
typecode = 'f'
sv = ShortVector2d(1/11, 1/27)
print(sv)
print(len(bytes(sv)))
(0.09090909090909091, 0.037037037037037035)
9