流畅的Python
说明
我发现流畅的python更适合我现在看,因为它写的很详细。而effective python知识点不是很连贯,我先看完这本书,再去过一遍effective python吧!
由于typora使用两个下划线就变成了自动转义,将字体变为粗体,所以文中的粗体就代表特殊方法。
第1章 Python数据类型
这里主要研究类中的各种特殊方法(魔术方法)的用途
1.1 一摞Python风格的纸牌
这里研究 _ _ getitem _ _ 和 _ _ len _ _ 这两个特殊方 法,通过下面的例子我们能够看到特殊方法的强大之处。
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.suits for rank in self.ranks]
def __len__(self):
return len(self._cards)
def __getitem__(self, position):
return self._cards[position]
beer_card = Card('7', 'diamonds')
print(beer_card) #Card(rank='7', suit='diamonds')
deck = FrenchDeck()
len(deck) #52
deck = FrenchDeck()
print(deck[0]) #Card(rank='2', suit='spades')
上面代码用到了nametuple方法(用以构建只有少数属性但是没有方法的类),列表推导方法,可以在effective python中找到。可以看到self._cards是一个列表,所以 _ _ len _ _ 方法返回直接使用len()方法获取的列表长度,然后 _ _ getitem _ _ 方法根据索引取出列表中的对应位置的元素。我们直接对deck实例使用len()方法会调用 _ _ len _ _ 特殊方法,所以会返回实例deck的长度,对deck实例使用索引[]会调用 _ _ getitem _ _ 特殊方法,所以能够返回指定位置元素。而且因为 _ _ getitem _ _ 这个方法deck对象成为了可以迭代的,而且能够对它进行切片。deck实质上像是一个生成器。
如果我们需要随机抽取一张纸牌,那么我们需要自己再造轮子去实现这个功能吗?答案是不用哦!没必要,Python 已经内置了从一个序 列中随机选出一个元素的函数 random.choice,我们直接把它用在这一摞纸牌实例上就好:
from random import choice
print(choice(deck)) #Card(rank='9', suit='hearts')
print(choice(deck)) #Card(rank='J', suit='hearts')
print(choice(deck)) #Card(rank='2', suit='spades')
现在已经可以体会到通过实现特殊方法来利用 Python 数据模型的两个好处。 1.作为你的类的用户,他们不必去记住标准操作的各式名称,怎么得到元素的总数? 是 .size() 还是 .length() 还是别的什么。 2.可以更加方便地利用Python的标准库,比如random.choice函数,从而不用重新发明轮子。
虽然 FrenchDeck 隐式地继承了 object 类, 但功能却不是继承而来的。我们通过数据模型和 一些合成来实现这些功能。通过实现 _ _ len _ _ 和 _ _ getitem _ _ 这两个特殊方法,FrenchDeck 就跟一个 Python 自有的序列数据类型一样,可以体现出 Python 的核心语言特性(例如迭 代和切片)。同时这个类还可以用于标准库中诸如 random.choice、reversed 和 sorted 这 些函数。
1.2 如何使用特殊方法
首先明确一点,特殊方法的存在是为了被 Python 解释器调用的,你自己并不需要调用它 们。也就是说没有 my_object. _ _len _ _() 这种写法,而应该使用 len(my_object)。在执行 len(my_object) 的时候,如果 my_object 是一个自定义类的对象,那么 Python 会自己去调 用其中由你实现的 _ _ len _ _ 方法。
import math
class Vector:
def __init__(self, x=0, y=0):
self.x = x
self.y = y
def __repr__(self):
return 'Vector(%r, %r)' % (self.x, self.y)
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.x
y = self.y + other.y
return Vector(x, y)
def __mul__(self, scalar):
return Vector(self.x * scalar, self.y * scalar)
v1 = Vector(2, 4)
v2 = Vector(2, 1)
print(v1 + v2) #Vector(4, 5)
v = Vector(3, 4)
print(abs(v)) #5
print(v*3) #Vector(9, 12)
Python 有一个内置的函数叫 repr,它能把一个对象用字符串的形式表达出来以便辨认,这 就是“字符串表示形式”。repr 就是通过 repr 这个特殊方法来得到一个对象的字符串 表示形式的。如果没有实现 repr,当我们在控制台里打印一个向量的实例时,得到的 字符串可能会是 <Vector object at 0x10e100070>。repr 和 str 的区别在于,后者是在 str() 函数被使用,或是在用 print 函数打印 一个对象的时候才被调用的,并且它返回的字符串对终端用户更友好。如果你只想实现这两个特殊方法中的一个,repr 是更好的选择,因为如果一个对象没 有 str 函数,而 Python 又需要调用它的时候,解释器会用 repr 作为替代。
通过 add 和 mul,示例 1-2 为向量类带来了 + 和 * 这两个算术运算符。值得注意的 是,这两个方法的返回值都是新创建的向量对象,被操作的两个向量(self 或 other)还 是原封不动,代码里只是读取了它们的值而已。中缀运算符的基本原则就是不改变操作对 象,而是产出一个新的值。第 13 章会谈到更多这方面的问题。
我们对 bool 的实现很简单,如果一个向量的模是 0,那么就返回 False,其他情况则 返回 True。因为 bool 函数的返回类型应该是布尔型,所以我们通过 bool(abs(self)) 把模值变成了布尔值。
1.3 特殊方法一览
参见 https://docs.python.org/3/reference/datamodel.html,这里给出了83 个特殊方法的名字,其中 47 个用于实现算术运算、位运算和比较操作。
1.4 问什么len不是普通方法
,如果 x 是一个内置类型的实例,那么 len(x) 的速度会非常快。背后的 原因是 CPython 会直接从一个 C 结构体里读取对象的长度,完全不会调用任何方法。len 之所以不是一个普通方法,是为了让 Python 自带的数据结构可以走后门, abs 也是同理。但是多亏了它是特殊方法,我们也可以把 len 用于自定义数据类型。这种 处理方式在保持内置类型的效率和保证语言的一致性之间找到了一个平衡点,也印证了 “Python 之禅”中的另外一句话:“不能让特例特殊到开始破坏既定规则。” 就是说对于len来说,如果用于自定数据类型他就会调用__len__方法,如果用于内置数据类型就会直接从C结构体里读取对象的长度,而不调用任何方法。保证了一致性。
第2章 序列构成的数组
2.1 内置序列类型概览
序列能够按照是否能够容纳不同类型的数据分为容器序列和扁平序列:
容器序列:list,tuple,collections.deque 这些序列能存放不同类型的数据
扁平序列:str,bytes,bytearray、memoryview 和 array.array,这类序列只能容纳一种类型。
容器类型存放的是它们所包含的任意类型对象的引用(地址),而扁平序列存放的是值,扁平序列是一块连续的内存空间,但是它只能存放字符,字节,数值这种基础数据类型。
此外,还能够按照是否可变分为可变序列与不可变序列:
可变序列(MutableSequence):list、bytearray、array.array、collections.deque 和 memoryview。
不可变序列(Sequence):tuple、str 和 bytes。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-uvl2t5z0-1659619142945)(C:\Users\sz\AppData\Roaming\Typora\typora-user-images\image-20220804162410862.png)]
2.2 列表推导和生成器表达式
列表推导是构建列表(list)的快捷方式,生成器表达式则可以用来创建其他任何类型 的序列。
2.2.1 列表推导和可读性
列表推导的通常使用原则是:只用列表推导来创建新的对象,并且尽量保持简短。如 序列构成的数组 | 19 果列表推导的代码超过了两行,你可能就要考虑是不是得用 for 循环重写了。列表推导可以帮助我们把一个序列或是其他可迭代类型中的元素过滤或是加工,然后再新 建一个列表。
2.2.2 列表推导同filter和map的比较
这个再effective中写的比较详细,这里写的很简单,就不做记录了。
2.2.3 笛卡尔积
colors = ['black', 'white']
sizes = ['S', 'M', 'L']
tshirts = [(color, size) for color in colors for size in sizes]
print(tshirts) #[('black', 'S'), ('black', 'M'), ('black', 'L'), ('white', 'S'), ('white', 'M'), ('white', 'L')]
笛卡尔积就是列表推导中用了两个for循环的一种说法啦!因为这两个for循环实际上是嵌套的,第一个for循环是外层循环,第二个for循环时内层循环。
2.2.4 生成器表达式
虽然也可以用列表推导来初始化元组、数组或其他序列类型,但是生成器表达式是更好的 选择。这是因为生成器表达式背后遵守了迭代器协议,可以逐个地产出元素,而不是先建 立一个完整的列表,然后再把这个列表传递到某个构造函数里。前面那种方式显然能够节 省内存。
colors = ['black', 'white']
sizes = ['S', 'M', 'L']
tshirts = ((color, size) for color in colors for size in sizes)
print(tshirts) #<generator object <genexpr> at 0x00000229B0DD1BA0>
for i in tshirts:
print(i) #('black', 'S')
('black', 'M')
('black', 'L')
('white', 'S')
('white', 'M')
('white', 'L')
可以看到的是使用生成器表达式会生成一个生成器,然后对这个生成器进行迭代可以取到其中的元素。
2.3 元组不仅仅是不可变的列表
2.3.1 元组和记录
city, year, pop, chg, area = ('Tokyo', 2003, 32450, 0.66, 8014)
从上代码可以看出,元组中不同位置的元素代表了特定的信息,分别是城市,年份,人口,人口变化,面积。如果只把元组理解为不可变的列表,那其他信息——它所含有的元素的总数和它们的位 置——似乎就变得可有可无。但是如果把元组当作一些字段的集合,那么数量和位置信息 就变得非常重要了。
2.3.2 元组拆包
这部分主要讲了参数对应的拆包以及使用了*的参数不相等的拆包,这里再ep里详细讲过了,就不记录了。
2.3.3 嵌套元组拆包
metro_areas = [
('Tokyo', 'JP', 36.933, (35.689722, 139.691667)),
('Delhi NCR', 'IN', 21.935, (28.613889, 77.208889)),
('Mexico City', 'MX', 20.142, (19.433333, -99.133333)),
('New York-Newark', 'US', 20.104, (40.808611, -74.020386)),
('São Paulo', 'BR', 19.649, (-23.547778, -46.635833)),
]
def main():
print(f'{"":15} | {"latitude":>9} | {"longitude":>9}')
for record in metro_areas:
match record:
case [name, _, _, (lat, lon)] if lon <= 0:
print(f'{name:15} | {lat:9.4f} | {lon:9.4f}')
main()
# | latitude | longitude
Mexico City | 19.4333 | -99.1333
New York-Newark | 40.8086 | -74.0204
São Paulo | -23.5478 | -46.6358
元组内部嵌套元组,只要元素能够一一对应,就能够正常拆包。
2.3.4 具名元组
collections.namedtuple 是一个工厂函数,它可以用来构建一个带字段名的元组和一个有 名字的类——这个带名字的类对调试程序有很大帮助。
import collections
cls = collections.namedtuple("country","name pop are")
ch = cls("china",14,960)
print(ch) #country(name='china', pop=14, are=960)
print(ch.are) #960
print(ch[2]) #960
print(cls._fields) #('name', 'pop', 'are')
print(cls._make(("china",14,960))) #country(name='china', pop=14, are=960)
namedtuple需要两个属性,一个是类名,一个是类中的属性名。其中属性名可传入由字符串组成的可迭代对象,或是上面所示由空格分开的字符串。可以通过属性名获取属性值,可以通过索引获取属性值
2.3.5 作为不可变列表的元组
| s.add(s2) • • s + s2,拼接 |
|---|
| s.contains(e) • • s 是否包含 e |
| s.count(e) • • e 在 s 中出现的次数 |
| s.getitem§ • • s[p],获取位置 p 的元素 |
| s.index(e) • • 在 s 中找到元素 e 第一次出现的位置 |
| s.iter() • • 获取 s 的迭代器 |
| s.len() • • len(s),元素的数量 |
| s.mul(n) • • s * n,n 个 s 的重复拼接 |
| s.rmul(n) • • n * s,反向拼接 * |
| 上面列出了列表对象和元组对象拥有的共同方法,剩余的列表对象有的方法元组都没有,元组没有别的方法了。 |
2.4 切片
2.4.1 为什么切片和区间会忽略最后一个元素
当起止位置信息都可见时,我们可以快速计算出切片和区间的长度,用后一个数减去第 一个下标(stop - start)即可。这样做也让我们可以利用任意一个下标来把序列分割成不重叠的两部分,只要写成 my_ list[:x] 和 my_list[x:] 就可以了
2.4.2 对对象进行切片
可以对定义了getitem的对象进行切片. slice(a, b, c)。 在 10.4.1 节 中 会 讲 到, 对 seq[start:stop:step] 进 行 求 值 的 时 候,Python 会调用 seq. getitem(slice(start, stop, step))
invoice = """
0.....6.................................40........52...55........
1909 Pimoroni PiBrella $17.50 3 $52.50
1489 6mm Tactile Switch x20 $4.95 2 $9.90
1510 Panavise Jr. - PV-201 $28.00 1 $28.00
1601 PiTFT Mini Kit 320x240 $34.95 1 $34.95
"""
SKU = slice(0, 6)
DESCRIPTION = slice(6, 40)
UNIT_PRICE = slice(40, 52)
QUANTITY = slice(52, 55)
ITEM_TOTAL = slice(55, None)
line_items = invoice.split('\n')[2:]
for item in line_items:
print(item[UNIT_PRICE], item[DESCRIPTION])
# $17.50 imoroni PiBrella
# $4.95 mm Tactile Switch x20
# $28.00 anavise Jr. - PV-201
# $34.95 iTFT Mini Kit 320x240
2.4.3 多维切片和省略
[] 运算符里还可以使用以逗号分开的多个索引或者是切片,外部库 NumPy 里就用到了这 个特性,二维的 numpy.ndarray 就可以用 a[i, j] 这种形式来获取,抑或是用 a[m:n, k:l] 的方式来得到二维切片。
2.4.4 给切片赋值
try:
l[2:5] = 100
except TypeError as e:
print(repr(e)) #TypeError('can only assign an iterable')
l[2:5] = [100]
print(l) #[0, 1, 100, 22, 9]
赋值必须是一个可迭代对象,即使只要一个元素,也要写成列表的形式。
2.5 对序列使用+和*
通常 + 号两侧的序列由相同类型的数据所 构成,在拼接的过程中,两个被操作的序列都不会被修改,Python 会新建一个包含同样类 型数据的序列来作为拼接的结果。如果想要把一个序列复制几份然后再拼接起来,更快捷的做法是把这个序列乘以一个整 数。同样,这个操作会产生一个新序列。+ 和 * 都遵循这个规律,不修改原有的操作对象,而是构建一个全新的序列。
l = [1, 2, 3]
print(l * 5) #[1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3]
如果在 a * n 这个语句中,序列 a 里的元素是对其他可变对象的引用的话, 你就需要格外注意了,因为这个式子的结果可能会出乎意料。比如,你想用 my_list = [[]] * 3 来初始化一个由列表组成的列表,但是你得到的列表里 包含的 3 个元素其实是 3 个引用,而且这 3 个引用指向的都是同一个列表。 这可能不是你想要的效果。
board = [['_'] * 3 for i in range(3)]
board #[['_', '_', '_'], ['_', '_', '_'], ['_', '_', '_']]
board[1][2] = 'X'
board #[['_', '_', '_'], ['_', '_', 'X'], ['_', '_', '_']]
weird_board = [['_'] * 3] * 3
weird_board #[['_', '_', '_'], ['_', '_', '_'], ['_', '_', '_']]
weird_board[1][2] = 'O'
weird_board #[['_', '_', 'O'], ['_', '_', 'O'], ['_', '_', 'O']]
可以看到使用列表推导和 * 生成列表产生了不一样的结果。因为对于使用* 的列表,对可变对象实际内部是对象的引用,所以产生了三个相同的列表对象,所以修改其中一个值,剩余的都变了,因为实际上是一个值。
2.6 序列的增量赋值
+= 背后的特殊方法是 iadd(用于“就地加法”)。但是如果一个类没有实现这个方法的 话,Python 会退一步调用 add。考虑下面这个简单的表达式: >>> a += b 如 果 a 实现了 iadd 方 法, 就 会 调 用 这 个 方 法。 同 时 对 可 变 序 列( 例 如 list、 bytearray 和 array.array)来说,a 会就地改动,就像调用了 a.extend(b) 一样。但是如 果 a 没有实现 iadd 的话,a += b 这个表达式的效果就变得跟 a = a + b 一样了:首先 计算 a + b,得到一个新的对象,然后赋值给 a。也就是说,在这个表达式中,变量名会不 会被关联到新的对象,完全取决于这个类型有没有实现 iadd 这个方法。 总体来讲,可变序列一般都实现了 iadd 方法,因此 += 是就地加法。而不可变序列根 本就不支持这个操作,对这个方法的实现也就无从谈起。
2.7 list.sort方法和内置函数sorted
list.sort方法会对原列表进行排序,也就是说不会把原列表复制一份。这也是这个方法的返 回值是 None 的原因,提醒你本方法不会新建一个列表。 list.sort 相反的是内置函数 sorted,它会新建一个列表作为返回值。这个方法可以接 受任何形式的可迭代对象作为参数,甚至包括不可变序列或生成器(见第 14 章)。而不管 sorted 接受的是怎样的参数,它最后都会返回一个列表。不管是 list.sort 方法还是 sorted 函数,都有两个可选的关键字参数。
2.8 用bisect来管理已排序的序列
在第二版中好像把这一小节给删除了,感觉也确实用不到,就先不看了,如果有需要再回来看吧。
2.9 当列表不是首选时
2.9.1 数组
如果我们需要一个只包含数字的列表,那么 array.array 比 list 更高效。数组支持所有跟 可变序列有关的操作,包括 .pop、.insert 和 .extend。另外,数组还提供从文件读取和存 入文件的更快的方法,如 .frombytes 和 .tofile。 Python 数组跟 C 语言数组一样精简。创建数组需要一个类型码,这个类型码用来表示在 底层的 C 语言应该存放怎样的数据类型。比如 b 类型码代表的是有符号的字符(signed char),因此 array(‘b’) 创建出的数组就只能存放一个字节大小的整数,范围从 -128 到 127,这样在序列很大的时候,我们能节省很多空间。而且 Python 不会允许你在数组里存 放除指定类型之外的数据。
from array import array
from random import random, seed
seed(10) # Use seed to make the output consistent
floats = array('d', (random() for i in range(10 ** 7)))
print(floats[0:2]) #array('d', [0.5714025946899135, 0.4288890546751146])
从 Python 3.4 开始,数组(array)类型不再支持诸如 list.sort() 这种就地 排序方法。要给数组排序的话,得用 sorted 函数新建一个数组: a = array.array(a.typecode, sorted(a))
2.9.2 内存视图
这个没有看懂
2.9.3 NumPy和SciPy
这里就做了很简单的介绍,就没必要看了。
2.9.4 双向队列和其他形式的队列
利用 .append 和 .pop 方法,我们可以把列表当作栈或者队列来用(比如,把 .append 和 .pop(0) 合起来用,就能模拟队列的“先进先出”的特点)。但是删除列表的第一个元素 (抑或是在第一个元素之前添加一个元素)之类的操作是很耗时的,因为这些操作会牵扯 到移动列表里的所有元素。 collections.deque 类(双向队列)是一个线程安全、可以快速从两端添加或者删除元素的 数据类型。
示例 2-23 使用双向队列
>>> from collections import deque
>>> dq = deque(range(10), maxlen=10) ➊
>>> dq
deque([0, 1, 2, 3, 4, 5, 6, 7, 8, 9], maxlen=10)
>>> dq.rotate(3) ➋
>>> dq
deque([7, 8, 9, 0, 1, 2, 3, 4, 5, 6], maxlen=10)
>>> dq.rotate(-4)
>>> dq
deque([1, 2, 3, 4, 5, 6, 7, 8, 9, 0], maxlen=10)
>>> dq.appendleft(-1) ➌
>>> dq
deque([-1, 1, 2, 3, 4, 5, 6, 7, 8, 9], maxlen=10)
>>> dq.extend([11, 22, 33]) ➍
>>> dq
deque([3, 4, 5, 6, 7, 8, 9, 11, 22, 33], maxlen=10)
>>> dq.extendleft([10, 20, 30, 40]) ➎
>>> dq
deque([40, 30, 20, 10, 3, 4, 5, 6, 7, 8], maxlen=10)
杂谈
容器序列和扁平序列
有些对象里包含对其他对象的引用;这些对象称为容器。 因此,我特别使用了“容器序列”这个词,因为 Python 里有是容器但并非序列的类 型,比如 dict 和 set。容器序列可以嵌套着使用,因为容器里的引用可以针对包括自 身类型在内的任何类型。 与此相反,扁平序列因为只能包含原子数据类型,比如整数、浮点数或字符,所以不 能嵌套使用。 称其为“扁平序列”是因为我希望有个名词能够跟“容器序列”形成对比。这个词是 我自己发明的,专门用来指代 Python 中“不是容器序列”的序列,在其他地方你 可能找不到这样的用法。
第3章 字典和集合
dict 类型不但在各种程序里广泛使用,它也是 Python 语言的基石。模块的命名空间、 实例的属性和函数的关键字参数中都可以看到字典的身影。跟它有关的内置函数都在 builtins.dict 模块中。 正是因为字典至关重要,Python 对它的实现做了高度优化,而散列表则是字典类型性能出 众的根本原因。 集合(set)的实现其实也依赖于散列表,因此本章也会讲到它。
什么是可散列的数据类型?
如果一个对象是可散列的,那么在这个对象的生命周期中,它的散列值是不变 的,而且这个对象需要实现 hash() 方法。另外可散列对象还要有 eq() 方法,这样才能跟其他键做比较。如果两个可散列对象是相等的,那么它们的 散列值一定是一样的…原子不可变数据类型(str、bytes 和数值类型)都是可散列类型,frozenset 也是可散 列的,因为根据其定义,frozenset 里只能容纳可散列类型。元组的话,只有当一个元 组包含的所有元素都是可散列类型的情况下,它才是可散列的。一般来讲用户自定义的类型的对象都是可散列的,散列值就是它们的 id() 函数的返 回值,所以所有这些对象在比较的时候都是不相等的。
3.1 泛映射类型
字典有多种构造方式。
a = dict(one=1, two=2, three=3)
b = {'three': 3, 'two': 2, 'one': 1}
c = dict([('two', 2), ('one', 1), ('three', 3)])
d = dict(zip(['one', 'two', 'three'], [1, 2, 3]))
e = dict({'three': 3, 'one': 1, 'two': 2})
print(a == b == c == d == e) #True
3.2 字典推导
利用字典推导可以把一个装满元组的列表变成两 个不同的字典。
dial_codes = [ # <1>
(880, 'Bangladesh'),
(55, 'Brazil'),
(86, 'China'),
(91, 'India'),
(62, 'Indonesia'),
(81, 'Japan'),
(234, 'Nigeria'),
(92, 'Pakistan'),
(7, 'Russia'),
(1, 'United States'),
]
country_dial = {country: code for code, country in dial_codes}
print(country_dial) #{'Bangladesh': 880, 'Brazil': 55, 'China': 86, 'India': 91, 'Indonesia': 62, 'Japan': 81, 'Nigeria': 234, 'Pakistan': 92, 'Russia': 7, 'United States': 1}
3.3 常见的映射方法
参见ep 16条有详细的介绍。setdefault与get方法比较,好处是他对于字典只需查询一次,而get方法要查询两次。
3.4 映射的弹性键查询
有时候为了方便起见,就算某个键在映射里不存在,我们也希望在通过这个键读取值的 时候能得到一个默认值。有两个途径能帮我们达到这个目的,一个是通过 defaultdict 这 个类型而不是普通的 dict,另一个是给自己定义一个 dict 的子类,然后在子类中实现 missing 方法。但是get方法也可以呀!
3.4.1 defaultdict:处理找不到的键的一个选择
这里ef17条已经详细解读过了,就不说了
3.4.2 特殊方法__missing__
所有的映射类型在处理找不到的键的时候,都会牵扯到 missing 方法。这也是这个方法 称作“missing”的原因。虽然基类 dict 并没有定义这个方法,但是 dict 是知道有这么个 东西存在的。也就是说,如果有一个类继承了 dict,然后这个继承类提供了 missing 方 法,那么在 getitem 碰到找不到的键的时候,Python 就会自动调用它,而不是抛出一个 KeyError 异常。
# BEGIN STRKEYDICT0
class StrKeyDict0(dict): # <1>
def __missing__(self, key):
if isinstance(key, str): # <2>
raise KeyError(key)
return self[str(key)] # <3>
def get(self, key, default=None):
try:
return self[key] # <4>
except KeyError:
return default # <5>
def __contains__(self, key):
return key in self.keys() or str(key) in self.keys() # <6>
在我们通过键查找对应的值的时候会调用getitem方法,如果查找不到,你又定义了missing方法,那么getitem方法会抵用missing方法。注意我们对字典使用 d in k语句时会调用contains方法。
这里我总结的很不好,如果详细了解还是要去看书。
3.5 字典的变种
collections.OrderedDict 这个类型在添加键的时候会保持顺序,因此键的迭代次序总是一致的。OrderedDict 的 popitem 方法默认删除并返回的是字典里的最后一个元素,但是如果像 my_odict. popitem(last=False) 这样调用它,那么它删除并返回第一个被添加进去的元素。 这个在pytoch里被使用了哦!
3.6 子类化UserDict
就创造自定义映射类型来说,以 UserDict 为基类,总比以普通的 dict 为基类要来得方便。
没太看懂是怎么方便的。哈哈!?
3.7 不可变映射类型
标准库里所有的映射类型都是可变的,但有时候你会有这样的需求,比如不能让用户错误 地修改某个映射。
3.8 集合论
集合中的元素必须是可散列的,set 类型本身是不可散列的,但是 frozenset 可以。因此 可以创建一个包含不同 frozenset 的 set。除了保证唯一性,集合还实现了很多基础的中缀运算符 。给定两个集合 a 和 b,a | b 返回 的是它们的合集,a & b 得到的是交集,而 a - b 得到的是差集。合理地利用这些操作,不仅 能够让代码的行数变少,还能减少 Python 程序的运行时间。这样做同时也是为了让代码更易 读,从而更容易判断程序的正确性,因为利用这些运算符可以省去不必要的循环和逻辑操作。
3.8.1 集合字面量
不要忘了,如果要创建一个空集,你必须用不带任何参数的构造方法 set()。 如果只是写成 {} 的形式,跟以前一样,你创建的其实是个空字典。{1, 2, 3} 这种字面量句法相比于构造方法(set([1, 2, 3]))要更快且更易读。后者的 速度要慢一些,因为 Python 必须先从 set 这个名字来查询构造方法,然后新建一个列表, 最后再把这个列表传入到构造方法里。但是如果是像 {1, 2, 3} 这样的字面量,Python 会 利用一个专门的叫作 BUILD_SET 的字节码来创建集合。
a = {1,2,3} #这个叫做集合字面量
print(type(a)) #<class 'set'>
a = set([1,2,3])
print(type(a)) #<class 'set'>
3.8.2 集合推导
a = {i for i in range(10) if i % 2==0}
print(a) #{0, 2, 4, 6, 8}
和别的推导用法都是一模一样的。和字典推导外面的括号也是一样的,不过字典推导要同时推导键值对,这里只有一个值哦!
3.8.3 集合的操作
s & z交 s | z 并 s - z 差集 s ^ z 对称差集
在 写 这 本 书 的 时 候,Python 有 个 缺 陷(issue 8743,http://bugs.python.org/ issue8743),里面说到 set() 的运算符(or、and、sub、xor 和它们相对应的 就地修改运算符)要求参数必须是 set() 的实例,这就导致这些运算符不能 被用在 collections.abc.Set 这个子类上面。这个缺陷已经在 Python 2.7 和 Python 3.4 里修复了,在你看到这本书的时候,它已经成了历史。
3.9 dict和set的背后
3.9.1 一个关于效率的实验
字典和集合的查找效率差距极小,而且速度极快,在1000万个浮点数的集合或字典中查询1000个数只需要0.0003秒,时间几乎可以忽略不计。但是对于列表而言来说需要97秒,时间就很慢了。
3.9.2 字典中的散列表
内置的 hash() 方法可以用于所有的内置类型对象。为了获取 my_dict[search_key] 背后的值,Python 首先会调用 hash(search_key) 来计算 search_key 的散列值,把这个值最低的几位数字当作偏移量,在散列表里查找表元(具 体取几位,得看当前散列表的大小)。若找到的表元是空的,则抛出 KeyError 异常。若不 是空的,则表元里会有一对 found_key:found_value。这时候 Python 会检验 search_key == found_key 是否为真,如果它们相等的话,就会返回 found_value。
3.9.3 dict的实现及其导致的结果
1.所有由用户自定义的对象默认都是可散列的,因为它们的散列值由 id() 来获取,而且它们 都是不相等的。\2. 字典在内存上的开销巨大 由于字典使用了散列表,而散列表又必须是稀疏的,这导致它在空间上的效率低下。举例 而言,如果你需要存放数量巨大的记录,那么放在由元组或是具名元组构成的列表中会是 比较好的选择;最好不要根据 JSON 的风格,用由字典组成的列表来存放这些记录。用元 组取代字典就能节省空间的原因有两个:其一是避免了散列表所耗费的空间,其二是无需 把记录中字段的名字在每个元素里都存一遍。
3.9.4 set的实现以及导致的结果
set 和 frozenset 的实现也依赖散列表,但在它们的散列表里存放的只有元素的引用(就 像在字典里只存放键而没有相应的值)。在 set 加入到 Python 之前,我们都是把字典加上 无意义的值当作集合来用的。
第4章 文本和字节序列
后面再看,这个先不看
第5章 一等函数
5.1 把函数视作对象
def fact(n):
'''
:param n:
:return: value
'''
return 1 if n < 2 else n * fact(n-1)
print(fact(3)) #6
print(fact.__doc__) # :param n:
:return: value
第一个参数接受一个函数名,后面的参数接受一个或多个可迭代的序列,返回的是一个集合。把函数依次作用在list中的每一个元素上,得到一个新的list并返回。注意,map不改变原list,而是返回一个新list。
def fact(n):
'''
:param n:
:return: value
'''
return 1 if n < 2 else n * fact(n-1)
f = fact
print(f(3)) #6
print(f) #<function fact at 0x00000275FC4CF040>
a = map(f,range(11))
print(a) #<map object at 0x000001BEC9131C70>
print(list(a)) #[1, 1, 2, 6, 24, 120, 720, 5040, 40320, 362880, 3628800]
可以看到可以给函数起别名,而且在map中,函数可以当成参数进行传递。有了一等函数,就可以使用函数式风格编程。函数式编程的特点之一是使用高阶函数—— 这是下一节的话题。
5.2 高阶函数
接受函数为参数,或者把函数作为结果返回的函数是高阶函数(higher-order function)。 map 函数就是一例。此外,内置函数 sorted 也是:可选的 key 参数用于 提供一个函数,它会应用到各个元素上进行排序。map,filter内置函数,但是很少用到了,可以使用生成器推导或列表推导代替。
5.3 匿名函数
lambda 关键字在 Python 表达式内创建匿名函数。除了作为参数传给高阶函数之外,Python 很少使用匿名函数。由于句法上的限制,非平凡 的 lambda 表达式要么难以阅读,要么无法写出。这个函数用处不太多
5.4 可调用对象
除了用户定义的函数,调用运算符(即 ())还可以应用到其他对象上。如果想判断对象能 否调用,可以使用内置的 callable() 函数。用户定义的函数 2.内置函数 3.内置方法 4.方法 5.类 6.类的实例 7.生成器函数