这一章的内容和第一章类似,主要说明如何实现很多Python类型中常见的特殊方法。
本章包含以下话题:
- 支持用于生成对象其他表现形式的内置函数(如
repr()
,bytes()
, 等等) - 使用一个类方法实现备选构造方法
- 扩展内置的
format()
函数和str.format()
方法使用的格式微语言。 - 实现只读属性
- 把对象变为可散列的,以便在集合中及作为
dict
的键使用 - 利用
__slots__
节省内存
我们还会讨论两个概念: - 如何以及何时使用
@classmethod
和staticmethod
装饰器 - Python 的私有属性和受保护属性的用法,约定和局限。
对象表现形式
Python提供了两种获取对象字符串表现形式的标准方法:
repr()
: 以便于开发者理解的方式返回对象的字符串表现形式。str()
: 以便于用户理解的方式返回对象的字符串表现形式。
我们要实现 __repr__
和 __str__
特殊方法,为 repr()
和 str()
提供支持。
为了给对象提供其他的表示形式,还会用到另外两个特殊方法:__bytes__
和 __format__
。__bytes__
方法与 __str__
方法类似:bytes()
函数调用它获取对象的字节序列表示形式。而 __format__
方法会被内置的 format()
函数和 str.format()
方法调用,使用特殊的格式代码显示对象的字符串表现形式。
- tips:
__repr__
,__str__
和__format__
都必须返回 Unicode 字符串(str类型)。只有__bytes__
方法应该返回字节序列(bytes
类型)。
接下来我们将实现一个向量类,并实现其几个特殊方法。
from array import array
import math
class Vector2d:
typecode = 'd' # typecode 是类属性, 在 Vector2d 实例和字节序列之间转换时使用
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))
# 定义 __iter__ 方法, 把Vector2d 实例变成可迭代的对象,这样就能拆包了, 另外这个使用了生成器表达式
def __repr__(self):
class_name = type(self).__name__
return '{}({!r}, {!r})'.format(class_name, *self)
# __repr__ 方法使用{!r} 获取各个分量的表示形式,然后插值,构成一个字符串;
# 因为Vector2d实例是可迭代的对象,所以*self会把x和y分量提供给 format 函数。
def __str__(self):
return str(tuple(self))
def __bytes__(self):
return (bytes([ord(self.typecode)]) +
bytes(array(self.typecode, self)))
# 为了生成字节序列,我们把 typecode 转换成字节序列,然后迭代Vector2d 实例
# 得到一个数组,再把数组转换成字节序列。
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))
我们已经定义了很多基本方法,但是显然少了一个操作:使用 bytes()
函数生成的二进制表示形式重建 Vector2d
实例。
备选构造方法
我们可以使用一个类方法来实现备选构造方法。
@classmethod
def frombytes(cls, octets):
typecode = chr(octets[0])
memv = memoryview(octets[1:]).cast(typecode)
return cls(*memv)
classmethod
与 staticmethod
classmethod
定义操作类,而不是操作实例的方法。clasmethod
改变了调用方法的方式,因此类方法的第一个参数是类本身,而不是实例。staticmethod
装饰器也会改变方法的调用方式,但是第一个参数不是特殊的值。其实,静态方法就是普通的函数,只是碰巧在类的定义体中,而不是在模块层定义。
class Demo:
@classmethod
def klassmeth(*args):
return args
@staticmethod
def statmeth(*args):
return args
print(Demo.klassmeth())
print(Demo.klassmeth('spam'))
print(Demo.statmeth())
print(Demo.statmeth('spam'))
"""
output:
(<class __main__.Demo at 0x10a678ae0>,)
(<class __main__.Demo at 0x10a678ae0>, 'spam')
()
('spam',)
"""
格式化显示
内置的 format()
函数和 str.format()
方法把各个类型的格式化方式委托给相应的 .__format__(format_spec)
方法。format_spec
是格式说明符,它是:
format(my_obj, format_spec)
的第二个参数,或者str.format()
方法的格式字符串, {} 里代换字段中冒号后面的部分。
一个例子
>>> br1 = 1/2.43
>>> br1
0.4115226337448559
>>> format(br1, '0.4f')
'0.4115'
>>> format(br1, '.4f')
'0.4115'
>>> '1 BRL = {rate:0.2f} USD'.format(rate=br1)
'1 BRL = 0.41 USD'
- 格式说明符使用的表示法叫格式规范微语言。
格式规范微语言为一些内置类型提供了专用的表示代码。比如 b
和 x
分别表示二进制和十六进制的 int
类型, f
表示小数形式的 float
类型, 而 %
表示百分数形式:
>>> format(42, 'b')
'101010'
>>> format(2/3, '.1%')
'0.0%'
>>> format(2/3.0, '.1%')
'66.7%'
格式规范微语言是可扩展的,因为各个类可以自行决定如何解释 format_spec
参数。如果类没有定义 __format__
方法,从 object
继承的方法会返回 str(my_object)
。
v1 = Vector2d(3, 4)
print(format(v1))
"""
output:
(3.0, 4.0)
"""
然而,如果传入格式说明符,object.__format__
方法会抛出 TypeError
v1 = Vector2d(3, 4)
print(format(v1, '.3f'))
"""
output:
Traceback (most recent call last):
File "b.py", line 39, in <module>
print(format(v1, '.3f'))
ValueError: Unknown format code 'f' for object of type 'str'
"""
我们将实现自己的微语言来解决这个问题。
def __format__(self, fmt_spec=''):
components = (format(c, fmt_spec) for c in self)
return '({}, {})'.format(*components)
可散列的Vector2d
按照定义,目前 Vector2d
实例是不可散列的,因此不能放入集合里面。
v1 = Vector2d(3, 4)
s = set()
s.add(v1)
"""
output:
Traceback (most recent call last):
File "b.py", line 45, in <module>
s.add(v1)
TypeError: unhashable instance
"""
为了把 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)
这样就满足了可散列的性质了。
Python 的私有属性和“受保护的属性”
Python没有专门的私有属性,但Python有个简单的机制,能避免子类意外覆盖“私有属性”。即以双下划线命名实例属性,Python 会把属性名存入实例的 __dict__
属性中,而且会在前面加上一个下划线和类名。因此对Dog类来说,__mood
会变成 _Dog__mood
; 对 Beagle
类来说, 会变成 _Beagle__mood
.这种语言特性叫名称改写(name mangling)
v1 = Vector2d(3, 4)
print(v1._Vector2d__x)
"""
output:
3.0
"""
使用 __slots__
类属性节省空间
默认情况下,Python 在各个实例中名为 __dict__
的字典里存储实例属性,为了使用底层的散列表提升访问速度,字典会消耗大量内存。如果要处理数百万个属性不多的实例,通过 __slots__
类属性,能节省大量内存,方法是让解释器在元祖中存储实例属性,而不用字典。
class Vector2d:
__slots__ = ('__x', '__y')
- 在类中定义
__slots__
属性之后,实例不能再有__slots__
中所列名称之外的其他属性。不要使用__slots__
属性禁止类的用户新增实例属性。 - 如果把
__dict__
这个名称添加到__slots__
中,实例会在元祖中保存各个实例的属性,此外还支持动态创建属性。 - 此外,还有一个实例属性可能需要注意,即
__weakref__
属性,为了让对象支持弱引用,必须有这个属性。用户定义的类中默认就有__weakref__
属性。如果类中定义了__slots__
属性,而且想把实例作为弱引用的目标,那么要把__weakref__
添加到__slots__
中。
覆盖类属性
Python 有个很独特的特性:类属性可用于为实例属性提供默认值。
>> from vector2d_v3 import Vector2d
>>> 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' # ➋
>>> dumpf = bytes(v1)
>>> dumpf
b'f\xcd\xcc\x8c?\xcd\xcc\x0c@'
>>> len(dumpf)
9
>>> Vector2d.typecode
'd'
如果想修改类属性的值,必须直接在类上修改,不能通过实例修改。
Vector2d.typecode = 'f'
然而有种修改方法更符合Python风格,而且效果持久,也更有针对性。即通过继承来修改。
>>> from vector2d_v3 import Vector2d
>>> class ShortVector2d(Vector2d):
... typecode = 'f'
...
>>> sv = ShortVector2d(1/11, 1/27)
>>> sv
ShortVector2d(0.09090909090909091, 0.037037037037037035)
>>> len(bytes(sv))
9