《Fluent Python》学习笔记:第 9 章 Pythonic 对象

本文主要是 Fluent Python 第 9 章的学习笔记。这部分主要是介绍了如何实现一个符合 Python 风格的类,包括常见的特殊方法、__slots__@classmethod@staticmethod 装饰器、Python 私有属性和受保护属性的用法、约定和局限等。

9.1 对象表示形式

每门面向对象的语言至少都有一种获取对象的字符串表示形式的标准方式。 Python 中提供了两种:

repr():以便于开发者理解的方式返回对象的字符串表示形式。
str():以便于用户理解的方式返回对象的字符串表示形式。

需要实现 __repr____str__ 特殊方法为 repr()str() 提供支持。此外还有 __bytes__ 对应 bytes()__format__ 对应内置的 format() 函数和 str.format() 方法。

9.2 再谈向量类

实现一个向量类,并在里面重写一些特殊方法。

# 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

9.3 备选构造方法

实现把字节序列转换成 Vector2d 实例,需要为 Vector2d 定义一个同名 frombytes 类方法:

@classmethod  # 装饰器
def frombytes(cls, octets):  # 类方法第一个参数是 cls
    typecode = chr(octets[0])  # 从第一个字节读取 typecode
    memv = memoryview(octets[1:]).cast(typecode)
    return cls(*memv)  # 拆包转换后的 memoryview,得到构造方法所需的一对参数

9.4 classmethod 和 staticmethod

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())
(<class '__main__.Demo'>,)
(<class '__main__.Demo'>, 'Spam')
()

classmethod 装饰器非常有用,不过 staticmethod 装饰器确可以替代,因为可以在模块中直接定义函数,而不用在类中定义。因此 staticmethod 不是特别有用。

9.5 格式化显示

为用户自定义的类型扩展格式规范微语言。如下:

# 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)

9.6 可散列的 Vector2d

为了实现 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>

9.7 Python 的私有属性和“受保护”属性

Python 中用 __XXX 表示私有属性,Python 会把私有属性名存入实例的 __dict__ 属性中,而且会在前面加上一个下划线和类名,即 _classname__XXX,这个语言特性叫名称改写(name mangling)。私有属性是一种保护措施,实际上我们也可以访问到,通常我们在调试或序列化才这么做。

不过由于有些 Python 程序员不喜欢名称改写功能,所以约定使用一个下划线前缀编写“受保护”的属性(如 self._x)。
Python 解释器不会对使用单个下划线的属性名做特殊处理,不过这是很多 Python 程序员遵守的约定,他们不会再类外部访问这种属性。

9.8 使用 __slots__ 类属性节省空间

在类中定义 __slots__ 属性的目的是告诉解释器:这个类中的所有实例属性都在这里。这样 Python 会在各个实例中使用类似元组的结构存储实例变量,从而避免使用消耗内存的 __dict__ 属性,从而节省内存。

定义 __slots__ 属性的方式是,创建一个类属性,使用 __slots__ 这个名字,并把它的值设为一个字符串构成的可迭代对象,其中各个元素表示各个实例属性。作者推荐元组,因为这样定义的 __slots__ 中所含的信息不会变化。

__slots__ 的问题:

  1. 每个子类都要定义 __slots__ 属性,因为解释器会忽略继承的 __slots__ 属性。
  2. 实例只能拥有 __slots__ 列出的属性,除非把 __dict__ 加入 __slots__ 中(这样做就是去了节省内存的功效)。
  3. 如果不把 __weakref__ 加入 __slots__,实例就不能成为弱引用的目标。

如果你的程序不用处理数百万个实例,一般情况下都不会使用 __slots__ 属性。权衡需求证明有必要才使用它。

9.9 覆盖类属性

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

巨人的肩膀

  1. 《Fluent Python》
  2. 《流畅的 Python》

后记:
我从本硕药学零基础转行计算机,自学路上,走过很多弯路,也庆幸自己喜欢记笔记,把知识点进行总结,帮助自己成功实现转行。
2020下半年进入职场,深感自己的不足,所以2021年给自己定了个计划,每日学一技,日积月累,厚积薄发。
如果你想和我一起交流学习,欢迎大家关注我的微信公众号每日学一技,扫描下方二维码或者搜索每日学一技关注。
这个公众号主要是分享和记录自己每日的技术学习,不定期整理子类分享,主要涉及 C – > Python – > Java,计算机基础知识,机器学习,职场技能等,简单说就是一句话,成长的见证!
每日学一技

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值