我們在這個系列的開篇有提到,所謂的編程,其實就兩個核心,一個是數據的表達,一個是數據的處理。
前面連著分享了幾篇關于函數的文章,主要是聚焦的數據的處理。Python中一切皆對象,其實是一種設計思想,將數據的表達與數據的處理封裝在一起。
從今天開始,打算通過幾篇文章,來重點介紹下Python中關于數據表達的部分,用計算機專業的術語來說,就是“數據結構”。
在編程中,最常用的數據結構,主要有數組、鏈表、字典、集合等。
Python中內置的對應實現有:列表(list)、元組(tuple)、字典(dict)、集合(set)等,還包括一些其他模塊中更好用的容器類,在后面的幾篇文章中都會陸續進行介紹,今天這篇文章,首先來看一下Python中的列表(list)。
需要說明的是,有的地方把這些數據結構稱為集合的操作,有些地方又稱為容器對象。為了避免與set的集合的含義的混淆,我這里統一稱為容器對象了。
Python中的列表,是一個非常強大的數據結構,具有很多顯著的特點。用一句話來形容Python的列表的話,就是“Great Actor”(特別能裝)。因為不同于其他編程語言中類似的數據結構,Python中的列表,可以存儲任意類型的數據元素。除了特別能裝外,列表的主要特點如下:
1、可變性(Mutable):列表是可變的,可以進行增、刪、改的操作;
2、有序性(Ordered):列表中的元素是有序的;
3、支持任意類型:不要求元素類型一致,這是跟其他編程語言尤其是靜態類型語言中數據結構的不同點;
4、可以動態調整大?。焊鶕嶋H的需要,自動進行長度的縮減;
5、提供了豐富的內置方法:如增加、刪除、排序等
通過查看list的定義,可以看到list所支持的內置方法清單。這里,不再進行一一列舉,感興趣的童鞋可以自行查看。
這里只對常用的一些方法做一下簡要說明:
# 創建一個空列表
great_box=[]
print(great_box)
# 尾部追加一個字符串
great_box.append('Python')
print(great_box)
# index為0的位置插入一個字符串
great_box.insert(0, 'Java')
print(great_box)
# 追加一個list對象作為元素
great_box.append([1, 2, 3])
print(great_box)
# 將一個元組的元素分別添加到列表中
# 各種類型都可以
great_box.extend((tuple(), list(), {}))
print(great_box)
# 默認把尾部的元素移除(彈出)
great_box.pop()
print(great_box)
# 彈出指定索引的元素
res=great_box.pop(1)
print(res)
print(great_box)
# 列表元素逆序,原地修改,也就是改變原對象
great_box.reverse()
print(great_box)
# 獲取元素個數,內置函數len()
print(len(great_box))
執行結果:
可以看到,這些方法跟其他編程語言中也都是比較類似的。
接下來,以幾個實際場景,演示list的使用。
對數據的排序應該是數據處理中,一個特別常用的操作。雖然,我們不需要自己編寫排序算法,但是很多方法的底層,也會用到排序。
Python中對列表中的元素進行排序,有兩種常見的方法:
方法1:列表的方法(進行原地修改(in place))
import random
# 隨機生成一組身高數據,保存到列表heights中
heights=[]
for _ in range(10):
heights.append(random.randint(140, 200))
print('='*22 + '原始順序' + '='*22)
print(heights)
# 原地排序操作,默認升序
print('='*22 + '升序排序' + '='*22)
heights.sort()
print(heights)
# 降序排列
print('='*22 + '降序排序' + '='*22)
heights.sort(reverse=True)
print(heights)
執行結果:
上面這個例子,其實沒有任何實際的價值,我們在大多數場景中,不會只有一個列表存儲了身高,而沒有其他信息。
通常來說,會把人員的相關信息一起存儲,我們需要按照身高排序,或者按照年齡排序,sort()方法其實也是可以支持的,可以對照list的sort()方法的定義來看:
from faker import Faker
fk=Faker('zh_CN')
persons=[]
for _ in range(10):
# 以元組形式存儲人員信息(姓名,年齡,身高)
persons.append((fk.name(), fk.random_int(10, 150), fk.random_int(140, 200)))
print('=' * 22 + '原始順序' + '=' * 22)
print(persons)
print('=' * 22 + '按照年齡升序' + '=' * 22)
persons.sort(key=lambda x: x[1])
print(persons)
print('=' * 22 + '按照身高降序' + '=' * 22)
persons.sort(key=lambda x: x[2], reverse=True)
print(persons)
執行結果:
方法2:使用內置函數sorted()
需要注意的是,不同于列表對象的sort()方法,sorted()函數并不會在原地排序,也就是不會改變原列表對象,而是通過函數返回值,返回排序之后的新的列表對象:
from faker import Faker
fk=Faker('zh_CN')
persons=[]
for _ in range(10):
# 以元組形式存儲人員信息(姓名,年齡,身高)
persons.append((fk.name(), fk.random_int(10, 150), fk.random_int(140, 200)))
print('=' * 22 + '原始順序' + '=' * 22)
print(persons)
print('=' * 22 + '按照年齡升序' + '=' * 22)
# persons.sort(key=lambda x: x[1])
new_persons=sorted(persons, key=lambda x: x[1])
print(new_persons)
print('=' * 22 + '按照身高降序' + '=' * 22)
# persons.sort(key=lambda x: x[2], reverse=True)
new_persons=sorted(persons, key=lambda x: x[2], reverse=True)
print(persons)
執行結果:
可以看到,唯一的不同,是否原地修改的。
在Python中,使用in、not in操作符,可以很便捷地判定元素在列表對象中是否存在:
languages=['Python', 'Java', 'Go']
if 'Python' in languages:
print('Python exists')
if 'C++' not in languages:
print('C++ not exists')
執行結果:
在Python中,可以通過列表對象的index()方法,快速找到某個特定元素在列表中的位置。
需要注意的是,如果元素出現多次,會返回第一個索引;如果元素不存在,會拋異常。
languages=['Python', 'Java', 'Go', 'Java']
print(languages)
idx=languages.index('Java')
print(idx)
idx=languages.index('C++')
print(idx)
執行結果:
前面我們通過for循環的方式,將元素逐個追加到列表中,其實,是有些繁瑣的。還是那句話,“能用一行代碼搞定的事,絕對不寫兩行”。
Python中提供列表推導式的語法,幫助我們簡化列表對象的構建:
還是以測試人員信息的列表生成為例,我們使用列表推導式來改寫:
from faker import Faker
fk=Faker('zh_CN')
# persons=[]
# for _ in range(10):
# # 以元組形式存儲人員信息(姓名,年齡,身高)
# persons.append((fk.name(), fk.random_int(10, 150), fk.random_int(140, 200)))
persons=[(fk.name(), fk.random_int(10, 150), fk.random_int(140, 200)) for _ in range(10)]
注釋的三行代碼,使用列表推導式,只需要一行代碼就搞定了。
剛開始接觸列表推導式,可能不太習慣,但是用得多了,你一定會被這種簡潔性所征服,不由自主地選擇使用列表推導式。
除了列表的生成可以使用列表推導式外,字典、集合同樣支持推導式的快速生成。
此外,“Python一行流”的很多代碼編寫方法也是基于推導式的方式來加以實現的。
本文簡單介紹了Python中的列表這個容器類型,包括常用的內置方法以及列表推導式的使用。列表的靈活性,配合列表推導式的簡潔性,在實際的Python編程實踐中非常實用,掌握了列表的使用,一定能大大提升Python開發的效率。
創建容器視圖的范圍
新的范圍庫是 C++20 中更重要的新增內容之一。它為過濾和處理容器提供了一種新的范式。范圍提供了干凈直觀的構建塊,使得代碼更有效、更易讀。
讓我們首先定義一些術語:
一個范圍是可以迭代的對象集合。換句話說,任何支持 begin() 和 end() 迭代器的結構都是一個范圍。這包括大多數 STL 容器。
視圖是一個轉換另一個底層范圍的范圍。視圖是惰性的,這意味著它們只在范圍迭代時操作。視圖返回底層范圍的數據,本身不擁有任何數據。視圖以 O(1) 常數時間操作。
視圖適配器是一個對象,它接受一個范圍并返回一個視圖對象。視圖適配器可以使用 | 運算符與其他視圖適配器鏈式使用。
注意
<ranges> 庫使用 std::ranges 和 std::ranges::view 名稱空間。認識到這很笨重,標準包括一個別名 std::ranges::view 作為簡單的 std::view。我覺得這仍然很笨重。對于這個食譜,我將使用以下別名,以節省空間,因為我覺得它更優雅:
namespace ranges=std::ranges; // 節省手指!
namespace views=std::ranges::views;
這適用于這個食譜中的所有代碼。
如何做到這一點…
范圍和視圖類在 <ranges> 頭文件中。讓我們看看你如何使用它們:
視圖應用于范圍,如下所示:
const vector<int> nums{ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
auto result=ranges::take_view(nums, 5);
for (auto v: result) cout << v << " ";
輸出:
1 2 3 4 5
ranges::take_view(range, n) 是一個返回前 n 個元素的視圖。
你也可以使用 take_view() 的視圖適配器版本:
auto result=nums | views::take(5);
for (auto v: result) cout << v << " ";
輸出:
1 2 3 4 5
視圖適配器在 std::ranges::views 名稱空間中。視圖適配器從 | 運算符左側的 range 操作數中獲取范圍,很像 iostreams 使用 << 運算符的方式。| 運算符是從左到右評估的。
因為視圖適配器是可迭代的,它也符合范圍的資格。這允許它們被連續應用,如下所示:
const vector<int> nums{ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
auto result=nums | views::take(5) |
views::reverse;
輸出:
5 4 3 2 1
filter() 視圖使用謂詞函數:
auto result=nums |
views::filter([](int i){ return 0==i % 2; });
輸出:
2 4 6 8 10
transform() 視圖使用轉換函數:
auto result=nums |
views::transform([](int i){ return i * i; });
輸出:
1 4 9 16 25 36 49 64 81 100
當然,這些視圖和適配器適用于任何類型的范圍:
cosnt vector<string> words{ "one", "two", "three", "four", "five" };
auto result=words | views::reverse;
輸出:
five four three two one
范圍庫還包括一些范圍工廠。iota 工廠將生成一系列遞增的值:
auto rnums=views::iota(1, 10);
輸出:
1 2 3 4 5 6 7 8 9
iota(value, bound) 函數從 value 開始生成一個序列,結束在 bound 之前。如果省略了 bound,序列就是無限的:
auto rnums=views::iota(1) | views::take(200);
輸出:
1 2 3 4 5 6 7 8 9 10 11 12 […] 196 197 198 199 200
范圍、視圖和視圖適配器非常靈活和有用。讓我們更深入地了解,以更好地理解。
它是如何工作的…
為了滿足范圍的基本要求,一個對象必須至少有兩個迭代器,begin() 和 end(),其中 end() 迭代器是一個哨兵,用于確定范圍的終點。大多數 STL 容器都符合范圍的要求,包括 string、vector、array、map 等,值得注意的例外是容器適配器,如 stack 和 queue,它們沒有 begin 和 end 迭代器。
視圖是一個操作范圍并返回修改后范圍的對象。視圖惰性操作,并且不包含自己的數據。它不保留底層數據的副本,而是根據需要簡單地返回指向底層元素的迭代器。讓我們檢查這段代碼片段:
vector<int> vi { 0, 1, 2, 3, 4, 5 };
ranges::take_view tv{vi, 2};
for(int i : tv) {
cout << i << " ";
}
cout << "\n";
輸出:
0 1
在這個例子中,take_view 對象接受兩個參數,一個范圍(在這種情況下,一個 vector<int> 對象)和一個計數。結果是從向量中取出前計數個對象的視圖。在評估時,在 for 循環迭代期間,take_view 對象簡單地根據需要返回指向向量對象元素的迭代器。在這個過程中,向量對象沒有被修改。
范圍名稱空間中的許多視圖在 views 名稱空間中都有相應的范圍適配器。這些適配器可以使用按位或 (|) 運算符使用,像這樣:
vector<int> vi { 0, 1, 2, 3, 4, 5 };
ranges::take_view tv{vi, 2};
for(int i : tv) {
cout << i << " ";
}
cout << "\n";
輸出:
0 1
正如預期的那樣,| 運算符從左到右評估。由于范圍適配器的結果又是另一個范圍,這些適配器表達式可以被鏈接:
vector<int> vi { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
auto tview=vi | views::reverse | views::take(5);
for(int i : tview) {
cout << i << " ";
}
cout << "\n";
輸出:
9 8 7 6 5
庫中還包括一個 filter 視圖,它與謂詞一起使用,用于定義簡單過濾器:
vector<int> vi { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
auto even=[](long i) { return 0==i % 2; };
auto tview=vi | views::filter(even);
輸出:
0 2 4 6 8
還包括一個 transform 視圖,它與轉換函數一起使用,用于轉換結果:
vector<int> vi { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
auto even=[](int i) { return 0==i % 2; };
auto x2=[](auto i) { return i * 2; };
auto tview=vi | views::filter(even) | views::transform(x2);
輸出:
0 4 8 12 16
庫中還有很多有用的視圖和視圖適配器。請查看您最喜歡的參考網站,或 (https://j.bw.org/ranges) 以獲取完整列表。
還有更多…
從 C++20 開始,<algorithm> 頭文件中的大多數算法都包括了用于范圍的版本。這些版本仍然在 <algorithm> 頭文件中,但在 std::ranges 名稱空間中。這使它們與舊算法區分開來。
這意味著,你可以用一個范圍而不是兩個迭代器來調用一個算法:
sort(v.begin(), v.end());
你現在可以這樣調用它:
ranges::sort(v);
這當然更方便,但它真的有什么幫助嗎?
考慮你想對向量的某部分進行排序的情況,你可以用舊方法這樣做:
sort(v.begin() + 5, v.end());
這將對向量的前 5 個元素之后的元素進行排序。使用范圍版本,你可以使用視圖跳過前 5 個元素:
ranges::sort(views::drop(v, 5));
你甚至可以組合視圖:
ranges::sort(views::drop(views::reverse(v), 5));
實際上,你可以甚至將范圍適配器作為參數傳遞給 ranges::sort:
ranges::sort(v | views::reverse | views::drop(5));
與使用傳統的 sort 算法和向量迭代器相比,雖然這肯定更短,也不是不可能理解,但我覺得范圍適配器的版本直觀得多。
你可以在 cppreference 網站(https://j.bw.org/algoranges)上找到已經約束為與范圍一起工作的算法的完整列表。
在本文中,我們只是淺嘗輒止地介紹了范圍和視圖。這個特性是十多年來許多不同團隊工作的結晶,我預計它將根本改變我們在 STL 中使用容器的方式。