当前位置: 代码迷 >> 综合 >> Fluent Python - Part9 符合Python风格的对象
  详细解决方案

Fluent Python - Part9 符合Python风格的对象

热度:103   发布时间:2023-10-21 05:21:45.0

这一章的内容和第一章类似,主要说明如何实现很多Python类型中常见的特殊方法。

本章包含以下话题:

  • 支持用于生成对象其他表现形式的内置函数(如 repr(), bytes(), 等等)
  • 使用一个类方法实现备选构造方法
  • 扩展内置的 format() 函数和 str.format() 方法使用的格式微语言。
  • 实现只读属性
  • 把对象变为可散列的,以便在集合中及作为 dict 的键使用
  • 利用 __slots__ 节省内存
    我们还会讨论两个概念:
  • 如何以及何时使用 @classmethodstaticmethod 装饰器
  • 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 mathclass 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)

classmethodstaticmethod

  • classmethod 定义操作类,而不是操作实例的方法。clasmethod 改变了调用方法的方式,因此类方法的第一个参数是类本身,而不是实例。
  • staticmethod 装饰器也会改变方法的调用方式,但是第一个参数不是特殊的值。其实,静态方法就是普通的函数,只是碰巧在类的定义体中,而不是在模块层定义。
class Demo:@classmethoddef klassmeth(*args):return args@staticmethoddef statmeth(*args):return argsprint(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'
  • 格式说明符使用的表示法叫格式规范微语言。

格式规范微语言为一些内置类型提供了专用的表示代码。比如 bx 分别表示二进制和十六进制的 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)@propertydef x(self):return self.__x@propertydef y(self):return self.__ydef __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