我发现流畅的python更适合我现在看,因为它写的很详细。而effective python知识点不是很连贯,我先看完这本书,再去过一遍effective python吧!
由于typora使用两个下划线就变成了自动转义,将字体变为粗体,所以文中的粗体就代表特殊方法。
这里主要研究类中的各种特殊方法(魔术方法)的用途
这里研究 _ _ 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 这 些函数。
首先明确一点,特殊方法的存在是为了被 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,当我们在控制台里打印一个向量的实例时,得到的 字符串可能会是
通过 add 和 mul,示例 1-2 为向量类带来了 + 和 * 这两个算术运算符。值得注意的 是,这两个方法的返回值都是新创建的向量对象,被操作的两个向量(self 或 other)还 是原封不动,代码里只是读取了它们的值而已。中缀运算符的基本原则就是不改变操作对 象,而是产出一个新的值。第 13 章会谈到更多这方面的问题。
我们对 bool 的实现很简单,如果一个向量的模是 0,那么就返回 False,其他情况则 返回 True。因为 bool 函数的返回类型应该是布尔型,所以我们通过 bool(abs(self)) 把模值变成了布尔值。
参见 https://docs.python.org/3/reference/datamodel.html,这里给出了83 个特殊方法的名字,其中 47 个用于实现算术运算、位运算和比较操作。
,如果 x 是一个内置类型的实例,那么 len(x) 的速度会非常快。背后的 原因是 CPython 会直接从一个 C 结构体里读取对象的长度,完全不会调用任何方法。len 之所以不是一个普通方法,是为了让 Python 自带的数据结构可以走后门, abs 也是同理。但是多亏了它是特殊方法,我们也可以把 len 用于自定义数据类型。这种 处理方式在保持内置类型的效率和保证语言的一致性之间找到了一个平衡点,也印证了 “Python 之禅”中的另外一句话:“不能让特例特殊到开始破坏既定规则。” 就是说对于len来说,如果用于自定数据类型他就会调用__len__方法,如果用于内置数据类型就会直接从C结构体里读取对象的长度,而不调用任何方法。保证了一致性。
序列能够按照是否能够容纳不同类型的数据分为容器序列和扁平序列:
容器序列: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)]
列表推导是构建列表(list)的快捷方式,生成器表达式则可以用来创建其他任何类型 的序列。
列表推导的通常使用原则是:只用列表推导来创建新的对象,并且尽量保持简短。如 序列构成的数组 | 19 果列表推导的代码超过了两行,你可能就要考虑是不是得用 for 循环重写了。列表推导可以帮助我们把一个序列或是其他可迭代类型中的元素过滤或是加工,然后再新 建一个列表。
这个再effective中写的比较详细,这里写的很简单,就不做记录了。
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循环时内层循环。
虽然也可以用列表推导来初始化元组、数组或其他序列类型,但是生成器表达式是更好的 选择。这是因为生成器表达式背后遵守了迭代器协议,可以逐个地产出元素,而不是先建 立一个完整的列表,然后再把这个列表传递到某个构造函数里。前面那种方式显然能够节 省内存。
colors = ['black', 'white']
sizes = ['S', 'M', 'L']
tshirts = ((color, size) for color in colors for size in sizes)
print(tshirts) # at 0x00000229B0DD1BA0>
for i in tshirts:
print(i) #('black', 'S')
('black', 'M')
('black', 'L')
('white', 'S')
('white', 'M')
('white', 'L')
可以看到的是使用生成器表达式会生成一个生成器,然后对这个生成器进行迭代可以取到其中的元素。
city, year, pop, chg, area = ('Tokyo', 2003, 32450, 0.66, 8014)
从上代码可以看出,元组中不同位置的元素代表了特定的信息,分别是城市,年份,人口,人口变化,面积。如果只把元组理解为不可变的列表,那其他信息——它所含有的元素的总数和它们的位 置——似乎就变得可有可无。但是如果把元组当作一些字段的集合,那么数量和位置信息 就变得非常重要了。
这部分主要讲了参数对应的拆包以及使用了*的参数不相等的拆包,这里再ep里详细讲过了,就不记录了。
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
元组内部嵌套元组,只要元素能够一一对应,就能够正常拆包。
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需要两个属性,一个是类名,一个是类中的属性名。其中属性名可传入由字符串组成的可迭代对象,或是上面所示由空格分开的字符串。可以通过属性名获取属性值,可以通过索引获取属性值
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,反向拼接 * |
上面列出了列表对象和元组对象拥有的共同方法,剩余的列表对象有的方法元组都没有,元组没有别的方法了。 |
当起止位置信息都可见时,我们可以快速计算出切片和区间的长度,用后一个数减去第 一个下标(stop - start)即可。这样做也让我们可以利用任意一个下标来把序列分割成不重叠的两部分,只要写成 my_ list[:x] 和 my_list[x:] 就可以了
可以对定义了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
[] 运算符里还可以使用以逗号分开的多个索引或者是切片,外部库 NumPy 里就用到了这 个特性,二维的 numpy.ndarray 就可以用 a[i, j] 这种形式来获取,抑或是用 a[m:n, k:l] 的方式来得到二维切片。
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]
赋值必须是一个可迭代对象,即使只要一个元素,也要写成列表的形式。
通常 + 号两侧的序列由相同类型的数据所 构成,在拼接的过程中,两个被操作的序列都不会被修改,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']]
可以看到使用列表推导和 * 生成列表产生了不一样的结果。因为对于使用* 的列表,对可变对象实际内部是对象的引用,所以产生了三个相同的列表对象,所以修改其中一个值,剩余的都变了,因为实际上是一个值。
+= 背后的特殊方法是 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 方法,因此 += 是就地加法。而不可变序列根 本就不支持这个操作,对这个方法的实现也就无从谈起。
list.sort方法会对原列表进行排序,也就是说不会把原列表复制一份。这也是这个方法的返 回值是 None 的原因,提醒你本方法不会新建一个列表。 list.sort 相反的是内置函数 sorted,它会新建一个列表作为返回值。这个方法可以接 受任何形式的可迭代对象作为参数,甚至包括不可变序列或生成器(见第 14 章)。而不管 sorted 接受的是怎样的参数,它最后都会返回一个列表。不管是 list.sort 方法还是 sorted 函数,都有两个可选的关键字参数。
在第二版中好像把这一小节给删除了,感觉也确实用不到,就先不看了,如果有需要再回来看吧。
如果我们需要一个只包含数字的列表,那么 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))
这个没有看懂
这里就做了很简单的介绍,就没必要看了。
利用 .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 中“不是容器序列”的序列,在其他地方你 可能找不到这样的用法。
dict 类型不但在各种程序里广泛使用,它也是 Python 语言的基石。模块的命名空间、 实例的属性和函数的关键字参数中都可以看到字典的身影。跟它有关的内置函数都在 builtins.dict 模块中。 正是因为字典至关重要,Python 对它的实现做了高度优化,而散列表则是字典类型性能出 众的根本原因。 集合(set)的实现其实也依赖于散列表,因此本章也会讲到它。
什么是可散列的数据类型?
如果一个对象是可散列的,那么在这个对象的生命周期中,它的散列值是不变 的,而且这个对象需要实现 hash() 方法。另外可散列对象还要有 eq() 方法,这样才能跟其他键做比较。如果两个可散列对象是相等的,那么它们的 散列值一定是一样的…原子不可变数据类型(str、bytes 和数值类型)都是可散列类型,frozenset 也是可散 列的,因为根据其定义,frozenset 里只能容纳可散列类型。元组的话,只有当一个元 组包含的所有元素都是可散列类型的情况下,它才是可散列的。一般来讲用户自定义的类型的对象都是可散列的,散列值就是它们的 id() 函数的返 回值,所以所有这些对象在比较的时候都是不相等的。
字典有多种构造方式。
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
利用字典推导可以把一个装满元组的列表变成两 个不同的字典。
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}
参见ep 16条有详细的介绍。setdefault与get方法比较,好处是他对于字典只需查询一次,而get方法要查询两次。
有时候为了方便起见,就算某个键在映射里不存在,我们也希望在通过这个键读取值的 时候能得到一个默认值。有两个途径能帮我们达到这个目的,一个是通过 defaultdict 这 个类型而不是普通的 dict,另一个是给自己定义一个 dict 的子类,然后在子类中实现 missing 方法。但是get方法也可以呀!
这里ef17条已经详细解读过了,就不说了
所有的映射类型在处理找不到的键的时候,都会牵扯到 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方法。
这里我总结的很不好,如果详细了解还是要去看书。
collections.OrderedDict 这个类型在添加键的时候会保持顺序,因此键的迭代次序总是一致的。OrderedDict 的 popitem 方法默认删除并返回的是字典里的最后一个元素,但是如果像 my_odict. popitem(last=False) 这样调用它,那么它删除并返回第一个被添加进去的元素。 这个在pytoch里被使用了哦!
就创造自定义映射类型来说,以 UserDict 为基类,总比以普通的 dict 为基类要来得方便。
没太看懂是怎么方便的。哈哈!
标准库里所有的映射类型都是可变的,但有时候你会有这样的需求,比如不能让用户错误 地修改某个映射。
集合中的元素必须是可散列的,set 类型本身是不可散列的,但是 frozenset 可以。因此 可以创建一个包含不同 frozenset 的 set。除了保证唯一性,集合还实现了很多基础的中缀运算符 。给定两个集合 a 和 b,a | b 返回 的是它们的合集,a & b 得到的是交集,而 a - b 得到的是差集。合理地利用这些操作,不仅 能够让代码的行数变少,还能减少 Python 程序的运行时间。这样做同时也是为了让代码更易 读,从而更容易判断程序的正确性,因为利用这些运算符可以省去不必要的循环和逻辑操作。
不要忘了,如果要创建一个空集,你必须用不带任何参数的构造方法 set()。 如果只是写成 {} 的形式,跟以前一样,你创建的其实是个空字典。{1, 2, 3} 这种字面量句法相比于构造方法(set([1, 2, 3]))要更快且更易读。后者的 速度要慢一些,因为 Python 必须先从 set 这个名字来查询构造方法,然后新建一个列表, 最后再把这个列表传入到构造方法里。但是如果是像 {1, 2, 3} 这样的字面量,Python 会 利用一个专门的叫作 BUILD_SET 的字节码来创建集合。
a = {1,2,3} #这个叫做集合字面量
print(type(a)) #
a = set([1,2,3])
print(type(a)) #
a = {i for i in range(10) if i % 2==0}
print(a) #{0, 2, 4, 6, 8}
和别的推导用法都是一模一样的。和字典推导外面的括号也是一样的,不过字典推导要同时推导键值对,这里只有一个值哦!
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 里修复了,在你看到这本书的时候,它已经成了历史。
字典和集合的查找效率差距极小,而且速度极快,在1000万个浮点数的集合或字典中查询1000个数只需要0.0003秒,时间几乎可以忽略不计。但是对于列表而言来说需要97秒,时间就很慢了。
内置的 hash() 方法可以用于所有的内置类型对象。为了获取 my_dict[search_key] 背后的值,Python 首先会调用 hash(search_key) 来计算 search_key 的散列值,把这个值最低的几位数字当作偏移量,在散列表里查找表元(具 体取几位,得看当前散列表的大小)。若找到的表元是空的,则抛出 KeyError 异常。若不 是空的,则表元里会有一对 found_key:found_value。这时候 Python 会检验 search_key == found_key 是否为真,如果它们相等的话,就会返回 found_value。
1.所有由用户自定义的对象默认都是可散列的,因为它们的散列值由 id() 来获取,而且它们 都是不相等的。\2. 字典在内存上的开销巨大 由于字典使用了散列表,而散列表又必须是稀疏的,这导致它在空间上的效率低下。举例 而言,如果你需要存放数量巨大的记录,那么放在由元组或是具名元组构成的列表中会是 比较好的选择;最好不要根据 JSON 的风格,用由字典组成的列表来存放这些记录。用元 组取代字典就能节省空间的原因有两个:其一是避免了散列表所耗费的空间,其二是无需 把记录中字段的名字在每个元素里都存一遍。
set 和 frozenset 的实现也依赖散列表,但在它们的散列表里存放的只有元素的引用(就 像在字典里只存放键而没有相应的值)。在 set 加入到 Python 之前,我们都是把字典加上 无意义的值当作集合来用的。
后面再看,这个先不看
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) #
a = map(f,range(11))
print(a) #
可以看到可以给函数起别名,而且在map中,函数可以当成参数进行传递。有了一等函数,就可以使用函数式风格编程。函数式编程的特点之一是使用高阶函数—— 这是下一节的话题。
接受函数为参数,或者把函数作为结果返回的函数是高阶函数(higher-order function)。 map 函数就是一例。此外,内置函数 sorted 也是:可选的 key 参数用于 提供一个函数,它会应用到各个元素上进行排序。map,filter内置函数,但是很少用到了,可以使用生成器推导或列表推导代替。
lambda 关键字在 Python 表达式内创建匿名函数。除了作为参数传给高阶函数之外,Python 很少使用匿名函数。由于句法上的限制,非平凡 的 lambda 表达式要么难以阅读,要么无法写出。这个函数用处不太多
除了用户定义的函数,调用运算符(即 ())还可以应用到其他对象上。如果想判断对象能 否调用,可以使用内置的 callable() 函数。用户定义的函数 2.内置函数 3.内置方法 4.方法 5.类 6.类的实例 7.生成器函数
class BingoCage:
def __init__(self, items):
self._items = list(items) # <1>
def pick(self): # <3>
try:
return self._items.pop()
except IndexError:
raise LookupError('pick from empty BingoCage') # <4>
def __call__(self): # <5>
return self.pick()
a = BingoCage(range(3))
print(a()) #2
print(a.pick()) #1
函数存在一些属性是常规对象没有的。计算两个属性集合的差集便能 得到函数专有属性列表,用户定义函数与常规对象相比,专有属性有:annotations,call ,closure ,…还有好几个就不列举了,下面会分析他们的作用
python中的函数提供了灵活的传参方式。
def tag(name, *content, class_=None, **attrs):
"""Generate one or more HTML tags"""
if class_ is not None:
attrs['class'] = class_
attr_pairs = (f' {attr}="{value}"' for attr, value
in sorted(attrs.items()))
attr_str = ''.join(attr_pairs)
if content:
elements = (f'<{name}{attr_str}>{c}{name}>'
for c in content)
return '\n'.join(elements)
else:
return f'<{name}{attr_str} />'
print(tag("br")) #
1
print(tag('p', 'hello')) #hello
2
print(tag('p', 'hello', id="33")) #hello
3
print(tag(content='testing', name="img")) #
4
my_tag = {'name': 'img', 'title': 'Sunset Boulevard','src': 'sunset.jpg', 'class': 'framed'}
print(tag(my_tag)) #<{'name': 'img', 'title': 'Sunset Boulevard', 'src': 'sunset.jpg', 'class': 'framed'} /> 5
1:输入一个位置参数,被name捕获。class_和content都为空,所以输出最后一个return
2:输入了两个位置参数,第一个参数"p"被那么捕获,第二个参数"hello被content捕获",如果后面还有任意多个位置参数都会被content捕获。class_为空,content不为空,所以输出第二个return内容
3:输出两个位置参数,一个关键字参数。两个位置参数参见2,这个关键字参数会被attrs捕获,如果还有任意多个关键字参数都会被attrs捕获哦!除了class_。
4.这里的content和*content不一样哦!content关键字参数会被attrs捕获
def clip(text, max_len=80):
"""Return text clipped at the last space before or after max_len
"""
end = None
if len(text) > max_len:
space_before = text.rfind(' ', 0, max_len)
if space_before >= 0:
end = space_before
else:
space_after = text.rfind(' ', max_len)
if space_after >= 0:
end = space_after
if end is None: # no spaces were found
end = len(text)
return text[:end].rstrip()
print(clip.__defaults__) #(80,) #
print( clip.__code__) #
print(clip.__code__.co_varnames) #('text', 'max_len', 'end', 'space_before', 'space_after')
print(clip.__code__.co_argcount) #2
可以看到函数可以调用以上的方法来观察函数的参数
def clip(text, max_len=80):
"""Return text clipped at the last space before or after max_len
"""
end = None
if len(text) > max_len:
space_before = text.rfind(' ', 0, max_len)
if space_before >= 0:
end = space_before
else:
space_after = text.rfind(' ', max_len)
if space_after >= 0:
end = space_after
if end is None: # no spaces were found
end = len(text)
return text[:end].rstrip()
from inspect import signature
sig = signature(clip)
print(sig) #(text, max_len=80)
for name, param in sig.parameters.items():
print(param.kind, ':', name, '=', param.default) #POSITIONAL_OR_KEYWORD : text = POSITIONAL_OR_KEYWORD : max_len = 80
inspect.signature 函数返回一个 inspect.Signature 对象,它有一个 parameters 属性,这是一个有序映射,把参数名和 inspect.Parameter 对象对应起来。各个 Parameter 属性 也有自己的属性,例如 name、default 和 kind。
inspect.Signature 对象有个 bind 方法,它可以把任意个参数绑定到签名中的形参上,所 用的规则与实参到形参的匹配方式一样。框架可以使用这个方法在真正调用函数前验证参 数,可以起到一个辅助作用,帮助我们判断,我们传入的实参对不对哦!代码就不放上了太多了。
def clip(text:str, max_len:'int > 0'=80) -> str: # <1>
"""Return text clipped at the last space before or after max_len
"""
end = None
if len(text) > max_len:
space_before = text.rfind(' ', 0, max_len)
if space_before >= 0:
end = space_before
else:
space_after = text.rfind(' ', max_len)
if space_after >= 0:
end = space_after
if end is None: # no spaces were found
end = len(text)
return text[:end].rstrip()
print(clip.__annotations__) #{'text': , 'max_len': 'int > 0', 'return': }
这个函数与上面的非注解函数区别只在第一行,可以看到函数注解在每个参数后面都跟上了这个参数的数据类型。函数声明中的各个参数可以在 : 之后增加注解表达式。如果参数有默认值,注解放在参数 名和 = 号之间。如果想注解返回值,在 ) 和函数声明末尾的 : 之间添加 -> 和一个表达式。 那个表达式可以是任何类型。注解中最常用的类型是类(如 str 或 int)和字符串(如 ‘int > 0’)。Python 对注解所做的唯一的事情是,把它们存储在函数的 annotations 属性里。仅 此而已,Python 不做检查、不做强制、不做验证,什么操作都不做。换句话说,注解对 Python 解释器没有任何意义。注解只是元数据,可以供 IDE、框架和装饰器等工具使用。
首先大概了解一下什么叫做函数式编程:函数式编程是一种编程范式,看待问题的一种方式,每一个函数都是为了用小函数组织成更大的函数,函数的参数也是函数,函数返回的也是函数。 而我们常见的编程范式有命令式编程、函数式编程、逻辑式编程。 常见的面向对象编程是也是一种命令式编程。
reduce Python有一个内建函数reduce,函数将一个数据集合(链表,元组等)中的所有数据进行下列操作:用传给 reduce 中的函数 function(有两个参数)先对集合中的第 1、2 个元素进行操作,得到的结果再与第三个数据用 function 函数运算,最后得到一个结果。
from functools import reduce
from operator import mul
def fact(n):
return reduce(mul, range(1, n+1))
print(fact(5)) #120
像Python这样构建于类C语言之上的函数式语言,由于语言本身提供了编写循环代码的能力,内置函数虽然提供函数式编程的接口,但一般在内部还是使用循环实现的。同样的,如果发现内建函数无法满足你的循环需求,不妨也封装它,并提供一个接口,这种方式可以有效地提高效率。
from functools import reduce
from operator import mul
def fact(n):
return reduce(mul, range(1, n+1))
print(fact(5))
operator 模块中还有一类函数,能替代从序列中取出元素或读取对象属性的 lambda 表达 式:因此,itemgetter 和 attrgetter 其实会自行构建函数。
functools.partial 这个高阶函数用于部分应用一个函数。部分应用是指,基于一个函数创 建一个新的可调用对象,把原函数的某些参数固定。使用这个函数可以把接受一个或多个 参数的函数改编成需要回调的 API,这样参数更少。
from operator import mul
from functools import partial
triple = partial(mul,3)
print(triple(7)) #21
a = map(triple,range(1,10)) #map函数参数为:函数,可迭代对象。返回一个集合
print(a) #
print(list(a)) #[3, 6, 9, 12, 15, 18, 21, 24, 27]
代码第三行,partial会将位置参数3冻结,之后我们只需要传入一个参数就可以了。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7FDkEHUV-1675785176820)(C:\Users\sz\AppData\Roaming\Typora\typora-user-images\image-20220806150610788.png)]
order:所定的货物 due:实际支付的钱 promotion:促销活动 fidelitypromo:积分活动 bulkitempromo:商品总量活动 LargeOrderpromo:商品种类活动。代码中的LineItem代表商品。
from abc import ABC, abstractmethod
from collections import namedtuple
Customer = namedtuple('Customer', 'name fidelity')
class LineItem: #商品
def __init__(self, product, quantity, price):
self.product = product #名字
self.quantity = quantity #数量
self.price = price #价格
def total(self):
return self.price * self.quantity
class Order: # the Context
def __init__(self, customer, cart, promotion=None): #这个promotion要我们自己指定
self.customer = customer
self.cart = list(cart)
self.promotion = promotion
def total(self):
if not hasattr(self, '__total'):
self.__total = sum(item.total() for item in self.cart)
return self.__total
def due(self):
if self.promotion is None:
discount = 0
else:
discount = self.promotion.discount(self) #promotion的dsicount会调用total方法
return self.total() - discount
def __repr__(self):
fmt = ''
return fmt.format(self.total(), self.due())
class Promotion(ABC): # the Strategy: an Abstract Base Class
@abstractmethod
def discount(self, order):
"""Return discount as a positive dollar amount"""
class FidelityPromo(Promotion): # first Concrete Strategy
"""5% discount for customers with 1000 or more fidelity points"""
def discount(self, order):
return order.total() * .05 if order.customer.fidelity >= 1000 else 0
class BulkItemPromo(Promotion): # second Concrete Strategy
"""10% discount for each LineItem with 20 or more units"""
def discount(self, order):
discount = 0
for item in order.cart:
if item.quantity >= 20:
discount += item.total() * .1
return discount
class LargeOrderPromo(Promotion): # third Concrete Strategy
"""7% discount for orders with 10 or more distinct items"""
def discount(self, order):
distinct_items = {item.product for item in order.cart}
if len(distinct_items) >= 10:
return order.total() * .07
return 0
custormer = Customer("sz",1100)
cart = [LineItem("book",3,2)]
print(Order(custormer,cart,FidelityPromo())) #
该模式中,我们首先定义了一个Order类(连接上下文,用来连接所有的类成为一个整体),Order需要我们传入顾客参数,商品参数,以及优惠策略参数,定义了两个方法一个方法是total用于计算该顾客商品总价格,一个是due方法用于计算优惠后的总价格。当然对于顾客需要设计一个类,这个类实现了存储顾客姓名,积分的属性。还需要设计一个商品类,这个实现了存储商品姓名,价格,数目,以及计算总价格的方法。还设计了计算优惠策略的类,该类有一个基类,三个类继承了这个基类,这三个类是按照三种不同优惠策略设计的类,类中实现了计算优惠价格的方法。
该模式为面向对象的模式,总分的结构,首先设计一个大类,该类实现主要功能。然后缺少什么类,我们再去实现什么类就好了。
在上段代码中,每个具体策略都是一个类,而且都只定义了一个方法,即 discount。此外, 策略实例没有状态(没有实例属性)。你可能会说,它们看起来像是普通的函数——的确 如此。接下来是对上段代码的重构,把具体策略换成了简单的函数,而且去掉了 Promo 抽象类。
就是把上面的实现优惠策略的类写成函数而已,这样可以不写基类。
假如某位顾客同时满足以上三种优惠方法的条件,我们想找出一个最优的策略该怎么办呢?可以很简单的实现。
promos = [fidelity_promo, bulk_item_promo, large_order_promo] ➊
def best_promo(order): ➋
"""选择可用的最佳折扣
"""
return max(promo(order) for promo in promos) ➌
这样就OK啦,是不是很简单嘞!熟悉了函数是一等对象,我们就可以很容易的想到设计列表这样的结构来存储函数或类啦!
如果我们新增策略后,上段代码那个promos列表却没有这个类,这时候肯定就不对啦!使用内置的globals模块来捕获所有的名字。
promos = [globals()[name] for name in globals() ➊
if name.endswith('_promo') ➋
and name != 'best_promo'] ➌
def best_promo(order):
"""选择可用的最佳折扣
"""
return max(promo(order) for promo in promos) ➍
很简单的介绍,咱也不太懂
装饰器是可调用的对象,其参数是另一个函数(被装饰的函数)。2 装饰器可能会处理被装 饰的函数,然后把它返回,或者将其替换成另一个函数或可调用对象。
def deco(func):
def inner():
print('running inner()')
return inner
@deco
def target():
print('running target()')
print(target) #.inner at 0x0000022B1D186940>
target() #running inner()
装饰器的一个关键特性是,它们在被装饰的函数定义之后立即运行。这通常是在导入时 (即 Python 加载模块时)
registry = [] # <1>
def register(func): # <2>
print('running register(%s)' % func) # <3>
registry.append(func) # <4>
return func # <5>
@register # <6>
def f1():
print('running f1()')
@register
def f2():
print('running f2()')
def f3(): # <7>
print('running f3()')
def main(): # <8>
print('running main()')
print('registry ->', registry)
f1()
f2()
f3()
if __name__=='__main__':
main() # <9>
输出如下:
running register(<function f1 at 0x0000025CC73D6670>)
running register(<function f2 at 0x0000025CC73D6940>)
running main()
registry -> [<function f1 at 0x0000025CC73D6670>, <function f2 at 0x0000025CC73D6940>]
running f1()
running f2()
running f3()
registry -> [<function f1 at 0x0000025CC73D6670>, <function f2 at 0x0000025CC73D6940>]
➊ registry 保存被 @register 装饰的函数引用。 ➋ register 的参数是一个函数。 ➌ 为了演示,显示被装饰的函数。 ➍ 把 func 存入 registry。 ➎ 返回 func:必须返回函数;这里返回的函数与通过参数传入的一样。 ➏ f1 和 f2 被 @register 装饰。 ➐ f3 没有装饰。 ➑ main 显示 registry,然后调用 f1()、f2() 和 f3()。 ➒ 只有把 registration.py 当作脚本运行时才调用 main()。总结:装饰器在导入模块时立即执行,而装饰器装饰的函数在我们调用时才执行. 装饰器返回的必须是一个函数.
在6.1.3中我们将所有的策略都放入一个列表然后寻找最优策略, 但是如果我们新增策略时,这个列表不会自动维护,但是我们可以使用装饰器来维护一个列表,用这个装饰器去装饰每个策略就好了. 就像7.2所做的,这个装饰器可以把函数名放入一个列表。
不过,多数装饰器会修改被装饰的函数。通常,它们会定义一个内部函数,然后将其返 回,替换被装饰的函数。使用内部函数的代码几乎都要靠闭包才能正确运作。为了理解闭 包,我们要退后一步,先了解 Python 中的变量作用域。
def f1(a):
print(a)
print(b)
b = 1
f1(3) #3 1
这样我们给b赋值为1,这时候b是一个全局变量,可以正常输出
b = 6
def f2(a):
print(a)
print(b)
b = 9
f2(3)
#输出为:
Traceback (most recent call last):
File "C:\Users\sz\Desktop\study\leetcode\test.py", line 7, in
f2(3)
File "C:\Users\sz\Desktop\study\leetcode\test.py", line 4, in f2
print(b)
UnboundLocalError: local variable 'b' referenced before assignment
3
可以看到3正常输出了,但是b不能够正常输出,程序报错。因为python在编译函数的定义体时,判断b是一个局部变量,但是在print(b)时,b还未定义,所以出错。
闭包就是函数嵌套函数。,只有嵌套在其他函数中的函数才可能需要处理不在全局作用域中的外部变量。
def make_averager():
series = []
def averager(new_value):
series.append(new_value)
total = sum(series)
return total/len(series)
return averager
avg = make_averager()
print(avg(10)) #10
print(avg(11)) #10.5
可以看到make_averager()会返回函数averager,所以avg = make_averager()这个语句会返回函数averager,然后我们调用avg就相当于调用averager函数。因为我们只调用了函数make_averager()一次,所以series只创建了一次,所以我们能够计算输出的累加平均值。这只是我便于理解去这么记录的,实际过程并不是这样的。书上和这个完全不一样哦!
实现一个对函数运行时间计时的装饰器。
import time
def clock(func):
def clocked(*args):
t0 = time.time()
result = func(*args)
elapsed = time.time() - t0
name = func.__name__
arg_str = ', '.join(repr(arg) for arg in args)
print('[%0.8fs] %s(%s) -> %r' % (elapsed, name, arg_str, result))
return result
return clocked
@clock
def snooze(t):
time.sleep(t)
print("我睡了%s秒"%t)
@clock
def factorial(n):
return 1 if n < 2 else n*factorial(n-1)
if __name__=='__main__':
print('*' * 40, 'Calling snooze(.123)')
snooze(.123)
print('*' * 40, 'Calling factorial(6)')
print('6! =', factorial(6))
print(factorial.__name__)
输出:
**************************************** Calling snooze(.123)
我睡了0.123秒
[0.13531947s] snooze(0.123) -> None
**************************************** Calling factorial(6)
[0.00000000s] factorial(1) -> 1
[0.00000000s] factorial(2) -> 2
[0.00000000s] factorial(3) -> 6
[0.00000000s] factorial(4) -> 24
[0.00000000s] factorial(5) -> 120
[0.00000000s] factorial(6) -> 720
6! = 720
clocked
注意看到我们输出factorial的__name__时候,输出却为clocked。这时因为factorial作为参数传给clock,clock返回clocked函数,python解释器将clocked赋值给factorial,因为实际上我们是要调用clocked。调用假factorial(clocked)函数时,内部会调用真正的factorial函数,并且也会返回真实factorial函数的返回值。所以可以看到虽然调用的是假factorial(clocked)函数,但是完全实现了真factorial函数的功能,并且也得到了想要的结果,只是说在这个过程中我们又干了点别的事。这个例子中,这个别的事就是求函数运行时间。
Python 内置了三个用于装饰方法的函数:property、classmethod 和 staticmethod。property 在 19.2 节讨论,另外两个在 9.4 节讨论。
functools.lru_cache 是非常实用的装饰器,它实现了备忘(memoization)功能。这是一 项优化技术,它把耗时的函数的结果保存起来,避免传入相同的参数时重复计算。LRU 三 个字母是“Least Recently Used”的缩写,表明缓存不会无限制增长,一段时间不用的缓存 条目会被扔掉。生成第 n 个斐波纳契数这种慢速递归函数适合使用 lru_cache。
我实验使用lru_cache计算30的fib花费0.000000秒,而不用这个方法需要花费16.67秒。差距好大哦!
把 @d1 和 @d2 两个装饰器按顺序应用到 f 函数上,作用相当于 f = d1(d2(f))。
解析源码中的装饰器时,Python 把被装饰的函数作为第一个参数传给装饰器函数。那怎么 让装饰器接受其他参数呢?答案是:创建一个装饰器工厂函数,把参数传给它,返回一个 装饰器,然后再把它应用到要装饰的函数上。
这个就是说变量只是一个指向对象的名字而已。
可以给一个对象起不同的名字,这些名字都指向相同的对象。
每个变量都有标识、类型和值。对象一旦创建,它的标识绝不会变;你可以把标识理解为对象在内存中的地址。is 运算符比较两个对象的标识;id() 函数返回对象标 识的整数表示。is 运算符比 == 速度快,因为它不能重载,所以 Python 不用寻找并调用特殊方法,而是直接比较两个整数 ID。而 a == b 是语法糖,等同于 a.eq(b)。继承自 object 的 eq 方法比较两个对象的 ID,结果与 is 一样。但是多数内置类型使用更有意义的方式覆盖了 eq 方法,会考虑对象属性的值。相等性测试可能涉及大量处理工作,例如,比较大型 集合或嵌套层级深的结构时。
a = (1,2,[30,40])
b = (1,2,[30,40])
print(id(a)) #1952194351872
print(id(b)) #1952196820160
a[-1].append(99)
print(a) #(1, 2, [30, 40, 99])
print(b) #(1, 2, [30, 40])
print(id(a)) #1952194351872
print(id(b)) #1952196820160
c = a
print(id(a)) #1952194351872
print(id(c)) #1952194351872
a = (1,2)
b = (1,2)
print(id(a)) #1952193857984
print(id(b)) #1952193857984
元组的相对不可变性。可以看到当我们新建两个变量a,b时,由于这两个元组内包含可变对象列表,为了维护列表的可变性,所以这个元组其实是一个可变的,所以a,b虽然是一个元组,但是他俩却是不同的对象。由于元组内的列表其实是实际列表的引用,所以即使修改这个列表,也不会改变元组的地址。最后新建变量a,b,他们是一个元组,内部是整形,是不可变的,所以这个元组是不可变的,所以a,b的地址一样,他们指向相同的对象。
l1 = [3, [66, 55, 44], (7, 8, 9)]
l2 = list(l1) # ➊
l1.append(100) # ➋
l1[1].remove(55) # ➌
print('l1:', l1) #l1: [3, [66, 44], (7, 8, 9), 100]
print('l2:', l2) #l2: [3, [66, 44], (7, 8, 9)]
l2[1] += [33, 22] # ➍
l2[2] += (10, 11) # ➎
print('l1:', l1) #l1: [3, [66, 44, 33, 22], (7, 8, 9), 100]
print('l2:', l2) #l2: [3, [66, 44, 33, 22], (7, 8, 9, 10, 11)]
注意使用list()方法与上段代码完全创建两个对象又不一样,可以看到对于上段代码中即使对于元组内的列表而言,两个指向的也是不同的对象。而对于list()方法列表内的列表与元组,l1和l2其实指向的都是同一个对象,所以你修改一个会对另一个产生影响。
➊ l2 是 l1 的浅复制副本。此时的状态如图 8-3 所示。 ➋ 把 100 追加到 l1 中,对 l2 没有影响。 ➌ 把内部列表 l1[1] 中的 55 删除。这对 l2 有影响,因为 l2[1] 绑定的列表与 l1[1] 是同 一个。 ➍ 对可变的对象来说,如 l2[1] 引用的列表,+= 运算符就地修改列表。这次修改在 l1[1] 中也有体现,因为它是 l2[1] 的别名。 ➎ 对元组来说,+= 运算符创建一个新元组,然后重新绑定给变量 l2[2]。这等同于 l2[2] = l2[2] + (10, 11)。现在,l1 和 l2 中最后位置上的元组不是同一个对象。
浅复制没什么问题,但有时我们需要的是深复制(即副本不共享内部对象的引用)。copy 模块提供的 deepcopy 和 copy 函数能为任意对象做深复制和浅复制。
import copy
l1 = [3, [66, 55, 44], (7, 8, 9)]
# l2 = list(l1) # ➊
l2 = copy.deepcopy(l1)
l1.append(100) # ➋
l1[1].remove(55) # ➌
print('l1:', l1) #[3, [66, 44], (7, 8, 9), 100]
print('l2:', l2) #[3, [66, 55, 44], (7, 8, 9)]
l2[1] += [33, 22] # ➍
l2[2] += (10, 11) # ➎
print('l1:', l1) #[3, [66, 44], (7, 8, 9), 100]
print('l2:', l2) #[3, [66, 55, 44, 33, 22], (7, 8, 9, 10, 11)]
Python 唯一支持的参数传递模式是共享传参(call by sharing)。函数内部的形参 是实参的别名。这种方案的结果是,函数可能会修改作为参数传入的可变对象,但是无法修改那些对象的 标识(即不能把一个对象替换成另一个对象)。
class HauntedBus:
"""A bus model haunted by ghost passengers"""
def __init__(self, passengers=[]): # <1>
self.passengers = passengers # <2>
def pick(self, name):
self.passengers.append(name) # <3>
def drop(self, name):
self.passengers.remove(name)
bus1 = HauntedBus(['Alice', 'Bill'])
print(bus1.passengers) #['Alice', 'Bill']
print(HauntedBus.__init__.__defaults__) #([],)
bus2 = HauntedBus()
print(bus2.passengers) #[]
print(HauntedBus.__init__.__defaults__) #([],)
bus2.pick('Carrie')
print(bus2.passengers) #['Carrie']
print(HauntedBus.__init__.__defaults__) #(['Carrie'],)
bus3 = HauntedBus()
print(bus3.passengers) #['Carrie']
print(HauntedBus.__init__.__defaults__) #(['Carrie'],)
将空列表(可变参数)设为默认值后,发现我们创建实例时,如果没有指定参数(即使用默认值)会导致如果我们往乘客这个列表添加东西时,它会被添加进默认值里。导致以后再使用这个默认值,这个默认值就不是空列表了。就会导致出现bus3的情况。根本是因为 self. passengers 变成了 passengers 参数默认值的别名。出现这个问题的根源是,默认值在定义 函数时计算(通常在加载模块时),因此默认值变成了函数对象的属性。因此,如果默认 值是可变对象,而且修改了它的值,那么后续的函数调用都会受到影响。
对于传入的可变参数,如果我们想不修改它,那么可以利用深拷贝或者浅拷贝操作来防止修改。
del 语句删除名称,而不是对象。del 命令可能会导致对象被当作垃圾回收,但是仅当删除 的变量保存的是对象的最后一个引用,或者无法得到对象时。2 重新绑定也可能会导致对象 的引用数量归零,导致对象被销毁。只有当一个对象没有变量指向它时,它才会被删除。
import weakref
s1 = {1,2,3}
s2 = weakref.ref(s1)
print(s2) #
print(s2()) #{1, 2, 3}
s1 = {1,2,3,4}
print(s2()) #None
s2是s1的弱引用,所以当s1指向别的对象时,s2也没有了。
WeakValueDictionary 类实现的是一种可变映射,里面的值是对象的弱引用。被引用的对象 在程序中的其他地方被当作垃圾回收后,对应的键会自动从 WeakValueDictionary 中删除。 因此,WeakValueDictionary 经常用于缓存。
import weakref
class Cheese:
def __init__(self, kind):
self.kind = kind
def __repr__(self):
return 'Cheese(%r)' % self.kind
stock = weakref.WeakValueDictionary()
catalog = [Cheese('Red Leicester'), Cheese('Tilsit'),Cheese('Brie'), Cheese('Parmesan')]
for cheese in catalog:
stock[cheese.kind] = cheese
print(list(stock.keys())) #['Red Leicester', 'Tilsit', 'Brie', 'Parmesan']
del catalog
print(list(stock.keys())) #['Parmesan']
del cheese
print(list(stock.keys())) #[]
为什么del catalog后,弱引用stock里面还有一个元素呢,这个是因为在for循环时被cheese变量保留下来的,看到我们del cheese后,stock里就什么都没有了
不是每个 Python 对象都可以作为弱引用的目标(或称所指对象)。基本的 list 和 dict 实 例不能作为所指对象,但是它们的子类可以轻松地解决这个问题。但是,int 和 tuple 实例不能作 为弱引用的目标,甚至它们的子类也不行。这些局限基本上是 CPython 的实现细节,在其他 Python 解释器中情况可能不一样。这些局 限是内部优化导致的结果
得益于 Python 数据模型,自定义类型的行为可以像内置类型那样自然。实现如此自然的行 为,靠的不是继承,而是鸭子类型(duck typing):我们只需按照预定行为实现对象所需 的方法即可。
每门面向对象的语言至少都有一种获取对象的字符串表示形式的标准方式。Python 提供了 两种方式。实现 repr 和 str 特殊方法,为 repr() 和 str() 提供支持。
import math
from array import array
class Vector2d():
typecode = 'd'
def __init__(self,x,y):
self.x = x
self.y = y
def __iter__(self):
return(i for i in (self.x,self.y))
def __repr__(self):
return "%s(%d,%d)"%(type(self).__name__,self.x,self.y)
def __str__(self):
return "(%d,%d)"%(self.x,self.y)
def __eq__(self, other):
return tuple(iter(self)) == tuple(iter(other))
def __abs__(self):
return math.hypot(self.x, self.y)
def __bool__(self):
return bool(abs(self))
def __bytes__(self):
return (bytes([ord(self.typecode)]) + bytes(array(self.typecode, self)))
########Test#########
vec = Vector2d(3,4)
x,y = vec #调用__iter__()
print(x,y) #3 4
print(vec) #(3,4) print默认调用__str__()
print(repr(vec)) #Vector2d(3,4) 可以显示调用__repr__
print(abs(vec)) #5 调用__abs__
vec_2 = Vector2d(3,4)
print(vec == vec_2) #True 调用__eq__
print(bytes(vec)) #b'd\x00\x00\x00\x00\x00\x00\x08@\x00\x00\x00\x00\x00\x00\x10@'
print(bool(vec)) #True 调用__bool__()
print(bool(Vector2d(0,0)))
print(vec == [3,4]) #True 这是__eq__()方法的一个缺点,事实上做这两个东西不相等
@classmethod # <1>
def frombytes(cls, octets): # <2>
typecode = chr(octets[0]) # <3>
memv = memoryview(octets[1:]).cast(typecode) # <4>
return cls(*memv) # <5>
加了这么一个方法用于将bytes输出转换为unicode
先来看 classmethod。上段代码展示了它的用法:定义操作类,而不是操作实例的方法。 classmethod 改变了调用方法的方式,因此类方法的第一个参数是类本身,而不是实例。 classmethod 最常见的用途是定义备选构造方法,例如示例 9-3 中的 frombytes。注意, frombytes 的最后一行使用 cls 参数构建了一个新实例,即 cls(*memv)。按照约定,类方 法的第一个参数名为 cls(但是 Python 不介意具体怎么命名)。 staticmethod 装饰器也会改变方法的调用方式,但是第一个参数不是特殊的值。其实,静 态方法就是普通的函数,只是碰巧在类的定义体中,而不是在模块层定义。
class Trial():
def __init__(self,x):
self.x = x
@classmethod
def cm(cls):
print("我是类方法")
@staticmethod
def sm():
print("我是静态方法")
def __str__(self):
return str(self.x)
a = Trial(3)
print(a) #3
a.cm() #我是类方法
a.sm() #我是静态方法
Trial.cm() #我是类方法
Trial.sm() #我是静态方法
Trial.__str__() #TypeError: __str__() missing 1 required positional argument: 'self'
可以看到对于静态方法和类方法,我们完全可以直接通过类来进行调用。但是对于方法而言必须通过实例进行调用。如果我们通过类进行调用,它会提醒你需要传入一个参数,而这个参数就是类的实例。
内置的 format() 函数和 str.format() 方法把各个类型的格式化方式委托给相应的 .format(format_spec) 方法。format_spec 是格式说明符,它是: • format(my_obj, format_spec) 的第二个参数,或者 • str.format() 方法的格式字符串,{} 里代换字段中冒号后面的部分。
a = 1/2.343543
print(a) #0.4267043531951409
print(format(a,"0.5f")) #0.42670
print("我的大小是:{s:0.4f}".format(s=a)) #我的大小是:0.4267
print("我的大小是:{}".format(a)) #我的大小是:0.4267043531951409
b = 3
print(format(b,"b")) #11
对Vector2d类,现在我们又可以添加一个方法喽!
def __format__(self, fmt_spec=''):
components = (format(c, fmt_spec) for c in self) # ➊
return '({}, {})'.format(*components)
我们在使用format(Vector2d实例,“.2f”),是可以这样使用的,指定实例和想要的输出格式,就可以得到我们想要的格式啦。这个例子是保留小数点后两位。
我们将把 Vector2d 变成可散列的,这样便可以构 建向量集合,或者把向量当作 dict 的键使用。不过在此之前,必须让向量不可变。
class Vector2d:
typecode = 'd'
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
这样就吧Vector2d变成不可变的了,因为x,y的不能修改。下面我们就可以定义hash方法了。注意,我们让这些向量不可变是有原因的,因为这样才能实现 hash 方法。这个方法 应该返回一个整数,理想情况下还要考虑对象属性的散列值(eq 方法也要使用),因 为相等的对象应该具有相同的散列值。根据特殊方法 hash 的文档(https://docs.python. org/3/reference/datamodel.html),最好使用位运算符异或(^)混合各分量的散列值——我 们会这么做。
def __hash__(self):
return hash(self.x) ^ hash(self.y)
class Vector2d:
typecode = 'd'
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
a = Vector2d(3,4)
print(a.__dict__) #{'_Vector2d__x': 3.0, '_Vector2d__y': 4.0}
在变量前加双下划线,表示该属性是私有属性(受保护的属性),外部不可访问,因为在内部这个属性名被改写了。我们可以通过这个被改写的名字在外部进行访问。
默认情况下,Python 在各个实例中名为 dict 的字典里存储实例属性。如 3.9.3 节所述, 为了使用底层的散列表提升访问速度,字典会消耗大量内存。如果要处理数百万个属性不 多的实例,通过 slots 类属性,能节省大量内存,方法是让解释器在元组中存储实例 属性,而不用字典。
class Vector2d:
__slots__ = ('__x', '__y')
typecode = 'd'
在类中定义 slots 属性的目的是告诉解释器:“这个类中的所有实例属性都在这儿 了!”这样,Python 会在各个实例中使用类似元组的结构存储实例变量,从而避免使用消 耗内存的 dict 属性。如果有数百万个实例同时活动,这样做能节省大量内存。
Python 有个很独特的特性:类属性可用于为实例属性提供默认值。Vector2d 中有个 typecode 类属性,bytes 方法两次用到了它,而且都故意使用 self.typecode 读取它的值。因为 Vector2d 实例本身没有 typecode 属性,所以 self.typecode 默认获取的是 Vector2d.typecode 类属性的值。
这一章讲如何优化上一章=所写的Vector2d类,并添加更多的功能。
我们重构了Vector2d,使其能够接收输出任意维度的向量。
import math
from array import array
class Vector():
typecode = 'd'
def __init__(self,argtuple):
self.__com = array(self.typecode,argtuple)
def __iter__(self):
return(i for i in self.__com)
def __repr__(self):
return "%s%s"%(type(self).__name__,self.__str__())
def __str__(self):
return str(tuple(self.__com))
def __eq__(self, other):
return tuple(iter(self)) == tuple(iter(other))
def __abs__(self):
return math.sqrt(sum(i*i for i in self.__com))
def __bool__(self):
return bool(abs(self))
def __bytes__(self):
return (bytes([ord(self.typecode)]) + bytes(self.__com))
def __format__(self, fmt_spec=''):
components = (format(c, fmt_spec) for c in self) # ➊
return '({}, {})'.format(*components)
########Test#########
vec = Vector([3,4,5,6,7])
x,*y = vec
print(x,*y) #3 4
print(vec) #(3,4) print默认调用__str__()
print(repr(vec)) #Vector2d(3,4) 可以显示调用__repr__
print(abs(vec)) #5 调用__abs__
vec_2 = Vector([3,4])
print(vec == vec_2) #True 调用__eq__
print(bytes(vec)) #b'd\x00\x00\x00\x00\x00\x00\x08@\x00\x00\x00\x00\x00\x00\x10@'
print(bool(vec)) #True 调用__bool__()
print(bool(Vector([0,0])))
print(vec == [3,4]) #True 这是__eq__()方法的一个缺点,事实上做这两个东西不相等
print(format(vec,".2f"))
print(vec.__dict__) {'_Vector__com': array('d', [3.0, 4.0])}
在面向对象编程中,协议是非正式的接口,只在文档中定义,在代码中不定义。例如, Python 的序列协议只需要 len 和 getitem 两个方法。任何类(如 Spam),只要使用 标准的签名和语义实现了这两个方法,就能用在任何期待序列的地方。Spam 是不是哪个类 的子类无关紧要,只要提供了所需的方法即可。
def __len__(self):
return len(self.__com)
def __getitem__(self, item):
return self.__com[item]
只需在Vector类中实现这两个方法就可以啦!非常滴简单o
现在 我们处理的向量可能有大量分量。不过,若能通过单个字母访问前几个分量的话会比较方 便。比如,用 x、y 和 z 代替 v[0]、v[1] 和 v[2]。属性查找失败后,解释器会调用 getattr 方法。简单来说,对 my_obj.x 表达式,Python 会检查 my_obj 实例有没有名为 x 的属性;如果没有,到类(my_obj.class)中查找;如果 还没有,顺着继承树继续查找。4 如果依旧找不到,调用 my_obj 所属类中定义的 getattr 方法,传入 self 和属性名称的字符串形式(如 ‘x’)。
def __getattr__(self, name):
cls = type(self)
if len(name)==1:
pos = cls.short_cutname.find(name)
if 0<=pos<len(cls.short_cutname):
return self.__com[pos]
msg = '{.__name__!r} object has no attribute {!r}'
raise AttributeError(msg.format(cls, name))
通过vec.x查询向量指定位置元素的方式实现了,很简单。但是这样做是存在一定问题的。可以看到我们虽然可以正常获取值,但是如果通过v.x还能进行修改,且修改后x被绑定为v的实例属性了,如果再想通过v.x的方式去获取向量指定位置的值就获取不到了,因为会先查询这个实例有没有x这个属性了,现在有了,就不会再调用__getattr__方法了。所以我们现在要做的就是通过v.x方式不能进行实例属性的绑定。__setattr__可以实现我们这一需求
>>> v = Vector(range(5))
>>> v
Vector([0.0, 1.0, 2.0, 3.0, 4.0])
>>> v.x # ➊
0.0
>>> v.x = 10 # ➋
>>> v.x # ➌
10
>>> v
Vector([0.0, 1.0, 2.0, 3.0, 4.0]) # ➍
def __hash__(self):
hashed = map(hash,self.__com)
return functools.reduce(operator.xor,hashed,0)
实现__format__,指定我们想要的格式。
这章1-6部分主要就是讨论一些术语,还有告诉我们千万不要自己去定义抽象基类,否则可能会出现各种各样的问题。鸭子类型:我们只关心有没有这个协议,不关心数据类型。举个例子说明:添加可变序列协议中的 setitem 方法之后,立即就能使用标 准库中的 random.shuffle 函数。了解现有的协议能让我们充分利用 Python 丰富的标准库。
import abc
class Tombola(abc.ABC): # <1>
@abc.abstractmethod
def load(self, iterable): # <2>
"""Add items from an iterable."""
@abc.abstractmethod
def pick(self): # <3>
"""Remove item at random, returning it.
This method should raise `LookupError` when the instance is empty.
"""
def loaded(self): # <4>
"""Return `True` if there's at least 1 item, `False` otherwise."""
return bool(self.inspect()) # <5>
def inspect(self):
"""Return a sorted tuple with the items currently inside."""
items = []
while True: # <6>
try:
items.append(self.pick())
except LookupError:
break
self.load(items) # <7>
return tuple(sorted(items))
被装饰器@abc.abstractmethod所修饰的方法是抽象方法,如果想继承这个抽象基类,必须实现这两个抽象方法。而另外两个具体方法法只能依赖抽象基类定义的接口(即只能使用抽象基类中的其他具 体方法、抽象方法或特性)。inspect() 方法实现的方式有些笨拙,不过却表明,有了 .pick() 和 .load(…) 方法,若想查看 Tombola 中的内容,可以先把所有元素挑出,然后再放回去。这个示例的目 的是强调抽象基类可以提供具体方法,只要依赖接口中的其他方法就行。Tombola 的具体子类 知晓内部数据结构,可以覆盖 .inspect() 方法,使用更聪明的方式实现,但这不是强制要求。
白鹅类型的一个基本特性(也是值得用水禽来命名的原因):即便不继承,也有办法把一 个类注册为抽象基类的虚拟子类。这样做时,我们保证注册的类忠实地实现了抽象基类定 义的接口,而 Python 会相信我们,从而不做检查。如果我们说谎了,那么常规的运行时异 常会把我们捕获。 注册虚拟子类的方式是在抽象基类上调用 register 方法。这么做之后,注册的类会变成抽 象基类的虚拟子类,而且 issubclass 和 isinstance 等函数都能识别,但是注册的类不会 从抽象基类中继承任何方法或属性。
from random import randrange
from tombola import Tombola
@Tombola.register # <1>
class TomboList(list): # <2>
def pick(self):
if self: # <3>
position = randrange(len(self))
return self.pop(position) # <4>
else:
raise LookupError('pop from empty TomboList')
load = list.extend # <5>
def loaded(self):
return bool(self) # <6>
def inspect(self):
return tuple(sorted(self))
使用register方法作为装饰器完成虚拟子类的注册。
至于内置类型的子类覆盖的方法会不会隐式调用,CPython 没有制定官方规则。 基本上,内置类型的方法不会调用子类覆盖的方法。例如,dict 的子类覆盖的 getitem() 方法不会被内置类型的 get() 方法调用。就是说如果我们继承内置类型,即使我们重写了其中的一些方法,对于没有重写的方法如果要调用这些方法,它还是不会调用我们重写的方法,而是之前的内置类型的方法。
直接子类化内置类型(如 dict、list 或 str)容易出错,因为内置类型的方 法通常会忽略用户覆盖的方法。不要子类化内置类型,用户自己定义的类应 该继承 collections 模块(http://docs.python.org/3/library/collections.html)中 的类,例如 UserDict、UserList 和 UserString,这些类做了特殊设计,因此 易于扩展。
class A:
def ping(self):
print('ping:', self)
class B(A):
def pong(self):
print('pong:', self)
class C(A):
def pong(self):
print('PONG:', self)
class D(B, C):
def ping(self):
super().ping()
print('post-ping:', self)
def pingpong(self):
self.ping()
super().ping()
self.pong()
super().pong()
C.pong(self)
d = D()
print(d.ping()) #ping: <__main__.D object at 0x0000021E3FC7A760>
#post-ping: <__main__.D object at 0x0000021E3FC7A760>
print(d.pong()) #pong: <__main__.D object at 0x0000021E3FC7A760>
print(C.pong(d)) #PONG: <__main__.D object at 0x0000021E3FC7A760>
print(D.__mro__) #(, , , , )
当我们调用pong方法时,发现它调用的是B的,因为我们是先继承B的(class D(B, C):),如果我们这样写:class D(C, B):,它就会调用C的。使用__mro__方法可以查看一个类的继承关系。若想把方法调用委托给超类,推荐的方式是使用内置的 super() 函数。使用 super() 最安全,也不易过时。调用框架或不受自己控制的类层次结构中的方 法时,尤其适合使用 super()。使用 super() 调用方法时,会遵守方法解析顺序。
def __neg__(self):
return Vector(-x for x in self)
def __pos__(self):
return Vector(x for x in self)
上面一个是负号,一个是正号。
def __add__(self, other):
pairs = itertools.zip_longest(self,other,fillvalue=0)
return Vector(a+b for a,b in pairs)
只有一个正向加法,可以顺利执行vector+[1],但是不能够执行[1]+vector,所以我们要添加一个反向加法的魔法方法。
def __radd__(self, other):
return self + other
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WPwjWfb6-1675785176821)(C:\Users\sz\AppData\Roaming\Typora\typora-user-images\image-20220810113431256.png)]
迭代是数据处理的基石。扫描内存中放不下的数据集时,我们要找到一种惰性获取数据 项的方式,即按需一次获取一个数据项。这就是迭代器模式(Iterator pattern)。
import re
import reprlib
RE_WORD = re.compile(r'\w+')
class Sentence:
def __init__(self, text):
self.text = text
self.words = RE_WORD.findall(text) # <1> 返回一个字符串列表
def __getitem__(self, index):
return self.words[index] # <2>
def __len__(self): # <3>
return len(self.words)
def __repr__(self):
return 'Sentence(%s)' % reprlib.repr(self.text) # <4>
a = Sentence("abcd,a,a")
print(len(a)) #3
print(a) #Sentence('abcd,a,a')
内置的 iter 函数有以下作用。 (1) 检查对象是否实现了 iter 方法,如果实现了就调用它,获取一个迭代器。 (2) 如果没有实现 iter 方法,但是实现了 getitem 方法,Python 会创建一个迭代 器,尝试按顺序(从索引 0 开始)获取元素。 (3) 如果尝试失败,Python 抛出 TypeError 异常,通常会提示“C object is not iterable”(C 对象不可迭代),其中 C 是目标对象所属的类
我们要明确可迭代的对象和迭代器之间的关系:Python 从可迭代的对象中获取迭代器。可迭代的对象 使用 iter 内置函数可以获取迭代器的对象。如果对象实现了能返回迭代器的 iter 方法,那么对象就是可迭代的。序列都可以迭代;实现了 getitem 方法,而且其参 数是从零开始的索引,这种对象也可以迭代。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-L7eIkp9g-1675785176822)(C:\Users\sz\AppData\Roaming\Typora\typora-user-images\image-20220811114905536.png)]
Iterator 抽象基类实现 iter 方法的方式是返回实例本身(return self)。这样,在需 要可迭代对象的地方可以使用迭代器。在使用时,先使用iter()方法先返回一个对象的迭代器,然后再使用这个迭代器中的next()获取下一个元素。
迭代器 迭代器是这样的对象:实现了无参数的 next 方法,返回序列中的下一个元素;如 果没有元素了,那么抛出 StopIteration 异常。Python 中的迭代器还实现了 iter 方 法,因此迭代器也可以迭代。
import re
import reprlib
RE_WORD = re.compile(r'\w+')
class Sentence:
def __init__(self, text):
self.text = text
self.words = RE_WORD.findall(text)
def __repr__(self):
return 'Sentence(%s)' % reprlib.repr(self.text)
def __iter__(self): # <1>
return SentenceIterator(self.words) # <2>
class SentenceIterator:
def __init__(self, words):
self.words = words # <3>
self.index = 0 # <4>
def __next__(self):
try:
word = self.words[self.index] # <5>
except IndexError:
raise StopIteration() # <6>
self.index += 1 # <7>
return word # <8>
def __iter__(self): # <9>
return self
a = Sentence("abc def ghi")
print(a) #Sentence('abc def ghi')
iterator = iter(a)
print(iterator.__next__()) #abc
iterators = iter(iterator)
print(iterators) #<__main__.SentenceIterator object at 0x00000253F1EC3A30>
print(next(iterators)) #def
没必要在 SentenceIterator 类中实现 iter 方法,不过这 么做是对的,因为迭代器应该实现 next 和 iter 两个方法,而且这么做能让迭代器 通过 issubclass(SentenceInterator, abc.Iterator) 测试。
构建可迭代的对象和迭代器时经常会出现错误,原因是混淆了二者。要知道,可迭代的对 象有个 iter 方法,每次都实例化一个新的迭代器;而迭代器要实现 next 方法, 返回单个元素,此外还要实现 iter 方法,返回迭代器本身。除了 iter 方法之外,你可能还想在 Sentence 类中实现 next 方法,让 Sentence 实 例既是可迭代的对象,也是自身的迭代器。可是,这种想法非常糟糕。根据有大量 Python 代码审查经验的 Alex Martelli 所说,这也是常见的反模式。可迭代的对象一定不能是自身的迭代器。也就是说,可迭代的对象必须实现 iter 方法,但不能实现 next 方法。 另一方面,迭代器应该一直可以迭代。迭代器的 iter 方法应该返回自身。
import re
import reprlib
RE_WORD = re.compile(r'\w+')
class Sentence:
def __init__(self, text):
self.text = text
self.words = RE_WORD.findall(text)
def __repr__(self):
return 'Sentence(%s)' % reprlib.repr(self.text)
def __iter__(self):
for word in self.words: # <1>
yield word # <2>
return # <3>
Sentence 类中,iter 方法调用 SentenceIterator 类的构造方法 创建一个迭代器并将其返回。而在示例 14-5 中,迭代器其实是生成器对象,每次调用 iter 方法都会自动创建,因为这里的 iter 方法是生成器函数。
只要 Python 函数的定义体中有 yield 关键字,该函数就是生成器函数。调用生成器函数 时,会返回一个生成器对象。也就是说,生成器函数是生成器工厂。
import re
import reprlib
RE_WORD = re.compile(r'\w+')
class Sentence:
def __init__(self, text):
self.text = text # <1>
def __repr__(self):
return 'Sentence(%s)' % reprlib.repr(self.text)
def __iter__(self):
for match in RE_WORD.finditer(self.text): # <2>
yield match.group() # <3>
re.finditer 函数是 re.findall 函数的惰性版本,返回的不是列表,而是一个生成器,按 需生成 re.MatchObject 实例。
生成器表达式可以理解为列表推导的惰性版本:不会迫切地构建列表,而是返回一个生成 器,按需惰性生成元素。也就是说,如果列表推导是制造列表的工厂,那么生成器表达式 就是制造生成器的工厂。
def chain(*iterator):
for i in iterator:
yield from i
print(chain("abc",[1,2,3])) #
print(list(chain("abc",[1,2,3]))) #['a', 'b', 'c', 1, 2, 3]
a = [1,2,3,0]
print(all(a)) #False
print(any(a))#True
print(max(a))#3
print(min(a))#0
print(sum(a))#6
import functools
print(functools.reduce(lambda x1,x2:x1+x2,a))#6
可是,iter 函数还有一个鲜为人知的用法:传入两个参数,使用常规的函数或任何可调 用的对象创建迭代器。这样使用时,第一个参数必须是可调用的对象,用于不断调用(没 有参数),产出各个值;第二个值是哨符,这是个标记值,当可调用的对象返回这个值时, 触发迭代器抛出 StopIteration 异常,而不产出哨符。
内置函数 iter 的文档(https://docs.python.org/3/library/functions.html#iter)中有个实用的例 子。这段代码逐行读取文件,直到遇到空行或者到达文件末尾为止
with open('mydata.txt') as fp:
for line in iter(fp.readline, '\n'):
process_line(line)
注意:这里的else是不是用在if语句中的else哦。
else 子句的行为如下。 for 仅当 for 循环运行完毕时(即 for 循环没有被 break 语句中止)才运行 else 块。 while 仅当 while 循环因为条件为假值而退出时(即 while 循环没有被 break 语句中止)才运 行 else 块。 try 仅当 try 块中没有异常抛出时才运行 else 块。官方文档(https://docs.python.org/3/ reference/compound_stmts.html)还指出:“else 子句抛出的异常不会由前面的 except 子 句处理。” 在所有情况下,如果异常或者 return、break 或 continue 语句导致控制权跳到了复合语句 的主块之外,else 子句也会被跳过。