為什麼程式要從0開始計數

goodspeed發表於2019-02-19

這一篇是《流暢的 python》讀書筆記。主要介紹元組、分片、序列賦值以及引用了大師 Edsger W.Dijkstra為什麼序列從0開始計數的解釋。

元組

在有些python 的介紹中,元組被稱為不可變列表,這其實是不準確的,沒有完全概括元組的特點。元組除了用作不可變列表,還可以用於沒有欄位名的記錄

元組和記錄

元組其實是對資料的記錄:元組中的每個元素都存放了記錄中一個欄位的資料,外加這個資料的位置。

如果把元組當作一些欄位的集合,數量和位置資訊會變得非常重要。比如以下幾條用元組表示的記錄:

 >>> lax_coordinates = (33.9425, -118.408056) # 洛杉磯國際機場的經緯度
 # 東京的一些資訊:市名、年份、人口、人口變化和麵積
 >>> city, year, pop, chg, area = ('Tokyo', 2003, 32450, 0.66, 8014)複製程式碼

以上這兩個元組每個位置都對應一個資料記錄。

元組拆包

>>> city, year, pop, chg, area = ('Tokyo', 2003, 32450, 0.66, 8014)複製程式碼

這個例子中,我們把元組的資料用一條語句分別賦值給 city, year, pop, chg, area,這就是元組拆包的一個具體應用。

元組拆包可以應用到任何可迭代物件上,但是被迭代的物件窄的元素的數量必須跟接受這些元素的元組的空檔數一致。

比如:

>>> lax_coordinates = (33.9425, -118.408056)
>>> latitude, longitude = lax_coordinates
>>> latitude
33.9425
>>> longitude
-118.408056複製程式碼

還可以用 * 運算子把一個可迭代物件拆開作為函式的引數:

>>> divmod(20, 8)
(2, 4)
>>> t = (20, 8)
>>> divmode(*t)
(2, 4)
>>> quotient, remainder = divmode(*t)
>>> quotient, remainder
(2, 4)複製程式碼

在進行拆包是,我們可能對元組的某些值並不感興趣,這時可以用 _ 佔位符處理。比如:

>>> divmode(20, 8)
(2, 4)
>>> _, remainder = divmode(20, 8)  # 這裡我們只關心第二個值
>>> remainder
4複製程式碼

在處理函式引數時,我們經常用*args 來表示不確定數量的引數。在python3中,這個概念被擴充套件到了平行賦值中:

# python 3 程式碼示例
>>> a, b, *rest = range(5)
>> a, b, rest
(0, 1, [2, 3, 4])
# * 字首只能用在一個變數名前,這個變數可以在其他位置
>>> a, *rest, c, d = range(5) 
>> a, rest, c, d
(0, [1, 2], 3, 4)
>>> a, b, *rest = range(2)
>> a, b, rest
(0, 1, [])複製程式碼

元組也支援巢狀拆包,比如:

>>> l = (1, 2, 3, (4, 5))
>>> a, b, c, (d, e) = l
>>> d
4
>>> 5
4複製程式碼

具名元組

元組作為記錄除了位置以外還少一個功能,那就是無法給欄位命名,namedtuple解決了這個問題。

namedtuple 使用方式例項:

>>> from collecitons import namedtuple
>>> city = namedtuple('City', 'name country population coordinates')
>>> tokyo = City('Tokyo', 'JP', 36.933, (35.689722, 139.691667))
>>> tokyo.population  # 可以使用欄位名獲取欄位資訊
36.933
>>> tokyo[1] # 也可以使用位置獲取欄位資訊
'JP'
>>> City._fields # _fields 屬性是一個包含這個類所有欄位名的元組 
('name', 'country', 'population', 'coordinates')
>>> tokyo_data = ('Tokyo', 'JP', 36.933, (35.689722, 139.691667))
>>> tokyo = City._make(tokyo_data) # _make() 方法接受一個可迭代物件生成這個類的例項,和 City(*tokyo_data) 作用一致
>>>  tokyo._asdict() # _asdict() 把具名元組以 collections.OrderedDict 的形式呈現
OrderedDict([('name', 'Tokyo'), ('country', 'JP'), ('population', 36.933), ('coordinates', (35.689722, 139.691667))])複製程式碼

collections.namedtuple 是一個工廠函式,它可以用來構建一個帶欄位名的元組和一個有名字的類。
namedtuple 構建的類的例項鎖消耗的記憶體和元組是一樣的,因為欄位名都被存放在對應的類裡。這個例項和普通的物件例項相比也更小一些,因為 在這個例項中,Python 不需要用 __dict__ 來存放這些例項的屬性

切片

Python 中列表、元組、字串都支援切片操作。

在切片和區間操作裡不包含區間範圍的最後一個元素是 Python 的風格。這樣做的好處如下:

  • 當只有最後一個位置資訊時,我們可以快速看出切片和區間裡有幾個元素:range(3) 和 mylist[:3] 都只返回三個元素
  • 當氣質位置可見時,可以快速計算出切片和區間的長度,用後一個數減去第一個下標(stop-start)即可。
  • 這樣還可以讓我們利用任意一個下標來把序列分割成不重複的兩部分,只要寫成 mylist[:x] 和 mylist[x:] 就可以。

切片除了開始和結束的下標之外還可以有第三個引數,比如:s[a:b:c],這裡 c 表示取值的間隔,c 還可以為負值,負值意味著反向取值。

>>> s = 'bicycle'
>>> s[::3]
'bye'
>>> s[::-1]
'elcycib'
>>> s[::2]
'eccb'複製程式碼

a:b:c 這種用法只能作為索引或者下標在[] 中返回一個切片物件:slice(a, b, c)。對 seq[start:stop:step] 進行求值的時候,Python 會呼叫 seq.getitem(slice(start:stop:step)]。

給切片賦值

如果把切片放在賦值語句的左邊,或者把它作為 del 操作的物件,我們就可以對序列進行嫁接、切除或修改操作,比如:

>>> l = list(range(10))
>>> l
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
>>> l[2:5] = [20, 30]
>>> l
[0, 1, 20, 30, 5, 6, 7, 8, 9]
>>> del l[5:7]
[0, 1, 20, 30, 5, 8, 9]
>>> l[3::2] = [11, 22]
>>> l
[0, 1, 20, 11, 5, 22, 9]
>>> l[2:5] = 100
Traceback (most recent call last):
      file "<stdin>", line 1 in <moduld>
TypeError: can only assign an iterable複製程式碼

如果賦值的物件是一個切片,那麼賦值語句的右側必須是一個可迭代物件。

給切片命名

如果程式碼中已經出現了大量的無法直視的硬編碼切片下標,可以使用給切片命名的方式清理程式碼。比如你有一段程式碼要從一個記錄字串中幾個固定位置提取出特定的資料欄位 比如檔案或類似格式 :

### 01234567890123456789012345678901234567890123456789012345678901234
record = '............100....513.25........'
cost = int(record[20:23]) * float(record[31:37])
# 這時,可以先給切片命名,以避免大量無法理解的硬編碼下標,使程式碼可讀性更強
SHARES= slice(20, 23)
PRICE = slice(31, 37)
cost = int(record[SHARES]) * float(record[PRICE])複製程式碼

slice() 函式建立了一個切片物件,可以被用在任何切片允許使用的地方,比如:

>>> items = [0, 1, 2, 3, 4, 5, 6]
>>> a = slice(2, 4)
>>> items[2:4]
[2, 3]
>>> items[a]
[2, 3]
>>> items[a] = [10, 11]
>>> items
[0, 1, 10, 11, 4, 5, 6]複製程式碼

如果你有一個切片物件 a,還可以呼叫 a.start, a.stop, a.step 來獲取更多資訊,比如:

>>> a = slice(5, 50, 2)
>>> a.start
5
>>> a.step
2複製程式碼

擴充套件閱讀 為什麼下標要從0開始

Python 裡的範圍(range)和切片都不會返回第二個下標所指的元素,電腦科學領域的大師 Edsger W.Dijkstra 在一個很短的備忘錄 Why numbering should start at zero 裡對這一慣例做了說明。以下是部分關鍵說明:

為了表示出自然數的子序列,2, 3, ... , 12,不使用省略記號那三個點號,我們可以選擇4種約定方式:

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

是否有什麼理由,使選擇其中一種約定比其它約定要好呢?是的,確實有理由。可以觀察到,a) 和 b)有個優點,上下邊界的相減得到的差,正好等於子序列的長度。另外,作為推論,下面觀察也成立:在 a),b)中,假如兩個子序列相鄰的話,其中一個序列的上界,就等於另一個序列的下界。但上面觀察,並不能讓我們從a), b)兩者中選出更好的一個。讓我們重新開始分析。

一定存在最小的自然數。假如像b)和d)那樣,子序列並不包括下界,那麼當子序列從最小的自然數開始算起的時候,會使得下界進入非自然數的區域。這就比較醜陋了。所以對於下界來說,我們更應該採用≤,正如a)或c)那樣。
現在考慮,假如子序列包括上界,那麼當子序列從最小的自然數開始算起,並且序列為空的時候,上界也會進入非自然數的區域。這也是醜陋的。所以,對於上界,我們更應該採用 <, 正如a)或b)那樣。因此我們得出結論,約定a)是更好的選擇。

  • 比如要表示 0, 1, 2, 3 如果用 b) d) 的方式,下界就要表示成 -1 < i
  • 如果一個空序列用 c) 其實是無法表示的,用 a) 則可以表示成 0 ≤ i < 0

總結

這一篇主要介紹元組、分片、序列賦值以及對為什麼序列從0開始計數做了摘錄。

參考連結


最後,感謝女朋友支援。

歡迎關注(April_Louisa) 請我喝芬達
歡迎關注
歡迎關注
請我喝芬達
請我喝芬達

相關文章