《流畅的Python》 读书笔记 第二章数据结构(2) 231011
2.5 對序列使用+和*
通常 + 號兩側的序列由相同類型的數據所構成,在拼接的過程中,兩個被操作的序列都不會被修改,Python 會新建一個包含同樣類型數據的序列來作為拼接的結果
+和*都遵循這個規律,不修改原有的操作對象,而是構建一個全新的序列
l1 = [1,2,3]
l2 = [4,5,6]
print(id(l1))
print(id(l2))
l3 = l1+l2
print(id(l3)) # 變了
l4 = l1 * 3
print(id(l4)) # 變了
print(id(l1)) # 沒變
print(id(l2)) # 沒變
2.5.1 建立由列表組成的列表
如果在 a * n 這個語句中,序列 a 里的元素是對其他可變對象的引用的話,你就需要格外注意了
你想用my_list = [[]] * 3 來初始化一個由列表組成的列表,但是你得到的列表里包含的 3 個元素其實是 3 個引用,而且這 3 個引用指向的都是同一個列表
這一段在Python官方的doc中就有描述
參考: https://docs.python.org/zh-cn/3.9/library/stdtypes.html
board = [['_'] * 3 for i in range(3)]
print(board)
board[1][2] = 'X'
print(board)
other_board = [['_'] * 3] * 3
print(other_board)
other_board[1][2] = 'X'
print(other_board)
看完上面的代碼,你知道結果是什么嗎?
你要做啥,完全取決于你
# 第一個print
[['_', '_', '_'], ['_', '_', 'X'], ['_', '_', '_']]
# 第二個print
[['_', '_', 'X'], ['_', '_', 'X'], ['_', '_', 'X']]
2.6 序列的增量賦值
+= 背后的特殊方法是
__iadd__(用于“就地加法”)。但是如果一個類沒有實現這個方法的話,Python 會退一步調用__add__
看下面的示例代碼就能很好的說明這段
class A:
def __init__(self,age):
self.age = age
def __iadd__(self, other):
print('calling iadd')
self.age+=other.age
return self
class B:
def __init__(self,age):
self.age = age
def __add__(self, other):
print('calling add')
self.age+=other.age
return self
a1 = A(1)
a2 = A(2)
a1+=a2 # calling iadd
print(a1.age) # 3
b1 = B(1)
b2 = B(2)
b1+=b2 # calling add
print(b1.age) # 3
a += b
對 可 變 序 列( 例 如 list、bytearray 和 array.array)來說,a 會就地改動,就像調用了 a.extend(b) 一樣
但是如果 a 沒有實現
__iadd__的話,a += b 這個表達式的效果就變得跟 a = a + b 一樣了:首先計算 a + b,得到一個新的對象,然后賦值給 a。也就是說,在這個表達式中,變量名會不會被關聯到新的對象,完全取決于這個類型有沒有實現__iadd__這個方法總體來講,可變序列一般都實現了
__iadd__方法,因此 += 是就地加法。而不可變序列根本就不支持這個操作,對這個方法的實現也就無從談起
我覺得說的有點啰嗦了,從"也就是說,在這個..."中的"這個"是有點歧義的。可以概括為
可變序列調用a+=b,就地加法,a不變
不可變序列調用a+=b,就會產生一個新的對象a(來自a+b的賦值)
看下面的例子,ae是不可變的, 因此做了+=后id都變了,而c是可變的,id沒有變化
a = 1
b = 2
print(id(a))
a += b
print(id(a))
c = [1]
print(id(c))
d = [2]
c +=d
print(id(c))
e = 'e'
print(id(e))
f = 'f'
e +=f
print(id(e))
+= 的概念也適用于 *=,不同的是,后者相對應的是
__imul__
我在https://zhuanlan.zhihu.com/p/656429071 中對此參考官方文檔詳細的描述了
| 方法 | 說明 |
|---|---|
__iadd__(self, other) |
+= |
__isub__(self, other) |
-= |
__imul__(self, other) |
*= |
__imatmul__(self, other) |
@= |
__itruediv__(self, other) |
/= |
__ifloordiv__(self, other) |
//= |
__imod__(self, other) |
%= |
__ipow__(self, other[,modulo]) |
**= |
__ilshift__(self, other) |
<<= |
__irshift__(self, other) |
>>= |
__iand__(self, other) |
&= |
__ixor__(self, other) |
^= |
__ior__(self, other) |
|= |
很重要的一句話
對不可變序列進行重復拼接操作的話,效率會很低,因為每次都有一個新對象,而解釋器需要把原來對象中的元素先復制到新的對象里,然后再追加新的元素
2.6.1 一個關于+=的謎題
t = (1, 2, [30, 40])
t[2] += [50, 60]
到底會發生下面 4 種情況中的哪一種?
a. t 變成 (1, 2, [30, 40, 50, 60])。
b. 因為 tuple 不支持對它的元素賦值,所以會拋出 TypeError 異常。
c. 以上兩個都不是。
d. a 和 b 都是對的。
這種問題一般來講,不用想,肯定是a、b中的一個,c可能性不大,d怎么都不可能(又異常又正常)
可答案就是d
稍作改動你能看到異常也能看到改變后的t
t = (1, 2, [30, 40])
try:
t[2] += [50, 60]
except TypeError as e:
print(e)
print(t)
書中還對此展開了字節碼
>>> s = [1,2,[3,4]]
>>> s = (1,2,[30,40])
>>> b = [50,60]
>>> import dis
>>> dis.dis('s[2]+=b') # 此處 原文是s[a]
1 0 LOAD_NAME 0 (s)
2 LOAD_CONST 0 (2)
4 DUP_TOP_TWO
6 BINARY_SUBSCR①
8 LOAD_NAME 1 (b)
10 INPLACE_ADD②
12 ROT_THREE
14 STORE_SUBSCR③
16 LOAD_CONST 1 (None)
18 RETURN_VALUE
上面是我的結果,跟書中的不太一樣(這種我就不懂了,懂的大神可以說說)
對上面的①②③作者闡述了
? 將 s[a] 的值存入 TOS(Top Of Stack,棧的頂端)。
? 計算 TOS += b。這一步能夠完成,是因為 TOS 指向的是一個可變對象(也就是示例 2-15
里的列表)。
? s[a] = TOS 賦值。這一步失敗,是因為 s 是不可變的元組(示例 2-15 中的元組 t)。
至此我得到了 3 個教訓。
? 不要把可變對象放在元組里面。
? 增量賦值不是一個原子操作。我們剛才也看到了,它雖然拋出了異常,但還是完成了操作。
? 查看 Python 的字節碼并不難,而且它對我們了解代碼背后的運行機制很有幫助
難倒是不難,就是看不懂有點愁,背后都是C,全忘了~
有讀者提出,如果寫成 t[2].extend([50, 60]) 就能避免這個異常。確實是這樣,但這個例子是為了
展示這種奇怪的現象而專門寫的
2.7 list.sort方法和內置函數sorted
Python 的一個慣例:如果一個函數或者方法對對象進行的是就地改動,那它就應該返回None,好讓調用者知道傳入的參數發生了變動,而且并未產生新的對象
list.sort就是這么一個例子,還要random.shuffle等
以前有學員問為何返回是None,我總是回復,這個在于對方的設計,現在總算有了一個新的說辭,聽著還蠻高大上的。
用返回 None 來表示就地改動這個慣例有個弊端,那就是調用者無法將其串聯起來。而返回一個新對象的方法(比如說 str 里的所有方法)則正好相反,它們可以串聯起來調用,從而形成連貫接口(fluent interface)。詳情參見維基百科中有關連貫接口的討論(https://en.wikipedia.org/wiki/Fluent_
interface)
我習慣稱之為鏈式調用,就像selenium庫中ActionChains類的API的調用
list.sort 方法還是 sorted 函數,都有兩個可選的關鍵字參數
| 參數 | 說明 |
|---|---|
| reverse | 如果被設定為 True,被排序的序列里的元素會以降序輸出(也就是說把最大值當作最小值來排序)。這個參數的默認值是 False。 |
| key | 一個只有一個參數的函數,這個函數會被用在序列里的每一個元素上,所產生的結果將是排序算法依賴的對比關鍵字。比如說,在對一些字符串排序時,可以用 key=str.lower 來實現忽略大小寫的排序,或者是用 key=len 進行基于字符串長度的排序。這個參數的默認值是恒等函數(identity function),也就是默認用元素自己的值來排序 |
一個只有一個參數的函數 你會想到lambda吧~
作者給的例子
>>> fruits = ['grape', 'raspberry', 'apple', 'banana']
>>> sorted(fruits)
['apple', 'banana', 'grape', 'raspberry'] ?
>>> fruits
['grape', 'raspberry', 'apple', 'banana'] ?
>>> sorted(fruits, reverse=True)
['raspberry', 'grape', 'banana', 'apple'] ?
>>> sorted(fruits, key=len)
['grape', 'apple', 'banana', 'raspberry'] ?
>>> sorted(fruits, key=len, reverse=True)
['raspberry', 'banana', 'grape', 'apple'] ?
>>> fruits
['grape', 'raspberry', 'apple', 'banana'] ?
>>> fruits.sort() ?
>>> fruits
['apple', 'banana', 'grape', 'raspberry'] ?
我在https://zhuanlan.zhihu.com/p/658316452中也詳細闡述了一些簡單的用法
可選參數 key 還可以在內置函數 min() 和 max() 中起作用。另外,還有些標準庫里的函數也接受這個參數,像 itertools.groupby() 和 heapq.nlargest() 等
headq示例代碼
import heapq
# 創建一個列表
numbers = [1,-4,3,10,-15,9]
# 使用 heapq.nlargest() 并設置 key 參數為 square(平方)
largest_three = heapq.nlargest(3, numbers, key=lambda x: x ** 2)
print(largest_three) # 輸出:[-15, 10, 9]
groupby示例代碼
from itertools import groupby
# 示例數據,假設這是一組學生的成績記錄,每個學生有姓名和數學成績
students = [('Alice', 90), ('Bob', 80), ('Charlie', 85), ('David', 90), ('Eva', 85)]
# 使用 lambda 函數將學生按照數學成績進行分組
grouped_students = groupby(sorted(students, key=lambda x: x[1]), key=lambda x: x[1])
# 遍歷每個組并打印組名(數學成績)和組中的學生列表
for name, group in grouped_students:
print(f"Group name: {name}")
for student in group:
print(f" - {student[0]}")
示例輸出
Group name: 80
- Bob
Group name: 85
- Charlie
- Eva
Group name: 90
- Alice
- David
2.8 用bisect來管理已排序的序列
bisect 模塊包含兩個主要函數,bisect 和 insort,兩個函數都利用二分查找算法來在有序序列中查找或插入元素
2.8.1 用bisect來搜索
Bisection 二分法
bisect(haystack, needle)
haystack 干草垛是一個已經排好序的序列
needle是待插入的針
插入完畢后的newstack依然是有序的
bisect(haystack, needle) 查找位置 index
haystack.insert(index,needle) 來插入新值
insort 可以一步到位,并且速度更快一些
bisect默認情況下遇到等值,插入到右側(bisect = bisect_right),bisect_left可以插入到左側
bisect_right(a, x, lo=0, hi=None) 完整的有4個參數,lo默認是0即第一個數,hi默認是len(a)
作者給的代碼,稍作更改
import bisect
import sys
HAYSTACK = [1, 4, 5, 6, 8, 12, 15, 20, 21, 23, 23, 26, 29, 30]
NEEDLES = [0, 1, 2, 5, 8, 10, 22, 23, 29, 30, 31]
ROW_FMT = 'Needle: {0:2d} ,index: {1:2d}==> {2}{0:<2d}'
def demo(bisect_fn):
for needle in reversed(NEEDLES):
position = bisect_fn(HAYSTACK, needle) #?
offset = position * ' |' #?
print(ROW_FMT.format(needle, position, offset)) #?
if __name__ == '__main__':
if sys.argv[-1] == 'left': #?
bisect_fn = bisect.bisect_left
else:
bisect_fn = bisect.bisect
print('DEMO:', bisect_fn.__name__) #?
print('haystack ->')
print(' ',' '.join('%2d' % n for n in HAYSTACK))
demo(bisect_fn)
作者的解釋
? 用特定的 bisect 函數來計算元素應該出現的位置。
? 利用該位置來算出需要幾個分隔符號。
? 把元素和其應該出現的位置打印出來。
? 根據命令上最后一個參數來選用 bisect 函數。
? 把選定的函數在抬頭打印出來
你可能看到的輸出是這樣的
DEMO: bisect_right
haystack ->
1 4 5 6 8 12 15 20 21 23 23 26 29 30
Needle: 31 ,index: 14==> | | | | | | | | | | | | | |31
Needle: 30 ,index: 14==> | | | | | | | | | | | | | |30
Needle: 29 ,index: 13==> | | | | | | | | | | | | |29
Needle: 23 ,index: 11==> | | | | | | | | | | |23
Needle: 22 ,index: 9==> | | | | | | | | |22
Needle: 10 ,index: 5==> | | | | |10
Needle: 8 ,index: 5==> | | | | |8
Needle: 5 ,index: 3==> | | |5
Needle: 2 ,index: 1==> |2
Needle: 1 ,index: 1==> |1
Needle: 0 ,index: 0==> 0
你可以執行python demo.py left 得到不一樣的結果,這個代碼接受參數,只不過它處理的 是最后一個參數if sys.argv[-1] == 'left':
bisect一個經典的案例,我在https://www.liujiangblog.com/course/python/57也看到過,第一次見的時候覺得還蠻厲害的,后來發現在官網就有了,https://docs.python.org/3/library/bisect.html
>>> def grade(score, breakpoints=[60, 70, 80, 90], grades='FDCBA'):
... i = bisect.bisect(breakpoints, score)
... return grades[i]
...
>>> [grade(score) for score in [33, 99, 77, 70, 89, 90, 100]]
['F', 'A', 'C', 'C', 'B', 'A', 'A']
通過計算score在breakpoints中的位置,得到對應grades(必須要與breakpoints對應)的等級
雖然你可以用字典來做,但這個方法也是非常不錯的案例
拓展閱讀
Raymond Hettinger 寫了一個排序集合模塊(http://code.activestate.com/recipes/577197-sortedcollection
2.8.2 用bisect.insort插入新元素
insort(seq, item) 把變量 item 插入到序列 seq 中,并能保持 seq 的升序順序
import bisect
import random
SIZE=7
random.seed(1729)
my_list = []
for i in range(SIZE):
new_item = random.randrange(SIZE*2)
bisect.insort(my_list, new_item)
print('%2d ->' % new_item, my_list)
輸出
10 -> [10]
0 -> [0, 10]
6 -> [0, 6, 10]
8 -> [0, 6, 8, 10]
7 -> [0, 6, 7, 8, 10]
2 -> [0, 2, 6, 7, 8, 10]
10 -> [0, 2, 6, 7, 8, 10, 10]
insort的源碼很簡單,底層就是bisect_right或bisect_left得到索引后用a.insert來插入
def insort_right(a, x, lo=0, hi=None):
lo = bisect_right(a, x, lo, hi)
a.insert(lo, x)
def insort_left(a, x, lo=0, hi=None):
lo = bisect_left(a, x, lo, hi)
a.insert(lo, x)
insort = insort_right
insort 跟 bisect 一樣,有 lo 和 hi 兩個可選參數用來控制查找的范圍
目前所提到的內容都不僅僅是對列表或者元組有效,還可以應用于幾乎所有的序列類型上
如果是所有的序列類型,那從源碼看,你得支持.insert才可以,元組其實就不支持了,你要實現必須轉換下
2.9 當列表不是首選時
要存放1000 萬個浮點數的話,數組(array)的效率要高得多,因為數組在背后存的并不是 float
對象,而是數字的機器翻譯,也就是字節表述
如果需要頻繁對序列做先進先出的操作,deque(雙端隊列)的速度應該會更快。
如果在你的代碼里,包含操作(比如檢查一個元素是否出現在一個集合中)的頻率很高,用 set(集合)會更合適
用什么數據結構是應該利用數據結構的特點再結合你的應用場景而定的
下面這幾個我工作中反正是幾乎沒用過
2.9.1 數組 array
我也是一直把Python的list當做array來處理的,其實不然
如果我們需要一個只包含數字的列表,那么 array.array 比 list 更高效
數組支持所有跟可變序列有關的操作,包括 .pop、.insert 和 .extend。另外,數組還提供從文件讀取和存
入文件的更快的方法,如 .frombytes 和 .tofile。
在Python的array.array函數中,第一個參數是一個表示類型的代碼,它決定了數組中元素的數據類型。以下是可以使用的類型代碼:
'b':布爾型(Boolean),取值為True(1)或False(0)。'B':無符號布爾型(Unsigned Byte),取值為0或1。'u':Unicode字符(Unicode Character)。'i':有符號整數(Signed Integer)。'I':無符號整數(Unsigned Integer)。'l':長整數(Long Integer)。'L':無符號長整數(Unsigned Long Integer)。'f':浮點數(Floating Point)。'd':十進制浮點數(Decimal Floating Point)。'g':十進制浮點數或定點數(General Purpose)。
這些類型代碼的長度也代表了數組中元素的數據類型的長度,例如'i'代表的是有符號的整數類型,長度為當前平臺下int的對齊長度,而'B'則代表的是無符號的8位整數類型。
請注意,創建的數組只能包含一種數據類型的元素。
array('b') 創建出的數組就只能存放一個字節大小的整數,范圍從 -128 到127,這樣在序列很大的時候,我們能節省很多空間。而且 Python 不會允許你在數組里存放除指定類型之外的數據
書中給出的代碼(稍作更改)
from array import array # 1
from random import random
floats1 = array('d', (random() for i in range(10**7))) # 2
print(floats1[-1]) # 3
with open('floats.bin', 'wb') as fp1: # 4
floats1.tofile(fp1)
floats2 = array('d') # 5
with open('floats.bin', 'rb') as fp2:
floats2.fromfile(fp2, 10**7) # 6
print(floats2[-1]) # 7
print(floats1 == floats2) # 8
? 引入 array 類型。
? 利用一個可迭代對象來建立雙精度浮點數組(類型碼是 'd'),這里我們用的可迭代對象是一個生成器表達式。
? 查看數組的最后一個元素。
? 把數組存入一個二進制文件里。
? 新建一個雙精度浮點空數組。
? 把 1000 萬個浮點數從二進制文件里讀取出來。
? 查看新數組的最后一個元素。
? 檢查兩個數組的內容是不是完全一樣。
劃下重點
- 用 array.fromfile 從一個二進制文件里讀出 1000 萬個雙精度浮點數只需要 0.1 秒,這比從文本文件里讀取的速度要快60 倍。
- 使用 array.tofile 寫入到二進制文件,比以每行一個浮點數的方式把所有數字寫入到文本文件要快 7倍
- 1000 萬個這樣的數在二進制文件里只占用 80 000 000 個字節(每個浮點數占用8 個字節,不需要任何額外空間),如果是文本文件的話,我們需要 181 515 739 個字節(20多倍)
另外一個快速序列化數字類型的方法是使用 pickle(https://docs.python.org/3/library/pickle.html)模塊。pickle.dump 處理浮點數組的速度幾乎跟 array.tofile 一樣快。不過前者可以處理幾乎所有的內置數字類型,包含復數、嵌套集合,甚至用戶自定義的類。前提是這些類沒有什么特別復雜的實現
| 操作 | 列表 | 數組 | 說明 |
|---|---|---|---|
s.__add__(s2) |
√ | √ | s + s2 ,拼接 |
s.__iadd__(s2) |
√ | √ | s += s2 ,就地拼接 |
| s.append(e) | √ | √ | 在尾部添加一個元素 |
| s.byteswap | √ | 翻轉數組內每個元素的字節序列,轉換字節序 | |
| s.clear() | √ | 刪除所有元素 | |
s.__contains__(e) |
√ | √ | s 是否含有 e |
| s.copy() | √ | 對列表淺復制 | |
s.__copy__() |
√ | 對 copy.copy 的支持 | |
| s.count(e) | √ | √ | s 中 e 出現的次數 |
s.__deepcopy__() |
√ | 對 copy.deepcopy 的支持 | |
s.__delitem__(p) |
√ | √ | 刪除位置 p 的元素 |
| s.extend(it) | √ | √ | 將可迭代對象 it 里的元素添加到尾部 |
| s.frombytes(b) | √ | 將壓縮成機器值的字節序列讀出來添加到尾部 | |
| s.fromfile(f, n) | √ | 將二進制文件 f 內含有機器值讀出來添加到尾部,最多添加 n 項 | |
| s.fromlist(l) | √ | 將列表里的元素添加到尾部,如果其中任何一個元素導致了TypeError 異常,那么所有的添加都會取消 | |
s.__getitem__(p) |
√ | √ | s[p],讀取位置 p 的元素 |
| s.index(e) | √ | √ | 找到 e 在序列中第一次出現的位置 |
| s.insert(p, e) | √ | √ | 在位于 p 的元素之前插入元素 e |
| s.itemsize | √ | 數組中每個元素的長度是幾個字節 | |
s.__iter__() |
√ | √ | 返回迭代器 |
s.__len__() |
√ | √ | len(s),序列的長度 |
s.__mul__(n) |
√ | √ | s * n,重復拼接 |
s.__imul__(n) |
√ | √ | s *= n ,就地重復拼接 |
s.__rmul__(n) |
√ | √ | n * s ,反向重復拼接 * |
| s.pop([p]) | √ | √ | 刪除位于 p 的值并返回這個值,p 的默認值是最后一個元素的位置 |
| s.remove(e) | √ | √ | 刪除序列里第一次出現的 e 元素 |
| s.reverse() | √ | √ | 就地調轉序列中元素的位置 |
s.__reversed__() |
√ | 返回一個從尾部開始掃描元素的迭代器 | |
s.__setitem__(p, e) |
√ | √ | s[p] = e,把位于 p 位置的元素替換成 e |
| s.sort([key], [revers]) | √ | 就地排序序列,可選參數有 key 和 reverse | |
| s.tobytes() | √ | 把所有元素的機器值用 bytes 對象的形式返回 | |
| s.tofile(f) | √ | 把所有元素以機器值的形式寫入一個文件 | |
| s.tolist() | √ | 把數組轉換成列表,列表里的元素類型是數字對象 | |
| s.typecode | √ | 返回只有一個字符的字符串,代表數組元素在 C 語言中的類型 |
從 Python 3.4 開始,數組(array)類型不再支持諸如 list.sort() 這種就地排序方法
要給數組排序的話,得用 sorted 函數新建一個數組:a = array.array(a.typecode, sorted(a))
2.9.2 內存視圖 memoryview
memoryview 是一個內置類,它能讓用戶在不復制內容的情況下操作同一個數組的不同切片
在數據結構之間共享內存。其中數據結構可以是任何形式,比如 PIL 圖片、SQLite數據庫和 NumPy 的數組,等等。這個功能在處理大型數據集合的時候非常重要
示例 2-21 : 通過改變數組中的一個字節來更新數組里某個元素的值
>>> numbers = array.array('h', [-2, -1, 0, 1, 2])
>>> memv = memoryview(numbers) ?
>>> len(memv)
5
>>> memv[0] ?
-2
>>> memv_oct = memv.cast('B') ?
>>> memv_oct.tolist() ?
[254, 255, 255, 255, 0, 0, 1, 0, 2, 0]
>>> memv_oct[5] = 4 ?
>>> numbers
array('h', [-2, -1, 1024, 1, 2]) ?
? 利用含有 5 個短整型有符號整數的數組(類型碼是 'h')創建一個 memoryview。
? memv 里的 5 個元素跟數組里的沒有區別。
? 創建一個 memv_oct,這一次是把 memv 里的內容轉換成 'B' 類型,也就是無符號字符。
? 以列表的形式查看 memv_oct 的內容。
? 把位于位置 5 的字節賦值成 4。
? 因為我們把占 2 個字節的整數的高位字節改成了 4,所以這個有符號整數的值就變成
了 1024。
2.9.3 NumPy和SciPy
這部分多少有點跑偏,簡單瞄一眼就行了,我基本就是CTRL+C/V
憑借著 NumPy 和 SciPy 提供的高階數組和矩陣操作,Python 成為科學計算應用的主流語言
NumPy 實現了多維同質數組(homogeneous array)和矩陣,這些數據結構不但能處理數字,還能存放其他由用戶定義的記錄
SciPy 是基于 NumPy 的另一個庫,它提供了很多跟科學計算有關的算法,專為線性代數、數值積分和統計學而設計
SciPy 把基于C 和 Fortran 的工業級數學計算功能用交互式且高度抽象的 Python 包裝起來
這些跟計算有關的部分都源自于 Netlib 庫(http://www.netlib.org)
示例 2-22:NumPy 二維數組的基本操
>>> import numpy ?
>>> a = numpy.arange(12) ?
>>> a
array([ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11])
>>> type(a)
<class 'numpy.ndarray'>
>>> a.shape ?
(12,)
>>> a.shape = 3, 4 ?
>>> a
array([[ 0, 1, 2, 3],
[ 4, 5, 6, 7],
[ 8, 9, 10, 11]])
>>> a[2] ?
array([ 8, 9, 10, 11])
>>> a[2, 1] ?
9
>>> a[:, 1] ?
array([1, 5, 9])
>>> a.transpose() ?
array([[ 0, 4, 8],
[ 1, 5, 9],
[ 2, 6, 10],
[ 3, 7, 11]])
? 安裝 NumPy 之后,導入它(NumPy 并不是 Python 標準庫的一部分)。
? 新建一個 0~11 的整數的 numpy.ndarray,然后把它打印出來。
? 看看數組的維度,它是一個一維的、有 12 個元素的數組。
? 把數組變成二維的,然后把它打印出來看看。
? 打印出第 2 行。
? 打印第 2 行第 1 列的元素
? 把第 1 列打印出來。
? 把行和列交換,就得到了一個新數組
>>> import numpy
>>> floats = numpy.loadtxt('floats-10M-lines.txt') ?
>>> floats[-3:] ?
array([ 3016362.69195522, 535281.10514262, 4566560.44373946])
>>> floats *= .5 ?
>>> floats[-3:]
array([ 1508181.34597761, 267640.55257131, 2283280.22186973])
>>> from time import perf_counter as pc ?
>>> t0 = pc(); floats /= 3; pc() - t0 ?
0.03690556302899495
>>> numpy.save('floats-10M', floats) ?
>>> floats2 = numpy.load('floats-10M.npy', 'r+') ?
>>> floats2 *= 6
>>> floats2[-3:] ?
memmap([3016362.69195522, 535281.10514262, 4566560.44373946])
? 從文本文件里讀取 1000 萬個浮點數。
? 利用序列切片來讀取其中的最后 3 個數。
? 把數組里的每個數都乘以 0.5,然后再看看最后 3 個數。
? 導入精度和性能都比較高的計時器(Python 3.3 及更新的版本中都有這個庫)。
? 把每個元素都除以 3,可以看到處理 1000 萬個浮點數所需的時間還不足 40 毫秒。
? 把數組存入后綴為 .npy 的二進制文件。
? 將上面的數據導入到另外一個數組里,這次 load 方法利用了一種叫作內存映射的機制,
它讓我們在內存不足的情況下仍然可以對數組做切片。
? 把數組里每個數乘以 6 之后,再檢視一下數組的最后 3 個數
2.9.4 雙向隊列和其他形式的隊列
利用 .append 和 .pop 方法,我們可以把列表當作棧或者隊列來用(比如,把 .append和 .pop(0) 合起來用,就能模擬隊列的“先進先出”的特點)
此處貼一下LeetCode 232 用棧實現隊列的某個題解,你能很好的理解上面這句話
class MyQueue:
def __init__(self):
self.A, self.B = [], []
def push(self, x: int) -> None:
self.A.append(x)
def pop(self) -> int:
peek = self.peek()
self.B.pop()
return peek
def peek(self) -> int:
if self.B: return self.B[-1]
if not self.A: return -1
# 將棧 A 的元素依次移動至棧 B
while self.A:
self.B.append(self.A.pop())
return self.B[-1]
def empty(self) -> bool:
return not self.A and not self.B
但是刪除列表的第一個元素(抑或是在第一個元素之前添加一個元素)之類的操作是很耗時的,因為這些操作會牽扯到移動列表里的所有元素。
常見的列表操作的復雜度
| 列表操作 | 時間復雜度 |
|---|---|
| append | O(1) |
| list[0] 或 list[-1] 或 list[0] = 1 | O(1) # 下標訪問或賦值 |
| pop | O(1) |
| pop(index) | O(n) |
| insert | O(n) |
| del list[0] | O(n) |
| reverse | O(n) |
| sort | O(n log n) |
| sorted(list) | O(n log n) |
| remove | O(n) |
| list[a:b] | O(n) # 切片 |
| len(list) | O(1) |
| count | O(n) |
| n in nums | O(n) |
collections.deque 類(雙向隊列)是一個線程安全、可以快速從兩端添加或者刪除元素的數據類型。而且如果想要有一種數據類型來存放“最近用到的幾個元素”,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[1]
8
>>> dq[-1]
6
>>> 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)
? maxlen 是一個可選參數,代表這個隊列可以容納的元素的數量,而且一旦設定,這個屬性就不能修改了。
? 隊列的旋轉操作接受一個參數 n,當 n > 0 時,隊列的最右邊的 n 個元素會被移動到隊列的左邊。當 n < 0 時,最左邊的 n 個元素會被移動到右邊。
? 當試圖對一個已滿(len(d) == d.maxlen)的隊列做頭部appendleft添加操作的時候,它尾部的元素會被刪除掉。注意在下一行里,元素 0 被刪除了。
? 在尾部添加 3 個元素的操作會擠掉 -1、1 和 2(就是前三個)。
? extendleft(iter) 方法會把迭代器里的元素逐個添加到雙向隊列的左邊,因此迭代器里的元素會逆序出現在隊列里。這非常有特點
雙向隊列實現了大部分列表所擁有的方法,也有一些額外的符合自身設計的方法,比如說
popleft 和 rotate。但是為了實現這些方法,雙向隊列也付出了一些代價,從隊列中間刪
除元素的操作會慢一些,因為它只對在頭尾的操作進行了優化。
append 和 popleft 都是原子操作,也就說是 deque 可以在多線程程序中安全地當作先進先
出的隊列使用,而使用者不需要擔心資源鎖的問題
| 操作 | 列表 | 雙向隊列 | 說明 |
|---|---|---|---|
s.__add__(s2) |
√ | s + s2 ,拼接 | |
s.__iadd__(s2) |
√ | √ | s += s2 ,就地拼接 |
| s.append(e) | √ | √ | 添加一個元素到最右側(到最后一個元素之后) |
| s.appendleft(e) | √ | 添加一個元素到最左側(到第一個元素之前) | |
| s.clear() | √ | √ | 刪除所有元素 |
s.__contains__(e) |
√ | s 是否含有 e | |
| s.copy() | √ | 對列表淺復制 | |
s.__copy__() |
√ | 對 copy.copy (淺復制)的支持 | |
| s.count(e) | √ | √ | s 中 e 出現的次數 |
s.__delitem__(p) |
√ | √ | 刪除位置 p 的元素 |
| s.extend(it) | √ | √ | 將可迭代對象 it 里的元素添加到尾部 |
| s.extendleft(i) | √ | 將可迭代對象 i 中的元素添加到頭部 | |
s.__getitem__(p) |
√ | √ | s[p],讀取位置 p 的元素 |
| s.index(e) | √ | 找到 e 在序列中第一次出現的位置 | |
| s.insert(p, e) | √ | 在位于 p 的元素之前插入元素 e | |
s.__iter__() |
√ | √ | 返回迭代器 |
s.__len__() |
√ | √ | len(s),序列的長度 |
s.__mul__(n) |
√ | s * n,重復拼接 | |
s.__imul__(n) |
√ | s *= n ,就地重復拼接 | |
s.__rmul__(n) |
√ | n * s ,反向重復拼接 * | |
| s.pop([p]) | √ | √ | 刪除位于 p 的值并返回這個值,p 的默認值是最后一個元素的位置 |
| s.popleft() | √ | 移除第一個元素并返回它的值 | |
| s.remove(e) | √ | √ | 刪除序列里第一次出現的 e 元素 |
| s.reverse() | √ | √ | 就地調轉序列中元素的位置 |
s.__reversed__() |
√ | √ | 返回一個從尾部開始掃描元素的迭代器 |
| s.rotate(n) | √ | 把 n 個元素從隊列的一端移到另一端 | |
s.__setitem__(p, e) |
√ | √ | s[p] = e,把位于 p 位置的元素替換成 e |
| s.sort([key], [revers]) | √ | 就地排序序列,可選參數有 key 和 reverse |
a_list.pop(p) 這個操作只能用于列表,雙向隊列的這個方法不接收參數
其他形式的隊列的實現
| 其他形式 | 說明 |
|---|---|
| queue | 提供了同步(線程安全)類 Queue、LifoQueue 和 PriorityQueue,不同的線程可以利用 這些數據類型來交換信息。這三個類的構造方法都有一個可選參數 maxsize,它接收正 整數作為輸入值,用來限定隊列的大小。但是在滿員的時候,這些類不會扔掉舊的元素 來騰出位置。相反,如果隊列滿了,它就會被鎖住,直到另外的線程移除了某個元素而 騰出了位置。這一特性讓這些類很適合用來控制活躍線程的數量 |
| multiprocessing | 這個包實現了自己的 Queue,它跟 queue.Queue 類似,是設計給進程間通信用的。同時 還有一個專門的 multiprocessing.JoinableQueue 類型,可以讓任務管理變得更方便 |
| asyncio | Python 3.4 新提供的包,里面有 Queue、LifoQueue、PriorityQueue 和 JoinableQueue, 這些類受到 queue 和 multiprocessing 模塊的影響,但是為異步編程里的任務管理提供 了專門的便利 |
| heapq | 跟上面三個模塊不同的是,heapq 沒有隊列類,而是提供了 heappush 和 heappop 方法, 讓用戶可以把可變序列當作堆隊列或者優先隊列來使用 |
2.10 本章小結
Python 序列類型最常見的分類就是可變和不可變序列
另外一種分類方式 扁平序列和容器序列
前者的體積更小、速度更快而且用起來更簡單,但是它只能保存一些原子性的數據,比如數字、字符和字節
后者更加靈活,如果用到可變對象,還要嵌套的數據結構,尤其要注意
列表推導和生成器表達式則提供了靈活構建和初始化序列的方式
元組它既可以用作無名稱的字段的記錄,又可以看作不可變的列表
前者的使用中,拆包是一個典型的做法,*句法更加便利
具名元組的實例也很節省空間,同時提供了方便地通過名字來獲取元組各個字段信息的方式
Python 里最受歡迎的一個語言特性就是序列切片
用戶自定義的序列類型也可以選擇支持 NumPy 中的多維切片和省略(...)
對切片賦值是一個修改可變序列的捷徑
增量賦值 += 和 *= 會區別對待可變和不可變序列
在遇到不可變序列時,這兩個操作會在背后生成新的序列。但如果被賦值的對象是可變的,那么這個序列會就地修改
序列的 sort 方法和內置的 sorted 函數,都接受一個函數作為可選參數來指定排序算法如何比較大小
這個參數名為key
如果在插入新元素的同時還想保持有序序列的順序,那么需要用到 bisect.insort。bisect.bisect 的作用則是快速查找
返回索引
2.11 延伸閱讀
| 素材 | URL | 相關信息 |
|---|---|---|
| Python 官 方 網 站的Sorting HOW TO | https://docs.python.org/3/howto/sorting.html | 講解了 sorted 和 list.sort 的高級用法 |
| PEP 3132 — Extended Iterable Unpacking | https://www.python.org/dev/peps/pep-3132/ | 使用 *extra 句法進行平行賦值的權威指南 |
| Missing *-unpacking generalizations | http://bugs.python.org/issue2292 | 關于如何更廣泛地使用可迭代對象拆包的討論和提議 |
| PEP 448—Additional Unpacking Generalizations | https://www.python.org/dev/peps/pep-0448/ | 上面討論的直接結果 |
| Less Copies in Python with the Buffer Protocol and memoryviews | http://eli.thegreenplace.net/2011/11/28/less-copies-in-python-with-the-buffer-protocol-and-memoryviews/ | |
| 利用 Python 進行數據分析 | Wes McKinney | |
| 8.3. collections — Container datatypes | https://docs.python.org/3/library/collections.html | 有一些關于雙向隊列和其他集合類型的使用技巧 |
| Why Numbering Should Start at Zero | http://www.cs.utexas.edu/users/EWD/transcriptions/EWD08xx/EWD831.html | |
ABC 里的 compounds 類型算得上是 Python 元組的鼻祖
容器”一詞來自“Data Model”文檔(https://docs.python.org/3/reference/datamodel.
html#objects-values-and-types)
有些對象里包含對其他對象的引用;這些對象稱為容器
扁平序列因為只能包含原子數據類型,比如整數、浮點數或字符,所以不能嵌套使用
這個名字是本書作者創造的!主要是為了跟容器序列做對比
列表是可以同時容納不同類型的元素的,但是實際上這樣做并沒有什么特別的好處
不推薦這么用
元組則恰恰相反,它經常用來存放不同類型的的元素
list.sort、sorted、max 和 min 函數的 key 參數是一個很棒的設計
用 key 參數能把事情變得簡單且高效。
說它更簡單,是因為只需要提供一個單參數函數來提取或者計算一個值作為比較大小的標準即可
說它更高效,是因為在每個元素上,key 函數只會被調用一次。
sorted 和 list.sort 背后的排序算法是 Timsort,它是一種自適應算法,會根據原始數據的順序特點交替使用插入排序和歸并排序,以達到最佳效率
https://en.wikipedia.org/wiki/Timsort
2009年起,Java 和 Android 也開始使用這個算法
Timsort 的創始人是 Tim Peters,也是“Python 之禪”(import this)的作者
總結
以上是生活随笔為你收集整理的《流畅的Python》 读书笔记 第二章数据结构(2) 231011的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 嵌入式BI的精解与探索
- 下一篇: P34_数据请求 - GET和POST请