《Fluent Python》學習筆記:第 2 章 序列組成的數組

本文主要是 Fluent Python 第 2 章的學習筆記。這部分主要是介紹了序列、列表、元組的一些高級用法。

2.1 內置序列分類

序列(sequence)分類,按照存放類型分類:

  1. 容器序列(container sequences):list, tuple, 和 collections.deue 能存放不同類型的數據。因爲它們存放的是任意類型對象的引用。
  2. 平面序列(flat sequences): str, bytes, bytearray, memoryview 和 array.array 只能放一種數據類型。因爲它們是在連續的內存空間上存放每個元素的值。

序列(sequence)分類,按照是否可變(mutability)分類:

  1. 可變序列(mutable sequences):list, bytearray, array.array, collections.deque, 和 memoryview.
  2. 不可變序列(immutable sequences):tuple, str, 和 bytes.

2.2 列表推導和生成器表達式

列表推導(list comprehensions, listcomps):構建 list 的快捷方式。
生成器表達式(generator expression, genexps):創建任何類型的序列的快捷方式。
它們的優點在於 可讀性更好(more readable)經常更高效(often faster)

2.2.1 列表推導和可讀性

通過對比“把一個字符串變爲 Unicode 碼位的列表”的兩種實現方式,看看列表推導的可讀性(readability)

# 常規方式
symbols = 'abcdef'
codes = []
for symbol in symbols:
    codes.append(ord(symbol))
print(codes)
[97, 98, 99, 100, 101, 102]
# 用列表推導
symbols = 'abcdef'
codes = [ord(symbol) for symbol in symbols]
print(codes)
[97, 98, 99, 100, 101, 102]

什麼時候用列表推導式呢?

  1. 只用列表推導式創建新的列表,並且儘量保持簡單。
  2. 如果列表推導式的代碼超過兩行,就要考慮用 for 循環重寫。

列表推導式相比較於 for 循環還有一個優勢在於,可以避免變量泄漏(leak variable)。注意:在 Python 2.x 版本存在變量泄漏問題。 Python 3 中不存在了。
舉個栗子:

x = 'my precious'
ls = [x for x in 'abc']
print(x)  # output: my precious

x = 'my precious'
ls_2 = []
for x in 'abc':
    ls_2.append(x)
print(x)  # output: c
my precious
c

注意我這裏用的是 Python 3.7 測試的,可以發現用 for 循環會修改變量 x 的值,導致變量泄漏。而用列表推導式就不會導致變量泄漏,原因是列表推導式(以及生成器表達式、集合推導式、字典推導式)在 Python 3 中都有自己的局部作用域,就像函數一樣,表達式內部的變量和賦值,只作用於局部,不會影響推導式外面的變量和賦值。

2.2. 列表推導式 VS filter and map

列表推導式通過對序列或者其他可迭代類型進行過濾和加工操作,從而生成新的列表。內置的 filter 和 map 函數也是做這個的,兩者有什麼異同呢?

  1. 列表推導式可讀性更強。
  2. 二者的運行速度不一定誰快誰慢。

看下面這個栗子:

# 列表推導式
symbols = 'abcdef'
codes = [ord(symbol) for symbol in symbols if ord(symbol) > 100]
print(codes)

# filter 和 map
symbols = 'abcdef'
codes = list(filter(lambda x: x > 100, map(ord, symbols)))
print(codes)
[101, 102]
[101, 102]

可以看到,filter 和 map 能做的事情,列表推導式也能做,而且可讀性更好。運行速度,它們得看情況。

2.2.3 笛卡爾積

列表推導式可以使用多個嵌套的 for 循環,如使用列表推導式計算笛卡爾積。

# 用列表推導式
colors = ['black', 'white']
sizes = ['S', 'M', 'L']
tshirts = [(color, size) for color in colors for size in sizes]
print(tshirts)

# 用嵌套 for 循環
for color in colors:
    for size in sizes:
        print((color, size))
[('black', 'S'), ('black', 'M'), ('black', 'L'), ('white', 'S'), ('white', 'M'), ('white', 'L')]
('black', 'S')
('black', 'M')
('black', 'L')
('white', 'S')
('white', 'M')
('white', 'L')

對比可以發現:

  1. 這裏得到的結果是先以顏色排列,再以尺碼排列。
  2. 列表推導式中的 2 個 for 語句順序和用 for 循環嵌套實現的順序一樣。
  3. 如果想實現先按尺碼排列,在以顏色排列,只需要把兩個 for 子句換一下位置。

2.2.4 生成器表達式

列表推導式的作用是用來生成列表,雖然列表推導式也可以初始化元組、數組或其他序列類型,但是這時選擇生成器表達式(generator expressions, genexp)是更好,因爲它更節省內存。具體原因是:生成器表達式遵循迭代器協議,是惰性計算,需要的時候一個一個生成元素;列表推導式是先創建一個完整的列表,然後把這個列表傳遞到某個構造函數。
生成器表達式的語法和列表推導式一樣,只是把方括號或者圓括號。
下面通過兩個栗子體驗一下,生成器表達式初始化除列表之外的序列。

# 用生成器表達式初始化元組
symbols = 'abcdef'
# 如果生成器表達式是一個函數調用過程中的唯一參數,
# 那麼不需要額外用圓括號把它括起來
codes = tuple(ord(symbol) for symbol in symbols)
print(codes)

# 用生成器表達式初始化數組
import array
# array 構造方法需要兩個參數,因此括號是必須的。
# array 構造方法的第一個參數制定了數組中數字的存儲方式
print(array.array('I', (ord(symbol) for symbol in symbols)))

# 生成器表達式計算笛卡爾積
colors = ['black', 'white']
sizes = ['S', 'M', 'L']
# 生成器會逐個產生元素,不會一次性生成含有6個T恤樣式的列表,
# 從而節省內存
for tshirt in (f"{c} {s}" for c in colors for s in sizes):
    print(tshirt)
(97, 98, 99, 100, 101, 102)
array('I', [97, 98, 99, 100, 101, 102])
black S
black M
black L
white S
white M
white L

2.3 元組不僅僅是不可變列表

元組(tuple)實際上有兩個作用:

  1. 作爲不可變的列表。
  2. 用於沒有字段名的記錄。

2.3.1 元組和記錄

元組其實是對數據的記錄(record):元組中的每個元素都存放了記錄中的一個字段的數據,外加這個字段(field)的位置。

2.3.2 元組拆包

元組拆包(tuple unpacking)可以應用到任何可迭代對象上,唯一的硬性要求是:被迭代對象中的元素數量必須要跟接受這些元素的元組的空擋數一樣,或者用 * 來表示忽略多餘的元素。現在可迭代元素拆包(iterable unpacking)慢慢流行被接受。
元組拆包應用:

  1. 多變量同時賦值。
  2. 交換變量值。
  3. 函數返回多個值。
  4. 對於不需要的數據,可以用佔位符 _ 處理。
  5. * 也可以處理多餘的元素。
a, b = 12, 13
print(a, b)
a, b = b, a  # 元組拆包
print(a, b)

import os
# _ 做佔位符,丟棄不要的數據
_, filename = os.path.split('/home/Jock/.ssh/idrsa.pub')
print(filename)

# * 處理剩餘元素
a, b, *rest = range(1, 5)
print(f"rest:{rest}")
*head, a, b = range(1, 3)
print(f"rest:{head}")
a, *body, b = range(1, 5)
print(f"body: {body}")

## 嵌套元組拆包
t = (1, 2, 3, (4, 5))
a, b, c, (d, e) = t
print(f"d: {d}")
print(f"e: {e}")
12 13
13 12
idrsa.pub
rest:[3, 4]
rest:[]
body: [2, 3]
d: 4
e: 5

2.4 切片

Python 中 list、tuple、str 都支持切片(slicing)操作。

Python 中爲什麼切片和區間操作不包含區間範圍內的最後一個元素?原因及好處:

  1. 這個習慣符合 Python、C 和其他語言下標以 0 開始的傳統。
  2. 當只有最後一個位置信息時,我們也可以快速看出切片和區間裏有幾個元素:range(3)和 my_list[:3]都返回 3 個元素。
  3. 當起止位置信息都可見時,我們可以快速計算出切片和區間的長度,即:用最後一個數減去第一個下標(stop - start)。
  4. 方便我們利用任意一個下標把序列分割成不重疊的兩部分,只要寫成 my_list[:x] 和 my_list[x:]即可。

切片的作用:

  1. 提取序列中的內容。
  2. 就地修改可變序列。
# 切片賦值
l = list(range(10))
print(l)
l[2:5]=[20, 30]
print(l)
del l[5:7]
print(l)
l[3::2] = [11, 22]
print(l)
# 對切片賦值則右邊也必須是一個可迭代對象
l[2:5] = 100
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
[0, 1, 20, 30, 5, 6, 7, 8, 9]
[0, 1, 20, 30, 5, 8, 9]
[0, 1, 20, 11, 5, 22, 9]



---------------------------------------------------------------------------

TypeError                                 Traceback (most recent call last)

<ipython-input-16-79d0212c1cd1> in <module>
      9 print(l)
     10 # 對切片賦值則右邊也必須是一個可迭代對象
---> 11 l[2:5] = 100


TypeError: can only assign an iterable

2.5 序列拼接(+和*)

+:+號兩側的序列由相同的數據類型構成,拼接過程中,兩個被操作的序列不會被修改,Python 會新建一個包含同樣類型數據的序列來作爲拼接的結果。
*:重複一個序列,然後拼接成一個新的序列。

+* 都遵循不改變原有的操作序列,而是構建一個新的全新序列。

list_a = [1, 2, 3]
list_b = [4, 5, 6]
list_c = list_a +list_b
print(list_c)

print(list_a * 3)
[1, 2, 3, 4, 5, 6]
[1, 2, 3, 1, 2, 3, 1, 2, 3]

注意由 * 引發的問題。

# 用 * 初始化嵌套列表,正確做法
board = [['_'] * 3 for i in range(3)]
print(board)
board[1][2] = 'X'
print(board)
[['_', '_', '_'], ['_', '_', '_'], ['_', '_', '_']]
[['_', '_', '_'], ['_', '_', 'X'], ['_', '_', '_']]

這等價於下面的代碼

board = []
for i in range(3):
    row = ['_'] * 3
    board.append(row)
print(board)
board[1][2] = 'X'
print(board)
[['_', '_', '_'], ['_', '_', '_'], ['_', '_', '_']]
[['_', '_', '_'], ['_', '_', 'X'], ['_', '_', '_']]
# 用 * 初始化嵌套列表,錯誤做法
board = [['_'] * 3 ] * 3
print(board)
board[1][2] = 'X'
print(board)
[['_', '_', '_'], ['_', '_', '_'], ['_', '_', '_']]
[['_', '_', 'X'], ['_', '_', 'X'], ['_', '_', 'X']]

可以發現錯誤的做法, 其實是在外面的列表包含了 3 個指向同一個列表的引用。即列表中的 3 個元素都是指向同一個列表對象。
這和下面的寫法是等效的

# 錯誤做法
row = ['_'] * 3
board = []
for i in range(3):
    board.append(row)
print(board)
board[1][2] = 'X'
print(board)
[['_', '_', '_'], ['_', '_', '_'], ['_', '_', '_']]
[['_', '_', 'X'], ['_', '_', 'X'], ['_', '_', 'X']]

2.6 序列的增量賦值

增量賦值運算符 +=*= 的表現取決於它們的第一個操作對象,+= 背後的特殊方法是 __iadd__ 用於就地加法,如果一個類沒有實現這個方法,那麼 Python 就會退一步調用,__add__
考慮這個簡單表達式:a += b
如果 a 實現了 __iadd__ 方法,那麼就會調用這個方法,同時對可變序列(list, set, array.array)來說,a 會就地改動。如果 a 沒有實現 __iadd__ 方法,那麼 a += b 的效果就和 a = a + b 一樣:首先計算 a + b,得到一個新的對象,再賦值給 a。即在這個表達式中,變量名會不會被關聯到新的對象中。完全取決於這個類型有沒有實現 __iadd__ 方法。

總體而言,可變序列一般實現了這個 __iadd__ 方法,因此 += 就是就地加法,而不可變序列,根本不支持這個操作,對這個方法的操作就無從談起。
所以寫成 list_a += list_b 比 list_a = list_a + list_b 高效!

# 用 ls = ls + [i]
ls = [1, 2]
print(id(ls))
for i in range(3, 10):
    ls = ls + [i]
print(ls)
print(id(ls))

# 用 ls += [i]
print('-' * 20)
ls = [1, 2]
print(id(ls))
for i in range(3, 10):
    ls += [i]
print(ls)
print(id(ls))
3140336148296
[1, 2, 3, 4, 5, 6, 7, 8, 9]
3140336867720
--------------------
3140336148296
[1, 2, 3, 4, 5, 6, 7, 8, 9]
3140336148296

+= 的概念也適用於 *=,不同的是,*= 是通過 __imul__ 實現的。

# *= 效果舉例
l = [1, 2, 3]
print(id(l))
l *= 2
print(l)
print(id(l))
print('-' * 20)
t = (1, 2, 3)
print(id(t))
t *= 2
print(t)
print(id(t))
3140336134728
[1, 2, 3, 1, 2, 3]
3140336134728
--------------------
3140336928072
(1, 2, 3, 1, 2, 3)
3140336653032

注意:對不可變序列進行重複拼接操作,效率會很低,因爲每次都有一個新對象,而解釋器需要把原來對象中的元素先複製到新的對象裏,然後再追加新的元素。
不過 str 是個例外,因爲對字符串 += 操作太頻繁了,所以 CPython 對它進行了優化, 爲 str 初始化內存的時候,程序會爲它留出額外的可擴展空間,因此進行增量操作的時候,並不會涉及複製原有字符串到新位置這類操作。

# 一個有意思的關於 += 的謎題
t = (1, 2, [30, 40])
t[2] += [50, 60]  # 這裏用 t[2].extend([50, 60]) 就不胡報錯了
print(t)
---------------------------------------------------------------------------

TypeError                                 Traceback (most recent call last)

<ipython-input-4-99689adb4d4a> in <module>
      1 # 一個有意思的關於 += 的謎題
      2 t = (1, 2, [30, 40])
----> 3 t[2] += [50, 60]  # 這裏用 t[2].extend([50, 60]) 就不胡報錯了
      4 print(t)


TypeError: 'tuple' object does not support item assignment
print(t)
import dis
print(dis.dis('s[a] += b'))
(1, 2, [30, 40, 50, 60])
  1           0 LOAD_NAME                0 (s)
              2 LOAD_NAME                1 (a)
              4 DUP_TOP_TWO
              6 BINARY_SUBSCR
              8 LOAD_NAME                2 (b)
             10 INPLACE_ADD
             12 ROT_THREE
             14 STORE_SUBSCR
             16 LOAD_CONST               0 (None)
             18 RETURN_VALUE
None
  1. 不要把可變對象放到元組裏面。
  2. 增量賦值不是一個原子操作,我們剛纔也看到了,它雖然拋出了異常,但是還是完成了操作。
  3. 查看 Python 的字節碼並不難,而且它對我們瞭解代碼背後的運行機制很有幫助!

注:原子操作(atomic operation):不會被線程調度機制打斷的操作,即一旦開始,一定會執行完纔會結束。

2.7 list.sort 方法和內置函數 sorted

list.sort 和 sorted 的異同:

相同點:都含有兩個可選的僅限關鍵字參數(keyword-only arguments) keyreverse。其中 key 是一個只有一個參數的函數,這個函數會依次作用於序列的每一個元素,所得的結果將作爲排序的關鍵字(sorting key),key 默認是 None,即恆等函數(identity function),也就是默認用元素自己的值排序。reverse 默認是 False,即升序,設定爲 True 即爲降序排列。

不同點: list.sort 是列表方法,對列表進行原地(in place)排序,返回值是 None。sorted 是內置函數,可以對所有可迭代對象進行排序,並將排序結果作爲一個新的列表返回。

注:Python API 的一個約定是如果一個函數或者方法是原地改變對象,那麼應該返回 None。這麼做的目的是爲了告訴調用者對象被原地改變了。這個約定的弊端是無法級聯調用(cascade call)這些方法。而返回新對象的方法可以級聯調用,從而形參連貫的接口(fluent interface)。

# list.sort 和 sorted 舉例
fruits = ['gape', 'raspberry', 'apple', 'banana']
print('返回一個新的按字母排序的列表')
print(sorted(fruits))
print('原列表不變')
print(fruits)
print('返回一個新的按字母降序排列的列表')
print(sorted(fruits, reverse=True))
print('返回一個新的按長度排序的列表,,注意這裏是穩定排序算法實現,即grape 和 apple 長度相同,所以它們的相對位置和原來列表裏一樣')
print(sorted(fruits, key=len))
print('返回一個新的按長度降序排序的列表,,注意這裏是穩定排序算法實現,即 grape 和 apple 長度相同,所以它們的相對位置和原來列表裏一樣')
print(sorted(fruits, key=len, reverse=True))
print('原列表 fruits 一直不變')
print(fruits)
print('對原列表進行原地排序')
print(fruits.sort())  # 這裏打印 None 返回值
print('打印原地排序後的列表')
print(fruits)
返回一個新的按字母排序的列表
['apple', 'banana', 'gape', 'raspberry']
原列表不變
['gape', 'raspberry', 'apple', 'banana']
返回一個新的按字母降序排列的列表
['raspberry', 'gape', 'banana', 'apple']
返回一個新的按長度排序的列表,,注意這裏是穩定排序算法實現,即grape 和 apple 長度相同,所以它們的相對位置和原來列表裏一樣
['gape', 'apple', 'banana', 'raspberry']
返回一個新的按長度降序排序的列表,,注意這裏是穩定排序算法實現,即 grape 和 apple 長度相同,所以它們的相對位置和原來列表裏一樣
['raspberry', 'banana', 'apple', 'gape']
原列表 fruits 一直不變
['gape', 'raspberry', 'apple', 'banana']
對原列表進行原地排序
None
打印原地排序後的列表
['apple', 'banana', 'gape', 'raspberry']

2.8 用 bisect 管理已排序的序列

bisect 模塊中的 bisect 和 insort 都是利用二分查找算法來在有序序列中查找或插入元素。

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 = '{0:2d} @ {1:2d}    {2}{0:<2d}'

def demo(bisect_fn):
    for needle in reversed(NEEDLES):
        # 用特定的 bisect 函數來計算元素應該出現的位置
        position = bisect_fn(HAYSTACK, needle)
        # 利用該位置來算出需要幾個分隔符
        offset = position * '  |'
        # 把元素和其應該出現的位置打印出來
#         print(ROW_FMT.format(needle, position, offset))
        print(f'{needle:2d} @ {position:2d}    {offset} {needle:<2d}')

if __name__ == '__main__':

    # 根據命令的最後一個參數來選用 bisect 函數
    if sys.argv[-1] == 'left':
        bisect_fn = bisect.bisect_left
    else:
        bisect_fn = bisect.bisect
    # 把選定的函數擡頭打印出來
    print('DEMO:', bisect_fn.__name__)
    print('haystack ->', ' ' .join('%2d' % n for n in HAYSTACK))
    demo(bisect_fn)
DEMO: bisect_right
haystack ->  1  4  5  6  8 12 15 20 21 23 23 26 29 30
31 @ 14      |  |  |  |  |  |  |  |  |  |  |  |  |  | 31
30 @ 14      |  |  |  |  |  |  |  |  |  |  |  |  |  | 30
29 @ 13      |  |  |  |  |  |  |  |  |  |  |  |  | 29
23 @ 11      |  |  |  |  |  |  |  |  |  |  | 23
22 @  9      |  |  |  |  |  |  |  |  | 22
10 @  5      |  |  |  |  | 10
 8 @  5      |  |  |  |  | 8
 5 @  3      |  |  | 5
 2 @  1      | 2
 1 @  1      | 1
 0 @  0     0

上面的輸出,每一行都以 needle @ position (元素及其應該插入的位置)開始,並展示了該元素在原序列中的物理位置。

排序的代價很高,當我們得到一個有序序列,最好能夠保持它的有序。
bisect.insort 方法就是用於插入新元素且使序列保持有序。

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)
    print(f'{new_item:2d} -> {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]

2.9 當列表不是首選時

列表非常靈活強大且簡單。有時要考慮效率的話,列表可能不是更好的選擇。比如存放 100 萬個浮點數,數組(array)的效率要高很多,因爲數組背後存的並不是 float 對象,而是數字的機器翻譯,也就是字節表述,這和 C 語言中的數組一樣。

  1. 如果需要頻繁對序列做先進先出(FIFO)或者後進先出(LIFO)操作,雙端隊列(double-ended queue)deque 會更快。collection.deque 類是一個線程安全、可以快速從兩端添加或者刪除元素的數據類型。存放“最近用到的幾個元素”deque 也非常有用。
  2. 對成員進行測試時,set 更合適。
  3. 如果只需要一個存儲數字的列表,array.array 比 list 更加高效。
# Creating, saving, and loading a large array of floats
from array import array  # 導入 array 類型
from random import random

# 利用一個可迭代對象(這裏是一個生成器表達式)創建一個雙精度浮點數組(類型碼'd')
floats = array('d', (random() for i in range(10**7)))
print(floats[-1])  # 打印數組最後一個元素
fp = open('floats.bin', 'wb')
# 把數組存入一個二進制文件
floats.tofile(fp)
fp.close()
floats2 = array('d')  # 新建一個雙精度浮點空數組
fp = open('floats.bin', 'rb')
floats2.fromfile(fp, 10**7)
fp.close()
print(floats2[-1])  # 打印數組最後一個元素
print(floats2 == floats)  # 比較 floats2 與 floats 是否相等
0.051056611520245765
0.051056611520245765
True

二進制文件比文本文件的讀取和存儲都更加高效。

collection.deque 類是一個線程安全、可以快速從兩端添加或者刪除元素的數據類型。存放“最近用到的幾個元素”deque 也非常有用。deque 對頭尾部的操作進行了優化, 它的代價是對隊列中的中間元素進行操作會慢一些。

deque 中 append 和 popleft 都是原子操作,可以在多線程中安全地當做 LIFO 隊列使用,而不用使用鎖。

from collections import deque

# maxlen 是一個可選參數,代表這個隊列可以容納的元素數量,而且一旦設定,這個屬性就不能修改了。
dq = deque(range(10), maxlen=10)
print(dq)
# 隊列的旋轉操作,接受一個參數 n,當 n > 0時,隊列的最右邊的 n 個元素會被的移動到隊列的左邊。
# 當 n < 0 時,最左邊的 n 個元素會被移動到右邊。
dq.rotate(3)
print(dq)
dq.rotate(-4)
print(dq)
# 在隊列的左端條件元素-1,因爲此時隊列已滿(len(d) == d.maxlen())
# 因此在它的頭部添加元素時,它的尾部元素會被刪除。
# 因此下一行中,元素0被刪除了
dq.appendleft(-1)
print(dq)
# dq 隊列尾部添加3個元素,同時會把dq隊列頭部的-1, 1 和 2 刪除
dq.extend([11, 22, 33])
print(dq)
# 在dq隊列頭部添加 4 個元素,注意extendleft(iter)是把iter裏面的元素依次添加到dq隊列頭部
# 所以iter中的元素會逆序出現在dq隊列,同時dq隊列尾部的4個元素被刪除
dq.extendleft([10, 20, 30, 40])
print(dq)
deque([0, 1, 2, 3, 4, 5, 6, 7, 8, 9], maxlen=10)
deque([7, 8, 9, 0, 1, 2, 3, 4, 5, 6], maxlen=10)
deque([1, 2, 3, 4, 5, 6, 7, 8, 9, 0], maxlen=10)
deque([-1, 1, 2, 3, 4, 5, 6, 7, 8, 9], maxlen=10)
deque([3, 4, 5, 6, 7, 8, 9, 11, 22, 33], maxlen=10)
deque([40, 30, 20, 10, 3, 4, 5, 6, 7, 8], maxlen=10)

2.10 本章小結

  1. 序列可以分爲可變和不可變序列,或者扁平序列和容器序列。扁平序列體積小、速度快、用起來更簡單,但是隻能保存原子性數據,如:數字、字符和字節。容器序列更加靈活,但是要注意容器序列中包含可變對象時需要小心。
  2. 列表推導式和生成器表達式對於創建和初始化序列非常強大。一定要學會,用上了會上癮。
  3. 元組可以作爲無名稱字段的記錄,也可以作爲不可變的列表。作爲記錄時,元組拆包非常安全可靠,* 在元組拆包非常有用。
  4. 序列切片非常強大,也是 Python 最後歡迎的一個特性。多維切片和省略,已經利用切片賦值改變可變序列很多時候都被忽略了。
  5. 重複拼接,+=*= 在處理可變序列和不可變序列時是不同的,對於可變序列是原地拼接,對於不可變序列是生成新的序列。本質上取決於序列本身對特殊方法的實現。
  6. sort 方法和內置函數 sorted 都使用簡單且靈活,因爲它們提供了一個可選的僅限關鍵字參數 key,用於指定一個僅有一個參數的函數,這個函數將用於對序列的每一個元素處理,並把處理結果作爲排序算法比較對象。對於一個排好序的序列,爲保持有序狀態,可以使用 bisect.insort 來插入元素,使用 bisect.bisect 來快速查找元素(二分查找算法)。
  7. collection.deque 靈活強大,線程安全,對於頻繁進行頭尾操作或者存放最近用的今個元素非常有用。不適合對中間元素頻繁操作的場景。

2.11 擴展閱讀

  1. Python 官網對於內置函數 sorted 和 list.sort 方法的更多高級用法,推薦閱讀:Sorting HOW TO

  2. Python 大多數排序(sort 方法和 sorted 函數)用的都是 Timsort 排序算法,它是一種混和的穩定排序算法,它綜合了歸併排序(merge sort)和插入排序(insertion sort)。可以在很多現實場景中都表現很好。具體介紹可以參考 Wiki Timsort

  3. 更好的使用元組拆包平行賦值,可以參考:使用 *extra 語法的權威指南:PEP 3132

  4. 更廣泛的使用可迭代對象拆包的討論和提議,可以參考 PEP 484:PEP 448 – Additional Unpacking Generalizations

  5. 關於 collections 的更多用法 collections — Container datatypes

  6. 關於爲什麼 range 和 slice 取下限,不取上限,可以看看這篇經典的文章:Why numbering should start at zero
    這篇文章主要內容是:
    爲什麼 range 和 slice 取下限,不取上限?
    表示 2, 3, …, 12 序列,可以用以下四種表示。

a) 2 ≤ i < 13
b) 1 < i ≤ 12
c) 2 ≤ i ≤ 12
d) 1 < i < 13

  1. 因爲上限和下限的差值就是所取序列的長度。所以 a 和 b 優於 c 和 d。
  2. 下限使用 < 以及上限使用 < 都比較醜陋。因此我們下限使用 ,上限使用 <。所以 a 優於 b。
  3. Mesa 編程語言對所有四個約定中的整數間隔都有特殊的表示法。 Mesa 的廣泛經驗表明,使用其他三個約定一直是笨拙和錯誤的來源,鑑於此經驗,強烈建議 Mesa 程序員不要使用後三個可用功能。

由此引出第二個問題,爲什麼下標從 0 開始?

  1. 從 0 開始索引,那麼通過下標,我們就可以知道有多少個元素在這個元素前面。比如:l[1],我們就知道這是取的第 2 個元素,它前面有 1 個元素。
  2. 編程語言慣例。

2.12 Luciano 的一些雜談

  1. 列表雖然可以存放不同類型的數據,但是通常不這麼做,因爲這麼做並沒有什麼好處,而且列表中的元素如果不能比較大小,則無法對列表進行排序。列表通常存放具有通用特性的元素。
  2. 元組恰好相反,元組常用來存放不同數據類型,因爲元組的每個元素都是獨立的,彼此不相關。
  3. list.sort、sorted、max 和 min 中可選參數 key 的設計非常棒!使用 key 會更加高效且簡潔。簡單是指你只需要定義一個只有一個參數的函數,用於排序即可。高效是指 key 的函數在每個元素上只會調用一次,而雙參數比較函數則每次兩兩比較的時候都會被調用。(PS:這一點還不是特別理解)
  4. sorted 和 list.sort 背後的排序算法都是 Timsort,它是一種自適應算法,會根據原始數據的特點交替使用歸併排序(merge sort)和插入排序(insertion sort)。在實際應用場景中這是非常有效的一種穩定排序算法。Timsort 在 2002 年開始在 CPython 中使用,2009 年起,開始在 Java 和 Android 中使用。

巨人的肩膀

  1. 《Fluent Python》
  2. 《流暢的 Python》
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章