[python] [VI coding] 第八章 字串 - 教學區

[python] [VI coding] 第八章 字串

文章瀏覽次數 3642

特種兵

特種兵圖像(預設)

2020-11-30 13:22:34

From:211.23.21.202

第八章 字串

8-0 notepad++ 的區塊處理

在上一章介紹完 notepad++ 的設定後,再來看一下 notepad++ 對於區塊的處理有何特殊之處。

撰寫程式的過程中,經常會出現需要區塊選取複製的狀況,也就是直向的選取,而不是選取一整行,快速鍵為 alt+shift+方向鍵,這樣就可以做區塊的選取。

當選取整個區塊後,還可以對這個區塊進行縮排或減少縮排,這也非常方便。尤其是對於 python 這種注重排版的程式語言,有時想要整個區塊都往後再縮一層或者全部往前一層,都是很常見的情況。

選取區塊後,往右內縮一層為 tab 鍵,往左退一層則為 shift+tab 鍵,這樣就不需要一行一行的按了。

補充:

  1. 在 alt 功能表中 -> 編輯 -> 縮排內有這兩個項目,可以用選的。
  2. 在 alt 功能表中 -> 編輯 -> 空格處理內有對於空格與 tab 符號的轉換或處理,也相當實用。
    • 像是把整個檔案的 tab 符號換成空格,或者空格換成 tab 符號,還可以去除行尾多餘的空格等等,功能很強大且完整。

8-1 前言

字串和其他三種形態不同(整數、浮點數與布林),它是其他形態的有序集合。

雖然之前已經開始使用字串,但其實還沒有很了解它們的詳細操作。

在本章中,我們將學習操作字串的方法,也可以讓你更了解字串這個資料型態的應用。

8-2 字串是一個序列

什麼是字串?字串就是一串字元,也就是把字元串起來的意思。

字串是一個有序的字元集合,你可以使用 [ ] (中括號) ,一次只取得一個字元:

>>> fruit = 'banana'
>>> letter = fruit[1]
>>> letter 
'a'

上例第二個敘述中,使用中括號取得 fruit 字串內容的 1 號字元,給 letter 變數。中括號內的數字稱為 index ,也就是索引,這個數字代表有序字串的第幾個字元。但請注意上例的執行結果,你的期望可能與 python 不同:

以非程式角度來看,多數人認為 banana 中的第一個字元是 b 而不是 a。但對電腦科學家不是這樣,因為他們認為字串的第一個字元是從 0 開始編號的,所以:

>>> letter = fruit[0]
>>> letter
'b'

第一個字元 b 的 index 是 0 號才對,那 a 當然就是 index = 1 了。

至於 index 索引到底要從 0 還是 1 開始,各有支持的專家,不同的語言也有所不同。至少先記住 python 的 index 是從 0 開始就好,而索引也可以是變數或其他運算式:

>>> i = 1
>>> fruit[i]
'a'
>>> fruit[i+1]
'n'

如果是數字的索引,索引的值一定要是整數,否則會發生錯誤。

>>> letter = fruit[1.5]
TypeError: string indices must be integers
>>> i
1

順便提醒,雖然我們在中括號裡執行 i + 1 這個算式,但實際上 i 的值沒有被改變,因為並未使用指派運算子重新賦值給 i 這個變數。

所以只能說 i + 1 這個運算式的結果是 2, 但不能說 i = 2,因為 i 還是 1。索引也可以是負整數,這部分在下一節介紹。

8-3 使用 len 函數獲得字串長度

在之前的練習中,我們已經看過這個標準的內建函數 len 了,給它一個字串作為參數,它會回傳一個整數,代表這個字串的長度。

>>> fruit = 'banana'
>>> len(fruit)
6

如果想獲得字串的最後一個字母,你可能會這樣做:

>>> length = len(fruit)
>>> last = fruit[length]
IndexError: string index out of range

因為 banana 雖然有 6 個字元,但 index 從 0 開始算起,所以最後一個字母的 index 是 5 才對。

上面的錯誤訊息是說,我們的 index 超過範圍,把總長度減掉一就可以了。

>>> last = fruit[length - 1]
>>> last
'a'

或者可以直接使用負數的 index 值, -1 代表最後一個字元, -2 代表倒數第二個字元,以此類推。

所以 index 為正,表示從字串左邊開始往右數;反之為負,則是從右邊開始往左數取值。

>>> s = 'string'
>>> s[-1]
'g'

8-4 使用 for 迴圈遍歷字串

上一章我們介紹過迭代,這裡來介紹遍歷。

很多字串的運算,都是依序一次只處理字串的一個字元。通常從字串的最左邊開始處理,一次只選擇一個字元做事,接著換下一個字元,然後再做同樣的事情。

以上不斷地循環,直到整個字串的結束,這個過程就稱為 遍歷。要操作遍歷的其中一種方法是使用 while 迴圈,我們的範例是一次印一個字母,直到全部印完:

index = 0
while index < len(fruit):
	letter = fruit[index]
	print(letter)
	index = index + 1

這個遍歷的範例是每一行印出 fruit 字串中的一個字元,直到整個字串結束。index 從 0 開始,只要小於 fruit 的字串長度就進入迴圈,每次在迴圈末尾累加 index 的值並更新它。

而當 index 的值等於 fruit 的字串長度時,條件為 False,不再進入迴圈(小於才能讓條件為真)。

剛才我們已經向各位說明,如果 fruit 的長度是 6 ,那麼它最後一個字元的 index 其實是 5, 因此上例並沒有遺漏任何字串中的字元。也就是說,一個字串最後一個字元的 index ,等於該字串的長度減一。

while 迴圈在上一個章節已經介紹過了,這邊我們做了個小小複習而已。另外一個方法是使用 for 迴圈:

for letter in fruit:
	print(letter)

每次迴圈執行, fruit 字串都會由左而右一次給予變數 letter 一個字元,直到整個字串結束。以程式碼的角度來看,while 在這個應用上比較複雜。

這個例子使用 for 迴圈來撰寫,會顯得簡潔很多。在不同的時機選擇更好的方式,需要長時間練習跟累積,才能獲得經驗。

接下來這個例子,是使用字串連接的方式組合成單字。這些單字的特性是,除了第一個字元外,其他字母都相同,第一個字元從 J 開始到 Q:

範例 8-1
prefixes = 'JKLMNOPQ'
suffix = 'ack'
for letter in prefixes:
	print(letter + suffix)

它的顯示如下:

Jack
Kack
Lack
Mack
Nack
Oack
Pack
Qack

8-5 字串切片

字串的分割稱為字串切片。選擇字串的片段跟選擇字元的方式很類似,只是指定索引範圍。

>>> s = 'Monty Python'
>>> s[0:5]
'Monty'
>>> s[6:12]
'Python'

[n:m] 這個運算子,表示從 index n 開始到 index m-1 這一段字串切片,乍看之下有點不直觀,但後來你會漸漸發現,這樣的設計有利於使用 index 的方式來取得字串切片。

另外,也要提醒,m 代表的不是取得的個數,這是很多人一開始容易犯的錯,它代表的是 index m-1 號字元。

省略冒號前面的數字表示從 index 0 開始,省略冒號右邊的數字表示取到最後一個字元為止。

>>> fruit = 'banana'
>>> fruit[:3]
'ban'
>>> fruit[3:]
'ana'

如果冒號左邊的 index 大於或等於右邊的 index ,則會返回一個空字串,用一對空引號表示:

>>> fruit = 'banana'
>>> fruit[3:3]
''

空字串是一個沒有任何字元且長度為 0 的字串,但其他性質都與一般的字串相同。

想想看,[:] 代表什麼?答案是代表整個字串,不過用索引取到的字串算是複製品,這個等下再來解釋。

使用中括號的 index 來處理字串或之後會提到的「串列(列表)」,最重要的是不要把 index 跟我們平常習慣說的「第幾個」搞混就好。

8-6 字串是不可變的

想要使用 [:] 這種運算子來變更字串的內容,是一個很誘人的想法,像這樣:

>>> greeting = 'Hello, world!'
>>> greeting[0] = 'J'
TypeError: 'str' object does not support item assignment

上面的錯誤訊息是說,字串物件不支援等號來賦值。字串是一個物件,而字元是字串物件中的元素,物件則是定值的概念。

例如 a = 26, 26 是一個數值,可以讓 a 加上 30 變成 56 ,也可以將 a 指向別的值,但不能直接將 26 的 2 改成5 ,變成 56 這個數值,因為它是一個不可分割的整體。

字串是一個不可變的整體,不能去改變已經存在的字串。

如果有這個需求,最好的方法是重新建立一個新的字串,而新字串是原本字串的變體。

>>> greeting = 'Hello, world!'
>>> new_greeting = 'J' + greeting[1:]
>>> new_greeting
'Jello, world!'

這個例子將字元 'J' 與 greeting 字串切片組合成一個新的字串,並沒有去變更原來的字串。

8-7 搜尋

來看看以下這個函數做了什麼:

範例 8-2
def find(word, letter):
	index = 0
	while index < len(word):
		if word[index] == letter:
			return index
		index = index + 1
	return -1

find 函數返回 letter 字元第一次出現在 word 字串的 index 值,若在字串中沒找到該字元,就返回 -1 這個值。這種遍歷整個序列尋找的行為就稱為 搜尋

再次提醒,return 敘述之後的程式碼不會再被執行,就算是在迴圈內,也會立刻停止程式或函數。

8-8 迴圈與計算次數

底下這個程式計算 a 字母在字串中出現的次數,像這樣的計數程式,也被稱為 計數器

範例 8-3
word = 'banana'
count = 0
for letter in word:
	if letter == 'a':
		count = count + 1
print(count)

在程式中,將 count 變數的初始值設定為 0,而當字串中出現 a 字母時,就將 count 的值累加一。離開迴圈後,程式結束前將 count 變數印出來,這也就是 'a' 出現的總數。

8-9 字串方法

字串有一些內建的方法,可以讓我們更方便地處理它。方法就像函數,可以擁有參數與回傳值,但使用的語法不同。

如果是函數的呼叫像這樣 upper(word), 而使用方法的語法來呼叫像這樣 word.upper(), 方法的使用範例如下:

>>> word = 'banana'
>>> new_word = word.upper()
>>> new_word
'BANANA'

使用 . (小數點) 來調用方法,小數點左邊是字串,右邊是方法的名稱,我們會說調用了 word 字串的 upper 方法。空小括號表示沒有帶入參數。

在 python 中有一個名為 find 的方法,功能就像剛剛我們撰寫的同名函數那樣,以下是調用這個方法的範例:

>>> word = 'banana'
>>> index = word.find('a')
>>> index
1

事實上, find 這個方法功能比之前我們自己寫的函數強,它可以搜尋字串,而不僅僅只是字元。

>>> word.find('na')
2

按照預設值,find 方法會從字串的第一個字元開始查找,你可以加入第二個參數,讓它從我們指定的 index 開始搜尋:

>>> word.find('na', 3)
4

雖然指定了從 index 3 開始找,但無論如何,index的值是絕對的,不會因為我們從哪個 index 開始找而有所不同。如果指定的 index 比原本要找的字串還後面,就會發生找不到的情形。

像 find 的第二個參數,這種非必要的參數稱為 可選參數。事實上,它還有第二個可選參數,也就是可以指定結束搜尋的範圍值。

>>> name = 'ben'
>>> name.find('b', 1, 2)
-1

搜尋失敗的原因是指定的範圍不存在 b 這個字母,思考一下上面的運算子 [n:m] 就可以了解。

因為 index 從 1 開始是 e ,而結束的地方是 2-1 ,也就是 index = 1, 因此找不到 b 這個字母,這與字串切片運算完全一致。

關於方法的介紹,會在之後的類別章節再詳述。

8-10 in 運算子

in 這個布林運算子的左、右運算元皆為字串,如果右邊的字串包含左邊的字串就回傳 True ,否則回傳 False,所以 in 運算子也可以拿來搜尋字串,通常用於確認搜尋結果,如下:

>>> 'a' in 'banana'
True
>>> 'seed' in 'banana'
False

下面這個範例遍歷了 word1 的每個字元,並判斷,如果字元也出現在 word2 中,就以一行一個的方式印出來。

def inBoth(word1, word2):
	for letter in word1:
		if letter in word2:
			print(letter)

在 for 中的 in 與 if 中的 in 是不同的, for 裡的是語法,而 if 裡的是運算子。

python 有時直接使用英文的語法理解或閱讀,對於了解其程式碼意義很有幫助。比較精通英文文法的人可以試看看,尤其變數名稱使用正確,就更明顯了,我們可以這樣讀上面的例子:

「對於 (每個) 在 (第一個) 字中的字母,如果 (該) 字母 (出現) 在 (第二個) 字中,則印出 (該) 字母」。

接下來是以上面的函數帶入 apples 與 orange 的範例:

>>> inBoth('apples', 'oranges')
a
e
s

8-11 字串比較

相關的比較運算子也可以在字串上運作,例如 == 比較運算子:

if word == 'banana':
	print('太棒了!相等')

其他的比較運算子,對於字母順序前後關係的比較很有用:

if word < 'banana':
	print('這個字 ' + word + ' 小於 banana')
elif word > 'banana':
	print('這個字 ' + word + ' 大於 banana')
else:
	print('太棒了!相等')

python 不像人們認知的大、小寫方式處理它們,對 python 來說,大寫永遠在小寫的順序之前,所以:

'Pineapple' 這個字的順序會被認定在 'banana' 之前。順序的原理是 ascii 碼,簡單地說,就是每個字元在電腦中都有一個編碼,編碼愈前面的,順序就越前面。比較好的解決方案,是在比較前先把字串轉成同一種格式,像是全大寫或全小寫,再來比較。

另外,也要提一下,使用小於和大於的符號做字串比較的原理,之後的章節會詳述,這邊先簡單列舉:

  • 兩個字串比較,同樣 index 的字元相比,只要 False 就停止
  • 兩個字串比較,如果一直是 True ,會以後面的字串長度為長度來比

最後提醒一下,範例中 word 是一個變數,需要有初始值程式才能運作,例如使用 input 函數讓使用者輸入。

除錯

當你使用 index 遍歷序列時,有時想要正確地指定開始與結束點,可能會是一個困難。

下面這個函數比較兩個字串,如果這兩個字串互為顛倒,就回傳 True,但是它包含了兩個錯誤:

def isReverse(word1, word2):
	if len(word1) != len(word2):
		return False
	i = 0
	j = len(word2)
	while j > 0:
		if word1[i] != word2[j]:
			return False
		i = i+1
		j = j-1
	return True

第一個判斷式,先比較兩個字串的長度,如果不同就回傳 False。

因此,接下來在函數中執行的過程,我們都可以假設這兩個字串的長度是相等的。

i 和 j 都是 index, i 是 word1 的索引,方向是往右遞增;j 是 word2 的索引,方向是往左遞減。

一次比較一個字元,如果兩個字串的字元不同時,則回傳 False。

假如完成了整個迴圈,就表示這兩個字串符合顛倒的規則,並且回傳 True。

現在,使用 'pots' 與 'stop' 兩個字串來測試這個函數,理論上應該得到 True, 然而卻得到一個 IndexError 錯誤:

>>> isReverse('pots', 'stop')
...
File "reverse.py", line 15, in isReverse
if word1[i] != word2[j]:
IndexError: string index out of range

錯誤訊息說我們的 index 超出範圍,為了抓出錯誤,可以在錯誤行出現前,印出 index 來觀察看看,所以加了第 4 行:

...
i = 0
j = len(word2)
while j > 0:
	print(i, j)  # 在這裡印出 i 與 j 的值來觀察 
	if word1[i] != word2[j]:
		return False
	i = i+1
	j = j-1
...

現在再一次執行程式,這將讓我們得到更多的資訊,以釐清問題:

>>> isReverse('pots', 'stop')
0 4
...
IndexError: string index out of range

檢查錯誤的方法,就是每一圈 while 都印出兩個 index 的值來觀察。

第一圈 while ,我們發現 j 是 4,但 stop 的最後一個字母的 index 應該是 3 才對。所以取得字串長度後,應該減一才能當作最後一個字母的 index 值。

我們修正了第一個錯誤,再執行一次看看:

>>> isReverse('pots', 'stop')
0 3
1 2
2 1
True

這次我們得到了正確答案 True, 但看起來迴圈只跑了 3 次,這有點令人疑惑。

如果每一個字元都要比對到,那應該要進行 4 圈。從上面印出的 i 與 j 可以發現,當 i 是 3 而 j 是 0 的情況下,迴圈並沒有進入。

從 while 迴圈的條件可以先排除 i 的問題,因為它並沒有被拿來當作進入迴圈的條件,所以凶手不是 i。

當 j 遞減至 0 時, 0 等於 0 ,也就是 0 不會大於 0 ,所以進入迴圈的條件為 False,因此也就少跑了最後一圈。

我們將迴圈的進入條件改成 j >= 0 ,也就是當 j 是負數,才不會進入迴圈,解決了第二個問題。

請自行完成這個完整版的函數,並再次測試,以確保完全沒有問題。

動動腦

  1. 寫一個函數 reverse, 將字串倒過來顯示,一行一個字元,並寫一個程式來驗證它。

版本 (1)

def reverse(s):
	length = len(s) - 1
	for i in range(length, -1, -1):
		print(s[i])

str = input('請輸入字串:')
reverse(str)

版本 (2)

def reverse(s):
	for i in s[::-1]:
		print(i)

str = input('請輸入字串:')
reverse(str)
  1. 上面的 範例 8-1 中,如果 oack 與 qack 是拼字錯誤,應該顯示 ouack 與 quack ,那怎麼改寫這個程式?
prefixes = 'JKLMNOPQ'
suffix = 'ack'
new_suffix = 'uck'

for letter in prefixes:
	if letter == 'O' or letter == 'Q':
		print(letter + new_suffix)
	else:
		print(letter + suffix)
  1. 參考 範例 8-2 改寫 find 函數,接受第三個參數,目的是讓搜尋的動作從該參數指定的起始位置開始進行。
def find(word, letter, n):
	index = n - 1
	while index < len(word):
		if word[index] == letter:
			return index
		index = index + 1
	return -1

print(find('banana', 'a', 5))
  1. 參考 範例 8-3 改寫計算 a 字母次數的程式,將這個功能封裝在一個名為 count 的函數當中。函數接受兩個參數,一個是字串,一個是字元。
def count(w, l):
	count = 0
	for i in w:
		if i == l:
			count = count + 1
	print(count)

word = 'banana'
letter = 'a'
count(word, letter)
  1. 將上個練習的函數改成如同第 3 題那樣,有三個參數的版本。
def count(w, l, n):
	count = 0
	for i in range(n - 1, len(w)):
		if w[i] == l:
			count = count + 1
	print(count)

word = 'banana'
letter = 'a'
count(word, letter, 3)
  1. 將 除錯 章節中的函數改成正確的版本,並使用 abc 與 cba, 今天心情很好 與 好很情心天今 來測試它。
def isReverse(word1, word2):
	if len(word1) != len(word2):
		return False
	i = 0
	j = len(word2) - 1
	while j >= 0:
		if word1[i] != word2[j]:
			return False
		i = i+1
		j = j-1
	return True

s = '今天心情很好'
t = '好很情心天今'
print(isReverse(s, t))

練習

第1題 檔名 8-1.py

閱讀這份文件中的 [String Methods][https://docs.python.org/3/library/stdtypes.html#string-methods] 部分,看看你能不能夠找到一些有用的方法。其中,strip 和 replace 這兩個方法很實用。

也確認一下裡面說明語法的部分是否看得懂。例如 str.find(sub[, start[, end]]) 代表什麼?

find 是一個字串方法,必要的參數是 sub ,而 [ ] 代表可選參數。

可選參數有 start 與 end ,排序第二的參數是 start。 如果有 start, 也不一定要有 end 參數。

寫一支程式來調用 strip 與 replace 這兩個方法,以確認你看得懂它的說明,也可以自己搜尋其他相關的資料來閱讀。

第2題 檔名 8-2.py

有一個字串方法名為 count, 它的功能類似 8-8 迴圈與計算次數 的同名函數。

請查詢這個方法應該怎麼使用,寫一支程式調用它來計算 a 字母出現在 banana 字串中的次數。

第3題 檔名 8-3.py

其實字串切片還可以有第三個參數,表示取得字元的 間距,直接看範例:

>>> fruit = 'banana'
>>> fruit[0:5:2]
'bnn'

如果第三個 index 是 -1 ,就表示字串切片的內容會反轉顯示。

寫一個函數名為 isPalindrome ,功能是判斷該字串參數是不是一個 迴文

先歸納與觀察一下迴文的規則,再思考函數應該怎麼寫。

(迴文是一種修辭方式,正讀或反讀讀出的語句皆能通順,甚至內容相同,也可以發展成文字遊戲。除了矩陣,亦能將文字排列成圓圈狀)

例如:上海自來水來自海上,或者英文的 noon, redivider 等,本題用字或句子必須是有意義且完全相同才行。

第4題 檔名 8-4.py

以下有幾個函數,它們的功能都是判斷一個字串是否為全部小寫。但有一些函數可能存在瑕疵,請對每個函數加以說明。

(假設傳入每個函數的參數都是一個字串)

(1)

def any_lowercase1(s):
	for c in s:
		if c.islower():
			return True
		else:
			return False

(2)

def any_lowercase2(s):
	for c in s:
		if 'c'.islower():
			return 'True'
		else:
			return 'False'

(3)

def any_lowercase3(s):
	for c in s:
		flag = c.islower()
	return flag

(4)

def any_lowercase4(s):
	flag = False
	for c in s:
		flag = flag or c.islower()
	return flag

(5)

def any_lowercase5(s):
	for c in s:
		if not c.islower():
			return False
	return True

如果覺得它們都有瑕疵,那請寫出一個正確的版本吧。

第5題 檔名 8-5.py

有一種編碼方式叫作 凱薩密碼 (Caesar cypher),這是一種古老的加密字母方式。

方法是將所有字母以指定數字順向或逆向旋轉,旋轉的數字就是解開加密的鑰匙,在這裡我們先把它稱為 key 好了。

例如 d 這個字母,如果 key 是 3 ,它就被加密為 g; 如果 key 是 -2 ,它就被加密為 b。

加密的過程是循環式的,例如字母 y 的 key是 4 ,就加密成 c; 字母 b 的 key 是 -2 ,就加密成 z。

通常加密是針對一整串的字,甚至整句或整篇文章,像 hal 就是 ibm 的 key 為 -1 來進行加密的結果。

請寫一個函數名為 rotateWord ,接受兩個參數,第一個是原來的字串,第二個是整數(正負) key,

功能就是進行上述的 凱薩密碼 加密動作,所以必須回傳加密過後的字串。

有兩個系統內建函數也許用得上:

  1. ord 接受一個字元並返回其 unicode 編碼,對字母來說 unicode 編碼就等於 ascii 碼。
  2. chr 與上述 ord 相反,接受一個 unicode 編碼,並返回該編碼所代表的字元。

因為字母的編碼是連續的,所以我們可以這樣使用:

>>> ord('c') - ord('a')
2

需要注意的是大小寫的編碼不同,也可以觀察一下標點符號或空格的加密。

參考資料

字串的索引 index

遍歷循環

迭代與遍歷

什麼是 ascii 碼

ascii 字碼表

凱薩密碼

數值與字串型態

影片

第八章 字串 part one

第八章 字串 part two

最後更新:2021-10-08 21:19:16

From: 111.249.165.250

By: 特種兵