天天看點

Python學習筆記19:清單 IIIPython學習筆記19:清單 III

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

就像示例中展示的那樣,在建立數組時候我們需要像其它語言中那樣,指明資料類型。

Python學習筆記19:清單 IIIPython學習筆記19:清單 III

如果你使用的是VSCode,可以在函數幫助文檔中看到資料類型說明。同樣的,我們需要像在C++中那樣,注意不同的資料類型下存儲容量和資料範圍。

此外,數組支援從其它資料類型中讀取和建立,也支援寫入到其它資料類型中,這其中甚至包含檔案。

就像示例中展示的那樣,我們可以很輕松地把數組寫入到二進制檔案,或者從二進制檔案讀取。這樣做和以文本形式讀取檔案相比,不僅空間占用低,寫入和讀取時間也顯著減少。

NumPy

NumPy是一個用于科學計算的強大的第三方子產品,中文官網是**https://www.numpy.org.cn/**。

Python學習筆記19:清單 IIIPython學習筆記19:清單 III

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

這個庫相當強大,這些僅僅是冰山一角,這裡僅做一個介紹。

除了以上介紹的幾種,扁平序列還包括位元組數組、字元串等,這裡不做過多介紹,後邊有機會再深入講解。

最後附上一個這部分内容的思維導圖:

Python學習筆記19:清單 IIIPython學習筆記19:清單 III

好了,關于清單的補充内容就介紹到這裡了,謝謝閱讀。