《流畅的Python第二版》读书笔记——Python数据模型
引言
这是《流畅的Python第二版》抢先版的读书笔记。Python版本暂时用的是python-3.8
。为了使开发更简单、快捷,本文使用了JupyterLab。
Python解释器调用一些特殊方法来进行基本的对象操作,这种特殊方法通常有特殊的写法。比如__getitem__
,如果你实现了该方法,就可以通过obj[key]
来触发obj.__getitem__(key)
方法。
这些特殊方法名能让你自己的对象实现和支持以下的语言构架,并与之交互:
- 集合类
- 属性访问
- 迭代
- 运算符重载
- 函数和方法的调用
- 对象的创建和销毁
- 使用await的异步编程
- 字符串表示形式和格式化
- 管理上下文(即 with 块)
有些人也称这些特殊方法为魔术方法(magic method)。
A Pythonic Card Deck
本节这个例子虽然简单,但是它通过实现__getitem__
和__len__
方法来展示特殊方法的强大。
frenchdeck.py
:
import collections# 利用namedtuple构造一个简单的表示纸牌的类
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]
我们可以像下面这样很方便的构造Card
对象:
from frenchdeck import Card,FrenchDeckbeer_card = Card('7', 'diamonds')
beer_card
但本例的重点是FrenchDeck
类(一叠纸牌),它虽然很小,但很强大。像标准的Python集合一样,可以调用len()
函数来获取对象中Card
实例的数量:
deck = FrenchDeck()
len(deck)
还可以从一叠纸牌中抽取第一张和最后一张,很简单deck[0]
、deck[-1]
即可,这是__getitem__
方法提供的:
Python提供了一个函数从序列中随机获取元素:random.choice
,我们可以直接用在deck
实例上:
from random import choice
choice(deck)
现在我们已经看到了实现特殊方法来利用Python数据模型的两个好处:
- 作为你的类的用户,无需记忆标准操作的各种名称(比如如何获取元素个数,通过
.size()
还是.length()
)。 - 更加方便地利用Python标准库,避免重复造轮子。就像
random.choice
函数一样。
因为__getitem__
方法把[]
操作委托(delegates)给了self._cards
列表,我们的deck
自动支持切片。下面展示了如何查看牌堆中前三张牌,然后通过从第13张牌开始(索引是12),每隔13张牌抽一张牌,来选取牌A:
deck[:3]
deck[12::13]
而且,仅仅是实现了__getitem__
特殊方法,我们的deck
还可以迭代:
for card in deck:print(card)
(上图只截取部分)
同时也可以反向迭代:
for card in reversed(deck):print(card)
迭代通常是隐式的,如果一个集合没有实现__contains__
方法,那么in
运算符就会执行一个顺序扫描。
于是,in
运算符可以用在我们的FrenchDeck
类上,因为它是可迭代的:
Card('Q', 'hearts') in deck
Card('7','beasts') in deck
还可以实现排序,比如用点数来判断纸牌大小,2最小、A最大;同时黑桃(spades)最大、红桃(hearts)次之、方块(diamonds)再次、梅花(clubs)最小。下面就是按照这个规则来排序的函数,约定梅花2的大小是0,黑桃A是51:
suit_values = dict(spades=3, hearts=2, diamonds=1,clubs=0)
def spades_high(card):rank_value = FrenchDeck.ranks.index(card.rank)# 点数乘以4 + 花色值return rank_value * len(suit_values) + suit_values[card.suit]
有了这个函数,就你可以对牌堆进行升序排序了:
for card in sorted(deck, key=spades_high):print(card)
通过实现__len__
和__getitem__
这两个特殊方法,FrenchDeck
就跟一个Python自有的序列数据类型一样,可以体现出Python的核心语言特性(例如迭代和切片)。同时这个类还可以用于标准库中random.choice
、reversed
和sorted
这些函数。另外,对于组合的运用是的__len__
和__getitem__
的具体实现可以委托给self._cards
这个list
对象。
如何使用特殊方法
首先要知道,特殊方法是被Python解释器而不是你调用的,你不能写my_object.__len__()
,而是写len(my_object)
:Python会调用你实现的__len__
方法。
然后如果是Python内置类型,比如列表、字符串、字节序列(bytearray)等,那么解释器会走捷径,__len__
实际上会直接返回PyVarObject
里的ob_size
属性,直接读取这个值比调用一个方法要快很多。
很多时候,特殊方法调用是隐式的。比如语句for i in x
:实际上会导致调用iter(x)
,而背后解释器会调用x.__iter__()
,如果有的这个方法的话。否则像FrenchDeck
例子一样,调用x.__getitem__()
。
通常你无需直接调用特殊方法,除非有大量的元编程存在。唯一的例外可能是__init__
方法。
通过内置函数来使用特殊方法是最好的选择。
不要想当然的随意添加特殊方法,比如__foo__
之类的,因为虽然这个名字现在没有被Python内部使用,以后就不一定了。
模拟数值类型
利用特殊方法,可以让自定义对象通过加号+
等运算符进行运算。
我们实现一个二维向量类vector
,就是我们数学中的向量。
上图是一个向量加法的例子:Vector(2,4)+Vector(2,1) = Vector(4,5)
我们实现的向量类应该能做这样的加法,通过+
运算符:
>>> v1 = Vector(2,4)
>>> v2 = Vector(2,1)
>>> v1 + v2
Vector(4,5)
其中+
运算符得到的结果也是一个向量,并且打印出来的结果很友好。
我们的向量也应该支持abs
函数,返回的是向量的模·:
>>> v = Vector(3,4)
>>> abs(v) # sqrt(3**2+4**2)
5.0
还可以利用*
运算符实现向量的标量乘法(向量与数的乘法):
>>> v * 3
Vector(9,12)
>>>abs(v * 3)
15.0
下面就来实现这样一个Vector
类,上面提到的操作在代码中是用__repr__
、__abs__
、__add__
和__mul__
实现的:
"""
vector2d.py: 一个展示一些特殊方法的简单类Addition::>>> v1 = Vector(2, 4)>>> v2 = Vector(2, 1)>>> v1 + v2Vector(4, 5)Absolute value::>>> v = Vector(3, 4)>>> abs(v)5.0Scalar multiplication::>>> v * 3Vector(9, 12)>>> abs(v * 3)15.0"""import mathclass Vector:def __init__(self, x=0, y=0):self.x = xself.y = ydef __repr__(self):return f'Vector({self.x!r}, {self.y!r})'def __abs__(self):return math.hypot(self.x, self.y)def __bool__(self):return bool(abs(self))def __add__(self, other):x = self.x + other.xy = self.y + other.yreturn Vector(x, y)def __mul__(self, scalar):return Vector(self.x * scalar, self.y * scalar)
乍一看我们实现了6个特殊方法,包括__init__
初始化方法。下面来介绍其他几个方法。
字符串表示
__repr__
特殊方法被Python内置的repr
函数调用,来展示一个对象的字符串形式。类似于Java中的toString()
。
交互式控制台和调试程序(debugger)用repr
函数来获取字符串表示形式。传统的字符串格式中使用%
运算符,现在有一种新的字符串格式化语法!r
用在str.format
方法中(也可以在要格式的字符串前加个f
,就如上面代码所示的)。也是利用repr
把!r
替换为字符串。
一些例子:
"Harold's a clever {0!s}" # 调用 str() , s for str "Bring out the holy {name!r}" # 调用repr(),r for repr
注意在我们的__repr__
实现中,使用了!r
来获取对象各个属性的标准字符串表示。这是一个好习惯,它展示了Vector(1,2)
和Vector('1','2')
的不同。后者会在我们的定义中报错,因为构造函数只接受数值,而不是字符串。
其实作者在第一版中说使用
%r
是一个好习惯。
__repr__
所返回的字符串应该准确且无歧义,并尽可能表达出如何用代码创建出这个被打印的对象。因此这里使用了类似调用对象构造函数的表达形式。
__repr__
和__str__
的区别在于,后者在str()
函数被使用时调用,或者通过print
函数隐式的调用。
如果你只想实现这两个方法中的一个,那么__repr__
是更好的选择。因为当没有自定义的__str__
,__str__
方法默认会调用__repr__
。
算术运算符
上面的代码中实现了两个运算:+
和*
,用到了__add__
和__mul__`。注意到这两者情况中,每次运算都返回一个新的实例。这是中缀运算符的基本原则:创建新对象而不修改操作数(不可变类的思想)。
自定义的布尔值
默认情况下,自定义的类实例都会认为是true
,除非实现了__bool__
或__len__
。bool(x)
会调用x.__bool__()
,如果__bool__
没有实现,那么Python会试着调用x.__len__()
,如果该方法返回0
,则bool(x)
返回False
,否则返回True
。
我们基于这个概念来实现__bool__
:如果向量的模是0
,则返回False
;否则返回True
。
一种更高效的实现是:
def __bool__(self):return bool(self.x or self.y)
但是可读性不好。
集合API
下图展示了比较重要的集合。该图中所有的类都是ABCs——抽象基类(abstract base classes)。
该图只是给大家看一下最重要的集合类实现了哪些特殊方法。
最上面的几个ABCs有一个单独的特殊方法,Python3.6中新增的Collection
联合了这三个重要的接口(Iterable
、Sized
和Container
),这是每个集合类都应该实现的:
Iterable
支持迭代Sized
支持内建的len
函数Container
支持in
运算符
Python不需要类去真正继承这些ABCs,而是,比如任何实现了__len__
的类都满足Sized
接口。
三个非常重要的特殊集合是:
Sequence
,内置接口的形式化,如list
和str
Mapping
,通过dict
和collection.defaultdict
实现Set
,内置的set
和frozenset
的接口
其中只有Sequence
是Reversible
,可以逆序访问的。
Set
还实现了一个中缀运算&
,比如a & b
代表两个集合的交集,通过__and__
实现。
特殊方法一览
下表展示了一些与运算符(中缀运算或abs
等)无关的特殊方法。
类别 | 方法名 |
---|---|
字符串/字节序列表示 |
__repr__ ,__format__ ,__bytes__ ,__fspath__
|
转换成数值 |
__abs__ ,__bool__ ,__complex__ ,__float__ ,__hash__ ,__index__
|
集合模拟 |
__len__ ,__getitem__ ,__setitem__ ,__delitem__ ,__contains__
|
迭代 |
__iter__ ,__aiter__ ,__next__ ,__anext__ ,__reversed__
|
可调用或协同执行 |
__call__ ,__await__
|
上下文管理 |
__enter__ ,__await__
|
实例创建和销毁 |
__new__ ,__init__ ,__del__
|
属性管理 |
__getattr__ ,__getattribute__ ,__setattr__ ,__delattr__ ,__dir__
|
属性描述符 |
__get__ ,__set__ ,__delete__ ,__set_name__
|
类服务 |
__prepare__ ,__init_subclass__ ,__instancecheck__ ,__subclasscheck__
|
跟运算符相关(中缀和数值运算符)的特殊方法如下表所示:
类别 | 方法名和相关操作 |
---|---|
一元数值运算 |
__neg__ - ,__pos__ + ,__abs__ abs()
|
众多比较运算符 |
__lt__ < ,__le__ <= ,__eq__ == ,__ne__ != ,__gt__ > ,__ge__ >=
|
算术运算符 |
__add__ + ,__sub__ - ,__mul__ * ,__truediv__ / ,__floordiv__ // ,__mod__ % ,__divmod__ divmod() ,__pow__ **或pow() ,__round__ round() ,__matmul__ @
|
反向算术运算符 |
__radd__ ,__rsub__ ,__rmul__ , __rtruediv__ , __rfloordiv__ , __rmod__ , __rdivmod__ , __rpow__ , __rmatmul__
|
增量赋值算术运算符 |
__iadd__ , __isub__ , __imul__ , __itruediv__ , __ifloordiv__ , __imod__ , __ipow__ , __imatmul__
|
位运算符 |
__invert__ ~ ,__lshift__ << ,__rshift__ >> , __and__ & , __or__ | ,__xor__ ^
|
反向位运算符 |
__rlshift__ ,__rrshift__ , __rand__ , __rxor__ , __ror__
|
增量赋值位运算符 |
__ilshift__ , __irshift__ , __iand__ , __ixor__ , __ior__
|
当交换两个操作数的位置时,就会调用反向运算符(
b * a
而不是a * b
)。增量运算符是一种把中缀运算符变成赋值运算的捷径(a = a * b
变成了a *= b
)。
为什么len不是普通方法
如果x
是一个内置类型的实例,那么len(x)
的速度会非常快。背后的原理是CPython会直接从一个C结构体里读取对象的长度,完全不会调用任何方法。
总结
通过实现特殊方法,能使你的对象像内建类型一样。
通过实现__reper
和__str__
,Python对象能通过字符串的形式表示自己,前者用在调试和打印上,后者用于给使用对象的用户。
《流畅的Python第二版》读书笔记——Python数据模型相关推荐
- Python核心教程(第二版)读书笔记(三)
第三章Python基础 2010-04-09 换行 一行过长的语句可以使用反斜杠'\'分解成几行.有两种例外情况一个语句不使用反斜线也可以跨行. 1.在使用闭合操作符时,单一语句可以跨多行.例如:在 ...
- Python 百天读书笔记 | Python语言进阶 16-20day
Python语言进阶 重要知识点 生成式(推导式)的用法 prices = {'AAPL': 191.88,'GOOG': 1186.96,'IBM': 149.24,'ORCL': 48.44,'A ...
- 【我的JS第三本】JavaScript_DOM编程艺术第二版读书笔记
经过前一段时间HTML&CSS的学习,感觉视频加读书是一个比较不错的学习方法,两者相辅相成,互相补充,所以也准备看看关于JavaScript的书. 2015年12月14日,之前使用韩顺平老师的 ...
- 深入理解JVM(第二版读书笔记)
一 开始前 HotSpot:http://xiaomogui.iteye.com/blog/857821 http://blog.csdn.net/u011521890/article/detail ...
- 《细说PHP》第二版--读书笔记
第五章 PHP的基本语法 5.2.4 在程序中使用空白的处理 5.3 变量 5.3.1 变量的声明 在php中变量的声明必须是使用一个$符号,后面跟变量名来表示 unset()函数释放指定变量 iss ...
- 刘鹏老师和王超老师的计算广告第二版读书笔记
广告的定义与目的 广告的基本概念 广告的分类 在线广告的表现形式 横幅广告 文字链广告 富媒体广告 视频广告 交互式广告 社交广告 移动广告 邮件营销广告 广告的基本概念 需求方:可以是广告主.代表广 ...
- 《计算广告》第二版 读书笔记
在线广告创意类型: 横幅广告 文字链广告 富媒体广告 视频广告 社交广告 移动广告 邮件定向营销广告. 广告发展历程: 合约广告->定向广告->竞价广告 (上下文广告) 术语解释: ADN ...
- sql注入攻击与防御第二版读书笔记二——SQL盲注利用
寻找并确认SQL盲注 强制产生通用错误 注入带副作用的查询 如 mssql waitfor delay '0:0:5' mysql sleep() 拆分与平衡 5 -> 7-2 常见SQL盲注场 ...
- Effective Java 英文 第二版 读书笔记 Item 14:In public classes,use accessor methods,not public fields...
本章主要分析 公开属性与私有属性提供公开get.set方法两种方式对比 // Degenerate classes like this should not be public! class Poin ...
- 《流畅的Python第二版》读书笔记——函数作为一等对象
引言 这是<流畅的Python第二版>抢先版的读书笔记.Python版本暂时用的是python3.10.为了使开发更简单.快捷,本文使用了JupyterLab. 函数是Python的一等( ...
最新文章
- [转]Ext Grid控件的配置与方法
- 2021-11-14Collection
- spring中事务配置的3种方式
- VMware 7.1.4安装Mac.OS.X.Lion.操作系统 key:安装 系统
- 基础算法 —— 调度问题 —— 多机并行调度问题
- MQTT 控制报文 - PUBLISH发布消息,PUBACK,PUBREC,PUBREL,PUBCOMP - 第6章
- bootstrap table背景_bootstrap table给行怎么加背景色
- 安防布线的一些常见问题。
- API安全前景与趋势
- 宾夕法尼亚州立大学:探索量子AI如何加速治愈癌症
- asp.net配置web.config发电子邮件详解
- 2022-2028年中国高通量药物筛选与创新药物行业市场调查研究及发展前景展望报告
- gird布局之容器属性justify-items与align-items
- java unrar.jar下载_unrar.jar解压缩rar文件
- 关于华硕主板的图像输出设置
- element-ui 级联选择器el-cascader踩坑
- 数字电路实验怎么接线视频讲解_【高中物理】电学实验满分知识点总结及例题精讲...
- 那你讲一下IntentService
- SAP官网学习教程(2)创建HANA数据库
- 谷歌的优化得分有什么好处?
热门文章
- 使用Adobe Fireworks CS6压图
- 集成电路制造工艺及设备
- haimeiktv服务器系统,海媚 Haimei KTV8001 智能网络效果器
- ag-grid 设置行高
- 基于C++和EasyX 实现的《双人贪吃蛇》小游戏,你不找个小伙伴陪你一起玩吗?
- httpclient 下载文件
- 小区门禁卡可以复制到手机上吗_手机NFC可以复制小区用的门禁卡吗?步骤是什么?...
- GBK 汉字编码转换
- linux系统怎么启动u盘启动盘,如何制作 linux 系统 U盘启动盘
- python论文排版格式_Latex论文排版工具使用教程