流畅的python--第十二章 序列的特殊方法

news/2024/10/3 9:24:32

Vector 类第 1 版:与 Vector2d 类兼容

示例 12-2 vector_v1.py:从 vector2d_v1.py 衍生而来

from array import array
import reprlib
import math
class Vector:typecode = 'd'def __init__(self, components):self._components = array(self.typecode, components)# ❶def __iter__(self):return iter(self._components) #❷def __repr__(self):components = reprlib.repr(self._components) #❸components = components[components.find('['):-1] #❹return f'Vector({components})'def __str__(self):return str(tuple(self))def __bytes__(self):return (bytes([ord(self.typecode)]) +bytes(self._components)) #❺def __eq__(self, other):return tuple(self) == tuple(other)def __abs__(self):return math.hypot(*self) #❻def __bool__(self):return bool(abs(self))@classmethoddef frombytes(cls, octets):typecode = chr(octets[0])memv = memoryview(octets[1:]).cast(typecode)return cls(memv) #❼

self._components 是“受保护”的实例属性,把 Vector 的分量保存在一个数组中。
❷ 为了迭代,使用 self._components 构建一个迭代器,作为返回值。
❸ 使用 reprlib.repr() 函数生成 self._components 的有限长度表
示形式(例如 array('d', [0.0, 1.0, 2.0, 3.0, 4.0,...]))
❹ 把字符串插入 Vector 的构造函数调用之前,去掉前面的array('d', 和后面的 )
❺ 直接使用 self._components 构建 bytes 对象。
❻ 从 Python 3.8 开始,math.hypot 接受 N 维坐标点。以前使用的表达式是 math.sqrt(sum(x * x for x in self))
❼ 只需在 frombytes 方法的基础上改动最后一行:直接把memoryview 传给构造函数,不用像前面那样使用 * 拆包。

协议和鸭子类型

在面向对象编程中,协议是非正式的接口,只在文档中定义,不在代码
中定义。例如,Python 的序列协议只需要__len____getitem__
两个方法。任何类(例如 Spam),只要使用标准的签名和语义实现了
这两个方法,就能用在任何预期序列的地方。Spam 是不是哪个类的子
类无关紧要,只要提供了所需的方法即可。示例 1-1 就是一例,这里再
次给出代码,如示例 12-3 所示。
示例 12-3 示例 1-1 的代码,为了方便参考,再次给出

import collections
Card = collections.namedtuple('Card', ['rank', 'suit'])
class FrenchDeck:ranks = [str(n) for n in range(2, 11)] + list('JQKA')suits = 'spades diamonds clubs hearts'.split()def __init__(self):self._cards = [Card(rank, suit) for suit in self.suitsfor rank in self.ranks]def __len__(self):return len(self._cards)def __getitem__(self, position):return self._cards[position]

示例 12-3 中的 FrenchDeck 类能充分利用 Python 的很多功能,因为它
实现了序列协议,即使代码中并没有声明这一点。任何有经验的 Python
程序员只要看一眼就知道它是序列,即便它是 object 的子类也无妨。
我们说它是序列,因为它的行为像序列,这才是重点。
协议是非正式的,没有强制力,因此如果知道类的具体使用场景,那么
通常只需要实现协议的一部分。例如,为了支持迭代,只需实现
__getitem__ 方法,没必要提供__len__方法。

Vector 类第 2 版:可切片的序列

FrenchDeck 类所示,如果能委托给对象中的序列属性(例如
self._components 数组),则支持序列协议特别简单。下面只有一行
代码的__len__方法和 __getitem__ 方法是很好的开始。

class Vector:typecode = 'd'def __init__(self, components):self._components = array(self.typecode, components)# ❶def __iter__(self):return iter(self._components) #❷def __repr__(self):components = reprlib.repr(self._components) #❸components = components[components.find('['):-1] #❹return f'Vector({components})'def __str__(self):return str(tuple(self))def __bytes__(self):return (bytes([ord(self.typecode)]) +bytes(self._components)) #❺def __eq__(self, other):return tuple(self) == tuple(other)def __abs__(self):return math.hypot(*self) #❻def __bool__(self):return bool(abs(self))@classmethoddef frombytes(cls, octets):typecode = chr(octets[0])memv = memoryview(octets[1:]).cast(typecode)return cls(memv) #❼def __len__(self):return len(self._components)def __getitem__(self, index):return self._components[index]


可以看到,连切片都支持了,不过尚不完美。如果 Vector 实例的切片
也是 Vector 实例,而不是数组,那就更好了。前面那个 FrenchDeck
类也有类似的问题:切片得到的是列表。对 Vector 来说,如果切片生
成普通的数组,那么将会失去大量功能。
想想内置序列类型:切片得到的都是各自类型的新实例,而不是其他类型。
为了把 Vector 实例的切片也变成 Vector 实例,不能简单地把切片操作委托给数组。要分析传给 __getitem__ 方法的参数,做适当的处理。

切片原理

示例 12-4 观察 __getitem__ 和切片的行为

❶ 在这个示例中,__getitem__ 直接返回传给它的值。
❷ 单个索引,没什么新奇的。
1:4 表示法变成了 slice(1, 4, None)
slice(1, 4, 2) 的意思是从1开始,到 4 结束,步幅为 2
❺ 神奇的事发生了:如果[]中有逗号,那么 __getitem__ 收到的就是元组。
❻ 元组中甚至可以有多个 slice 对象。
示例 12-5 查看slice类的属性

slice 是内置的类型(首次出现于 2.7.2 节)。
❷ 查看 slice,我们发现它有 startstopstep3 种数据属
性,还有 indices 方法。
在示例 12-5 中,调用 dir(slice) 得到的结果中有个 indices 属性,
这是一个方法,作用很大,但是鲜为人知。help(slice.indices)
出的信息如下。

给定长度为 len 的序列,计算 S 表示的扩展切片的起始(start
索引和结尾(stop)索引,以及步幅(stride)。超出边界的索引会
被截掉,就像常规切片一样。
换句话说,indices 方法开放了内置序列实现的棘手逻辑,可以优雅地
处理缺失索引和负数索引,以及长度超过目标序列的切片。这个方法
会“整顿”元组,把 startstopstride 都变成非负数,而且都落在
指定长度序列的边界内。
下面举几个例子。假设有一个长度为5的序列,例如 'ABCDE'

'ABCDE'[:10:2] 等同于 'ABCDE'[0:5:2]
'ABCDE'[-3:] 等同于 'ABCDE'[2:5:1]
Vector 类中无须使用 slice.indices() 方法,因为收到切片参数
时,我们委托 _components 数组处理。因此,如果没有底层序列类型
作为依靠,那么使用这个方法能节省大量时间。

能处理切片的 __getitem__ 方法

示例 12-6 列出了让 Vector 表现为序列所需的两个方法:__len____getitem__(后者现在能正确处理切片了)。
示例 12-6 vector_v2.py 的部分代码:为 vector_v1.py 中的 Vector
类(参见示例 12-2)添加 __len__ 方法和 __getitem__ 方法

def __len__(self):return len(self._components)
def __getitem__(self, key):if isinstance(key, slice): #❶cls = type(self) #❷return cls(self._components[key]) #❸index = operator.index(key) #❹return self._components[index] #❺

❶ 如果 key 参数的值是一个 slice 对象……
❷ ……就获取实例的类(Vector),然后……
❸ ……调用类的构造函数,使用 _components 数组的切片构建一个新
Vector 实例。
❹ 如果从 key 中得到的是单个索引……
❺ ……就返回 _components 中相应的元素。

🚩大量使用 isinstance 可能表明面向对象设计得不好,不过
__getitem__ 方法中使用它处理切片是合理的。
示例 12-7 测试示例 12-6 中改进的 Vector.__getitem__ 方法

❶ 单个整数索引只获取一个分量,值为浮点数。
❷ 切片索引创建一个新 Vector 实例。
❸ 长度为 1 的切片也创建一个 Vector 实例。
Vector 不支持多维索引,因此索引元组或多个切片会抛出错误。

Vector 类第 3 版:动态存取属性

Vector2d 变成 Vector 之后,就无法通过名称访问向量的分量(例如
v.x v.y)了。现在,我们处理的向量可能有大量分量。不过,如果
能通过单个字母访问前几个分量的话会比较方便。例如,用 xyz
代替 v[0]v[1]v[2]
我们想额外提供以下句法,用于读取向量的前 4 个分量。

class Vector:typecode = 'd'def __init__(self, components):self._components = array(self.typecode, components)# ❶def __iter__(self):return iter(self._components) #❷def __repr__(self):components = reprlib.repr(self._components) #❸components = components[components.find('['):-1] #❹return f'Vector({components})'def __str__(self):return str(tuple(self))def __bytes__(self):return (bytes([ord(self.typecode)]) +bytes(self._components)) #❺def __eq__(self, other):return tuple(self) == tuple(other)def __abs__(self):return math.hypot(*self) #❻def __bool__(self):return bool(abs(self))@classmethoddef frombytes(cls, octets):typecode = chr(octets[0])memv = memoryview(octets[1:]).cast(typecode)return cls(memv) #❼def __len__(self):return len(self._components)def __getitem__(self, index):return self._components[index]__match_args__ = ('x', 'y', 'z', 't') #new❶def __getattr__(self, name):cls = type(self) #new❷try:pos = cls.__match_args__.index(name) #new❸except ValueError: #new❹pos = -1if 0 <= pos < len(self._components): #new❺return self._components[pos]msg = f'{cls.__name__!r} object has no attribute {name!r}' #new❻raise AttributeError(msg)


new❶ 设定 __match_args__,让 __getattr__ 实现的动态属性支持位置模式匹配。
new❷ 获取 Vector 类,供后面使用。
new❸ 尝试获取 name__match_args__ 中的位置。
new❹ 如果未找到 name,那么 .index(name) 就会抛出 ValueError。此
时,把 pos 设为 -1。(我也想在这里使用 str.find 之类的方法,可
tuple 没有实现这样的方法。)
new❺ 如果 pos 落在分量长度范围内,就返回对应的分量。
new❻ 如果执行到这里,就抛出 AttributeError,输出一个标准消息。
__getattr__ 方法的实现不难,但是这样实现还不够。看看示例 12-9
中古怪的交互行为。
示例 12-9 不恰当的行为:为 v.x 赋值没有抛出错误,但是前后矛盾

❶ 使用 v.x 获取第一个元素(v[0])。
❷ 为 v.x 赋新值。这个操作应该抛出异常。
❸ 读取 v.x,得到的是新值 10。
❹ 可是,向量的分量没变。
示例 12-10 vector_v3.py 的部分代码:在 Vector 类中实现__setattr__ 方法

class Vector:typecode = 'd'def __init__(self, components):self._components = array(self.typecode, components)# ❶def __iter__(self):return iter(self._components) #❷def __repr__(self):components = reprlib.repr(self._components) #❸components = components[components.find('['):-1] #❹return f'Vector({components})'def __str__(self):return str(tuple(self))def __bytes__(self):return (bytes([ord(self.typecode)]) +bytes(self._components)) #❺def __eq__(self, other):return tuple(self) == tuple(other)def __abs__(self):return math.hypot(*self) #❻def __bool__(self):return bool(abs(self))@classmethoddef frombytes(cls, octets):typecode = chr(octets[0])memv = memoryview(octets[1:]).cast(typecode)return cls(memv) #❼def __len__(self):return len(self._components)def __getitem__(self, index):return self._components[index]__match_args__ = ('x', 'y', 'z', 't') #new❶def __getattr__(self, name):cls = type(self) #new❷try:pos = cls.__match_args__.index(name) #new❸except ValueError: #new❹pos = -1if 0 <= pos < len(self._components): #new❺return self._components[pos]msg = f'{cls.__name__!r} object has no attribute {name!r}' #new❻raise AttributeError(msg)def __setattr__(self, name, value):cls = type(self)if len(name) == 1: #❶if name in cls.__match_args__: #❷error = 'readonly attribute {attr_name!r}'elif name.islower(): #❸error = "can't set attributes 'a' to 'z' in {cls_name!r}"else:error = '' #❹if error: #❺msg = error.format(cls_name=cls.__name__, attr_name=name)raise AttributeError(msg)super().__setattr__(name, value) #❻

❶ 特别处理名称是单个字符的属性。
❷ 如果 name __match_args__ 中,就设置特殊的错误消息。
❸ 如果 name 是小写字母,就设置一个针对所有小写字母的错误消息。
❹ 否则,把错误消息设为空字符串。
❺ 如果错误消息不为空,就抛出 AttributeError
❻ 默认情况:在超类上调用__setattr__方法,提供标准行为。

🚩 super() 函数用于动态访问超类的方法,对 Python 这种支持
多重继承的动态语言来说,必须这么做。程序员经常使用这个函数
把子类方法的某些任务委托给超类中适当的方法,如示例 12-10 所示。

Vector 类第 4 版:哈希和快速等值测试

我们要再次实现 __hash__ 方法。加上现有的 __eq__ 方法,这会把
Vector 实例变成可哈希的对象。

归约函数(reducesumany all)把序列或有限的可
迭代对象聚合成一个结果。
我们已经知道,sum() 可以代替 functools.reduce(),下面说说它的
原理。reduce() 的关键思想是,把一系列值归约成单个值。reduce()
函数的第一个参数是一个接受两个参数的函数,第二个参数是一个可迭
代对象。假如有一个接受两个参数的函数 fn 和一个列表 lst。调用
reduce(fn, lst) 时,fn 首先会被应用到第一对元素上,即
fn(lst[0], lst[1]),生成第一个结果 r1。然后,fn 会被应用到 r1
和下一个元素上,即 fn(r1, lst[2]),生成第二个结果 r2。接着,
调用 fn(r2, lst[3]),生成 r3……直到最后一个元素,返回最后得
到的结果 rN

使用 reduce 函数可以计算 5!5 的阶乘)。

示例 12-11 计算整数 0~5 的累计异或的 3 种方式

❶ 使用 for 循环和累加器变量计算聚合异或。
❷ 使用 functools.reduce函数,传入匿名函数。
❸ 使用 functools.reduce 函数,把 lambda 表达式换成operator.xor
示例 12-12 vector_v4.py 的部分代码:在 vector_v3.py 中的
Vector 类的基础上导入两个模块,并添加 __hash__ 方法

from array import array
import reprlib
import math
import functools #❶
import operator #❷class Vector:typecode = 'd'def __init__(self, components):self._components = array(self.typecode, components)def __iter__(self):return iter(self._components) def __repr__(self):components = reprlib.repr(self._components) components = components[components.find('['):-1] return f'Vector({components})'def __str__(self):return str(tuple(self))def __bytes__(self):return (bytes([ord(self.typecode)]) +bytes(self._components)) def __eq__(self, other): # ❸return tuple(self) == tuple(other)def __hash__(self):hashes = (hash(x) for x in self._components)# ❹return functools.reduce(operator.xor, hashes, 0) #❺def __abs__(self):return math.hypot(*self) def __bool__(self):return bool(abs(self))@classmethoddef frombytes(cls, octets):typecode = chr(octets[0])memv = memoryview(octets[1:]).cast(typecode)return cls(memv) def __len__(self):return len(self._components)def __getitem__(self, index):return self._components[index]__match_args__ = ('x', 'y', 'z', 't') def __getattr__(self, name):cls = type(self) try:pos = cls.__match_args__.index(name)except ValueError: pos = -1if 0 <= pos < len(self._components): return self._components[pos]msg = f'{cls.__name__!r} object has no attribute {name!r}' raise AttributeError(msg)def __setattr__(self, name, value):cls = type(self)if len(name) == 1: if name in cls.__match_args__: error = 'readonly attribute {attr_name!r}'elif name.islower(): error = "can't set attributes 'a' to 'z' in {cls_name!r}"else:error = '' if error:msg = error.format(cls_name=cls.__name__, attr_name=name)raise AttributeError(msg)super().__setattr__(name, value) 

❶ 为了使用 reduce 函数,导入 functools 模块。
❷ 为了使用xor函数,导入 operator 模块。
__eq__方法没有变化。这里把它列出来是为了将其和 __hash__
法放在一起,因为它们要结合在一起使用。
❹ 创建一个生成器表达式,惰性计算各个分量的哈希值。
❺ 把 hashes 提供给 reduce 函数,使用 xor 函数计算聚合的哈希值。
第三个参数(0)是初始值(参见下面的“警告栏”)。

示例 12-12 实现的 __hash__ 方法是一种完美的映射归约(map-reduce)计算

图 12-2:映射归约:把函数应用到各个元素上,生成一个新序列(映射),然后计算聚合值(归约)
示例 12-15 内置函数 zip 的使用示例

zip 函数返回一个生成器,按需生成元组。
❷ 为了输出,构建一个列表。通常,我们会迭代生成器。
❸ 当一个可迭代对象耗尽后,zip 不发出警告就停止。
itertools.zip_longest 函数的行为有所不同,它使用可选的
fillvalue(默认值为 None)来填充缺失的值,因此可以继续生
成元组,直到最后一个可迭代对象耗尽。
zip 函数还可以转置以嵌套的可迭代对象表示的矩阵。

为了避免在 for 循环中直接处理索引变量,还经常使用内置生成器
函数 enumerate。如果不熟悉这个函数,那么一定要阅读“Built-in functions”文档。

Vector 类第 5 版:格式化

Vector 类的 __format__ 方法类似于 Vector2d 类的方法,但是不使
用极坐标,而使用球面坐标(也叫“超球面”坐标),因为 Vector 类支
n 个维度,而超过四维后,球体变成了“超球体”。 因此,我们将把
自定义的格式后缀由 'p' 改成 'h'
例如,对四维空间(len(v) == 4)中的 Vector 对象来说,'h' 代码
得到的结果如下:<r, Φ₁, Φ₂, Φ₃>,其中 r 是模(abs(v)),余下
3 个数是角坐标 Φ₁、Φ₂ 和 Φ₃
示例 12-16 vector_v5.py:Vector 类最终版的 doctest 和全部代
码,带标号那几行是为了支持 __format__ 方法而添加的代码

示例 12-16 vector_v5.py:Vector 类最终版的 doctest 和全部代
码,带标号那几行是为了支持 __format__ 方法而添加的代码

一个多维Vector类,第5版
Vector实例使用数值可迭代对象构建::

测试二维向量(结果与vector2d_v1.py一样)::

测试类方法.frombytes()::

测试三维向量::

测试多维向量::

测试.__bytes__.frombytes()方法::

测试序列行为::

测试切片::

测试动态属性访问::

动态属性查找失败情况::

测试哈希::

大多数非整数的哈希码在32位和64位CPython中不一样::

测试使用format()格式化二维笛卡儿坐标::

测试使用format()格式化三维和七维笛卡儿坐标::

测试使用format()格式化二维、三维和四维球面坐标::

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hjln.cn/news/44557.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈,一经查实,立即删除!

相关文章

MybatisPlus - [05] 逻辑删除

题记部分 一、物理删除&逻辑删除 物理删除:delete from table_name where xxx = ?; 逻辑删除:update table_name set deleted = 0 where xxx = ?;二、测试 (1)增加逻辑删除字段deleted(默认1,1:存在,0:删除) alter table user add column deleted int(1) default…

yum失败原因汇总

@目录概要情形一报错解答情形二报错小结 概要 怎么说呢,这个经历不好截图追溯了,只能通过回忆记录了。之前看了一个教程说把docker的所有镜像源换下,执行了一大串以sed开头的命令语句,接着就不能执行yum的相关操作了,yum update也不行,实在没办法了只能找售后工程师帮我看…

PDF怎么转成长图?4个好用方法了解一下

PDF文件是一种常见的文档格式,它可以在不同的设备和操作系统上保持格式的一致性。有时候我们需要将PDF文件转换成长图,以便于在社交媒体上分享或者在网站上展示。为了解决这一问题,我们可以尝试通过在线工具或者下载应用来帮助我们实现这一操作。下面将介绍一些常用的工具,…

我才不要和你做朋友呢百度云/迅雷BT下载[BD/HD-MKV1.77G/8.52G][高清版]

电影《我才不要和你做朋友呢》是一部极具深度和情感的作品,通过展现人际关系的复杂性和友谊的真实性,为观众带来了深刻的思考。    影片以两位主角小明和小红的故事展开。小明是一个内向而害羞的男孩,小红则是一个外向而热情的女孩。他们在同一个学校里上学,但却几乎没…

liunx下prometheus页面设置用户名密码登陆

前言 前两天公司漏洞通报prometheus未设置鉴权,漏洞详情如下图所示。 安装依赖yum -y install epel-release python3 python3-bcrypt通过python脚本生产密码[root@localhost home]# cat python_passwd.py import getpass import bcryptpassword = getpass.getpass("pas…

不是所有的硬盘柜都叫“安全专家”,但它做到了!

随着高考的钟声被敲响,夏日的气息也悄然弥漫开来,空气里渐渐融入了那份独特的燥热与活力。 在炎炎夏日,最令人难以忍受的莫过于突如其来的停电。瞬间的黑暗不仅无法驱散难耐的酷热,还在电力不稳的情况下给正在运行的电子设备带来了潜在的损害。 而我们购买NAS或者硬盘柜的初…

crontab设置计划任务

crontab设置计划任务 # 设置定时任务 crontab -e # 或 vim /etc/crontab# 如每天晚上11点到早上7点之间,每小时执行一次/root/backup.sh脚本,并将输出内容导出到/var/backup_log.txt * 23-7/1 * * * root /root/backup.sh >> /var/backup_log.txt星号(*):代表所有可…

删除金蝶数据库后恢复的方法

前有程序员删除数据库跑路,今有财务人员删除金蝶数据库离职!这两天呢,百度副总裁的言论啊一下子上了热搜,我这边呢也有一个差不多类似的一个情况,就是某公司财务人员,因为个人的一点小情绪提离职,没想到老板呢秒批了。一般像财务人员这种重要岗位,可能都会三思一下,结…