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

这一章的内容和第一章类似,主要说明如何实现很多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 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)

classmethodstaticmethod

  • 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'
  • 格式说明符使用的表示法叫格式规范微语言。

格式规范微语言为一些内置类型提供了专用的表示代码。比如 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)

    @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
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值