Python學習筆記19:清單 III
其實這篇筆記标題應該是清單擴充,從清單開始,将涵蓋Python中的序列容器。
關于清單的基礎知識,可以看我的前兩篇文章:
- Python學習筆記1:清單。
- Python學習筆記17:清單II。
我們知道,Python的預設容器可以分為有序和無序兩大類,而有序容器中最常見的是清單和元組。
在這個概念之上,我們可以将所有有序的資料模型稱為序列。
以是否能容納複雜資料類型為标準,我們可以把序列分為兩大類,容器序列和扁平序列。
容器序列
容器序列,顧名思義,這類資料模型在保持順序的基礎上,可以容納複雜的資料類型。
這其中最通用和熟悉的就是清單了。
清單
清單的大部分用法都已經在前邊的筆記中介紹過了,這其中最有意思的用法是推導式和生成器。
- 推導式的内容可以看Python學習筆記15:推導式。
- 生成器的内容可以看Python學習筆記16:生成器。
對于推導式和生成器,除了基本用法,這裡還有一個特殊用法之前沒有涉及到。
生成笛卡爾積
除了用推導式和生成器生成一個一維清單,我們還可以生成笛卡爾積。
我有限的剩餘的那點高中數學知識告訴我,笛卡爾積就是兩個集合中的元素兩兩組合,求最終的所有可能結果。
我們來看如何用推導式生成笛卡爾積:
listA = ['a', 'b', 'c']
listB = [1, 2, 3]
result = [(a, b) for a in listA for b in listB]
print(result)
輸出
[(‘a’, 1), (‘a’, 2), (‘a’, 3), (‘b’, 1), (‘b’, 2), (‘b’, 3), (‘c’, 1), (‘c’, 2), (‘c’, 3)]
可以看到生成了一個3*3的結果集。
我們需要注意到,生成順序是與推導式中兩個
for
表達式的前後順序有關,如果我們要先基于
listB
來周遊生成,隻需要修改為
[(b,a) for b in ListB for a in ListA]
這樣即可。
類似的,生成器表達式也可以用于生成笛卡爾積。
listA = ['a', 'b', 'c']
listB = [1, 2, 3]
results = []
for result in ((a, b) for a in listA for b in listB):
results.append(result)
print(results)
寫法與推導式極為相似,不過需要注意的是,因為生成器是“一個個生成”,是以必須用在疊代中。
排序
排序也是一個在有序容器中很常見的問題。Python中提供兩個内置函數
sort
和
sorted
用于處理排序。
sort
sort
用于對有序資料模型直接排序,即會改變目前序列。
from random import randint
listA = [randint(1, 100) for i in range(0, 10)]
print(listA)
listA.sort()
print(listA)
輸出
[59, 5, 82, 10, 86, 14, 91, 30, 78, 74]
[5, 10, 14, 30, 59, 74, 78, 82, 86, 91]
sorted
sorted
與
sort
不同,它不會改變原本的序列,而是會生成一個排序後的副本。
from random import randint
listA = [randint(1, 100) for i in range(0, 10)]
print(sorted(listA))
print(listA)
輸出
[10, 13, 15, 26, 32, 52, 52, 71, 78, 86]
[86, 13, 10, 78, 32, 52, 26, 71, 15, 52]
你可能注意到了,
sort
和
sorted
的使用方式并不相同,
sort
是序列的方法,而
sorted
更像是預設函數。其實這種不同是Python這門語言特色的展現,說的直白點就是一切都向易用看齊。注意,是易用而非易學。
我覺得很多初學者都被Python随意的變量使用和寫法欺騙了,誤以為這是門很容易學的語言,然而這一切僅僅是為了友善使用,就像,隻要是實作了幾個預設的魔術方法,不管你的容器長啥樣,都可以用
sorted
來排序,這無疑是相當友善使用的,至于這對學習是否容易,那從來不是這門語言的建立者所考慮的。
sorted
key
除了以上的常見用法,Python的内置排序還支援指定key作為排序基準。
我們用以下示例說明:
listA = ["sdfsdf", "eaw", "dfwe", "aqwe", "kersfsq"]
print(sorted(listA))
print(sorted(listA, key=len))
輸出
[‘aqwe’, ‘dfwe’, ‘eaw’, ‘kersfsq’, ‘sdfsdf’]
[‘eaw’, ‘dfwe’, ‘aqwe’, ‘sdfsdf’, ‘kersfsq’]
可以看到,我們通過key制定了一個用于排序的基準,取代了預設的基準。需要注意的是key參數必須是一個隻接受一個參數的方法,這個方法用于處理序列中的元素,處理後再基于結果值進行排序。
二分查找
如果你接觸過排序算法,那肯定對二分查找不陌生。
所謂的二分查找,就是基于一組已排好序的資料,對一個新的元素,快速找出這個元素應當插入的部位,這個部位必須保持已有的順序。
二分查找的精髓是分治思想,即每次查找的時候都會縮小資料處理規模。
比如第一次,需要找出中位數進行比較,如果新元素小于中位數,就在中位數左側的一半資料中查找位置,反之就用右側資料。一次處理就可以讓資料比對規模減半。
雖然這種模式在極端情況下,比如新元素剛好小于中位數或大于中位數的時候,效率偏低。但總體來說效率依然客觀,是個不錯的算法。
Python原生支援二分查找,我們來看代碼實作:
import bisect
listA = [0, 1, 2, 3, 4, 5, 6]
index = bisect.bisect(listA, 3)
indexL = bisect.bisect_left(listA, 3)
indexR = bisect.bisect_right(listA, 3)
print(index)
print(indexL)
print(indexR)
輸出
4
3
4
可以看到,使用
bisect
子產品可以很容易實作二分查找。
這裡有個細節需要注意,在查找算法實作中,如果遇到新元素與已有元素相等的情況,我們通常要考慮是
左插入
還是
右插入
的問題,而
bisect
預設是右插入,這點在上邊示例中很容看出。
當然,二分查找的通常目的是找到正确位置後進行插入操作,這點
bisect
是支援的。
import bisect
listA = [0, 1, 2, 3, 4, 5, 6]
bisect.insort(listA, 3)
print(listA)
輸出
[0, 1, 2, 3, 3, 4, 5, 6]
當然,之前也說過了,二分查找是在排序算法中的内容,當然可以用二分查找來實作一個二分排序:
import random
import bisect
def bisectSort(listA: list) -> list:
sortedList = []
for num in listA:
bisect.insort(sortedList, num)
return sortedList
listA = [random.randint(1, 100) for i in range(0, 10)]
print(listA)
print(bisectSort(listA))
當然,這裡隻是用一個示例說明如何用二分查找實作二分排序,Python的内置排序函數
sort
的綜合效率是要優于純粹的二分查找算法的。
切片
在之前的文章Python學習筆記1:清單中我介紹過如何用切片快速通路清單中的某一段資料,和其它流行語言比起來,這無疑很酷很高效,但切片能做的不僅僅如此。
我們來看下面的示例:
listA = ['a', 'b', 'c', 'd', 'e']
listA[1:4] = ['d', 'c', 'b']
print(listA)
輸出
[‘a’, ‘d’, ‘c’, ‘b’, ‘e’]
在這個示例中我們“創造性”地把切片用于指派操作地左側,實作了修改某一段子序列地用途。
當然,我們還可以指定步進,間隔着修改序列:
listA = ['a', 'b', 'c', 'd', 'e']
listA[::2] = ['z', 'z', 'z']
print(listA)
輸出
[‘z’, ‘b’, ‘z’, ‘d’, ‘z’]
元組
我們通常對Python中元組的認識就是不可變序列,但其實除了不可變序列,元組也可以承擔類似字典的任務。
具名元組
除了常用的清單元組等,Python還提供一些其它有用的容器,這些容器都在标準庫的
collections
子產品。
這其中有個
namedtuple
,即具名元組,它是一個工廠函數,可以傳回一個類,這個類将實作類似字典的資料結構。
from collections import namedtuple
Person = namedtuple("Person", ("name", "age", "career", "favorite"))
Jack = Person("Jack chen", 17, "actor", ("swimming", "running"))
Brus = Person("Brus Lee", 20, "engineer", ("football", "table tennis"))
print(Jack)
print(Brus)
print(Jack.name)
print(Jack.favorite)
print(Jack[1])
dictJack = Jack._asdict()
print(dictJack)
輸出
Person(name=‘Jack chen’, age=17, career=‘actor’, favorite=(‘swimming’, ‘running’))
Person(name=‘Brus Lee’, age=20, career=‘engineer’, favorite=(‘football’, ‘table tennis’))
Jack chen
(‘swimming’, ‘running’)
17
{‘name’: ‘Jack chen’, ‘age’: 17, ‘career’: ‘actor’, ‘favorite’: (‘swimming’, ‘running’)}
我們可以看到,使用具名元組的方式很像是在excel中制表,
namedtuple(...)
的使用很像是在做一個表頭,做好後我們需要做的就是一行一行填充資料,而
Jack=Person(...)
就是在做這樣的工作。
事實上也是如此,我們的工作像是給一組資料結構相似的元組加上了一個表頭,這樣做的好處是節省存儲空間。
想一下,如果我們用字典來替代這裡的具名元組,每個字典需要4個key,如果有5個
Person
資料,那就是需要20個key,而具名元組的字段名是存儲在工廠方法生成的
Person
類上的,所有
Person
生成的具名元組擁有同樣的字段名,也就是說5個具名元組也隻需要4個key。
除了上面的優點,具名元組的資料通路也很靈活,既可以用
.key
這樣使用字段名通路資料,也可以用傳統的切片來通路。
最後,具名元組還可以使用
.asdict()
來轉換為字典,因為它表現出來的結構和字典基本一緻。
不可變序列
元組作為不可變序列,主要用途就是作為資料容器使用。
拆包
拆包其實在之前的文章Python學習筆記17:清單II中已經介紹過了,不過稱呼不同,當時使用的是解壓,這是《Python Cookbook》中的稱呼,《Fluent Python》中稱之為拆包。
關于拆包的大部分内容已經在前文介紹過了,這裡不再贅述。隻是在這基礎上提供一個額外的示例,用來說明Python的靈活性:
tupleA = ("Jack Chen", 16, "engineer", ("football", "table tennis"))
name, *_, (favorite1, favorite2) = tupleA
print(name, favorite1, favorite2)
輸出
Jack Chen football table tennis
我們可以看到,即使是元組嵌套元組,我們也可以直接使用拆包擷取嵌套結構中的資料。
當然,不止是指派語句,拆包還可以用在其它語句中,比如
for/in
:
persons = [("Jack Chen", 16, "engineer", ("football", "table tennis")),
("Brus Lee", 20, "actor", ("swimming", "running"))]
for name, *_, (favorite1, favorite2) in persons:
print(name, favorite1, favorite2)
輸出
Jack Chen football table tennis
Brus Lee swimming running
包含可變容器
雖然我們經常說元組是不可變清單,但這其實指的是元組包含的都是原子變量的情況,如果一個元組包含的是可變容器,那情況就會變得有些微妙。
我們來看一個很有趣的例子:
jack = ("Jack Chen", 16, "engineer", ["football", "table tennis"])
jack[-1] += ["swimming"]
print(jack)
輸出
Traceback (most recent call last):
File “d:\workspace\python\test\test.py”, line 2, in
jack[-1] += [“swimming”]
TypeError: ‘tuple’ object does not support item assignment
我們視圖擴充元組中的清單,輸出錯誤資訊,這似乎是理所應當的?
但我們再看下面這個:
jack = ("Jack Chen", 16, "engineer", ["football", "table tennis"])
jack[-1].extend(["swimming"])
print(jack)
輸出
(‘Jack Chen’, 16, ‘engineer’, [‘football’, ‘table tennis’, ‘swimming’])
正常執行。
這兩者有何差別?
jack[-1]+=["swimming"]
其實是兩個動作,顯示執行
jack[-1]+["swimming"]
生成一個新的清單,這是沒問題的,但是第二步
jack[-1]=new_list
就會出現錯誤,因為元組是不允許内部元素重新指派的。
但是如果我們使用的是
.extend()
,在原清單上進行擴充,并不涉及重新指派,那就沒有任何問題。
可以看到,元組并非真的内部元素完全不可變,而是取決于具體情況有所不同。是以最佳方案是盡量減少類似的做法,還是僅把元組作為一個不可變清單使用。如果你需要修改内部元素,那你或許應該一開始就聲明為清單,而非元組。
需要特别說明的是,雖然在介紹容器序列時,很多特殊操作都是用清單來舉例,但實際上這些操作都支援所有的容器序列。
扁平序列
扁平序列是相對于容器序列而言的,所謂扁平序列,就是内部資料類型統一,而且是原子類型,即不是容器。
這類序列通常是用于數學計算或大資料等,用于專業領域。
與容器序列相比,扁平序列更加特異化,隻能存儲某一類資料,但是記憶體占用更少,處理速度更快。
雙向隊列
隊列是一個很常見的資料結構,最經典的是
First in first out
隊列。
Python标準庫提供一個雙向隊列,即相比
First in first out
,隻能一邊進一邊出,雙向隊列可以同時在左右兩邊進行資料壓入和抛出。
from collections import deque
myQuen = deque(maxlen=10)
listNum = [i for i in range(0,20)]
myQuen.extend(listNum[:10])
print(myQuen)
myQuen.extend(listNum[10:13])
print(myQuen)
myQuen.extendleft(listNum[13:16])
print(myQuen)
myQuen.pop()
myQuen.insert(2,0)
print(myQuen)
輸出
deque([0, 1, 2, 3, 4, 5, 6, 7, 8, 9], maxlen=10)
deque([3, 4, 5, 6, 7, 8, 9, 10, 11, 12], maxlen=10)
deque([15, 14, 13, 3, 4, 5, 6, 7, 8, 9], maxlen=10)
deque([15, 14, 0, 13, 3, 4, 5, 6, 7, 8], maxlen=10)
在示例中,我們可以從左右兩邊都壓入和抛出資料(預設情況是右邊)。
通常我們會給隊列指定一個最大容量
maxlen
,在這種情況下,如果壓入資料後會超出隊列的最大容量,隊列就會自動抛棄另一頭相應數量的資料。
就像例子中的那樣,
deque
也支援一些非常“不隊列”的操作,比如在指定位置插入一個元素。但需要說明的是對于這種對于隊列來說非正常的操作,
deque
是沒有進行性能優化的,執行效率比較低。相對而言,對于兩頭壓入和抛出資料,
deque
的執行效率很高。
數組
我們經常說Python中的清單很像是數組,但它終究不是,清單實際上是一個便于使用和效率考量的折中方案。而這就意味着如果你在某種情況下,需要大規模單一資料類型的運算時,清單并不合适。
這時候,就需要使用數組了,真正的數組。
import array
import random
a = array.array('i', (random.randint(1, 100) for i in range(0, 1000)))
print(a[-1])
fopen = open(mode='wb', file='array.file')
a.tofile(fopen)
fopen.close()
b = array.array('i')
fopen = open(mode='rb', file='array.file')
b.fromfile(fopen, 1000)
fopen.close()
print(b[-1])
print(a == b)
輸出
77
77
True
就像示例中展示的那樣,在建立數組時候我們需要像其它語言中那樣,指明資料類型。

如果你使用的是VSCode,可以在函數幫助文檔中看到資料類型說明。同樣的,我們需要像在C++中那樣,注意不同的資料類型下存儲容量和資料範圍。
此外,數組支援從其它資料類型中讀取和建立,也支援寫入到其它資料類型中,這其中甚至包含檔案。
就像示例中展示的那樣,我們可以很輕松地把數組寫入到二進制檔案,或者從二進制檔案讀取。這樣做和以文本形式讀取檔案相比,不僅空間占用低,寫入和讀取時間也顯著減少。
NumPy
NumPy是一個用于科學計算的強大的第三方子產品,中文官網是**https://www.numpy.org.cn/**。
NumPy對矩陣的操作異常強大,我們僅舉一個簡單的例子說明:
import numpy
nArray = numpy.arange(20)
print(nArray)
print(type(nArray))
print(nArray.shape)
nArray.shape=(4,5)
print(nArray)
print(nArray[2])
print(nArray[:,3])
print(nArray[2][3])
print(nArray.transpose())
輸出
[ 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19]
<class ‘numpy.ndarray’>
(20,)
[[ 0 1 2 3 4]
[ 5 6 7 8 9]
[10 11 12 13 14]
[15 16 17 18 19]]
[10 11 12 13 14]
[ 3 8 13 18]
13
[[ 0 5 10 15]
[ 1 6 11 16]
[ 2 7 12 17]
[ 3 8 13 18]
[ 4 9 14 19]]
在這個例子中,我們通過
NumPy
提供的資料類型
ndarray
輕松把一個一維數組轉變為了二維數組,而且可以很容易地進行橫向和豎向資料切片。更厲害地是還可以進行矩陣操作,比如最後那個矩陣翻轉。
當然
NumPy
這個庫相當強大,這些僅僅是冰山一角,這裡僅做一個介紹。
除了以上介紹的幾種,扁平序列還包括位元組數組、字元串等,這裡不做過多介紹,後邊有機會再深入講解。
最後附上一個這部分内容的思維導圖:
好了,關于清單的補充内容就介紹到這裡了,謝謝閱讀。