跳至主要内容

數字與文字

數字與文字

大部份的程式語言都有設計不同的資料型態,有簡單型態的也有複雜的集合型態,這個章節先來看看最常見的數字與文字。

數字

對一般人來說,數字就是數字,這是我們每天眼睛張開就要面對的東西,再熟悉不過了。大部份的程式語言都有數字可以用,在 Python 的數字主要有分兩種,一個是沒有帶小數點的「整數(Integer)」,另一個則是帶有小數點的,在程式語言的世界通常會稱它做「浮點數(Float)」。

你可能會好奇,有沒有帶小數點不一樣都是數字嗎?是的,的確都是數字沒錯,但不要以為只差在有沒有小數點而已,事實上它們在電腦的世界裡是以不同的型態存在的,我們可以使用內建函數 type() 來試試看:

>>> type(1450)
<class 'int'>

>>> type(1450.0)
<class 'float'>

雖然現在大家可能還看不懂什麼是 class,但大概可以把它解釋成 1450「是一種」整數(int)以及 1450.0「是一種」浮點數(float)的意思。關於整數跟浮點數的細節我們晚點再來看,先來看看大家小時候學過的四則運算:

四則運算

>>> 1 + 2
3

>>> 10 - 8
2

>>> 3 * 4
12

>>> 10 / 2
5.0

如果在這裡我還得解釋什麼是四則運算,未免也太侮辱大家的智商了。在計算過程中如果把整數跟浮點數混在一起算的話,計算出來的結果會自動轉變成浮點數:

>>> 1 + 2.0
3.0

>>> 10.0 - 8
2.0

>>> 3 * 4.0
12.0

就算計算的結果剛剛好是整數,得到的答案也是浮點數。另外,Python 使用除法算出來的結果,就算剛好整除,答案也是帶小數點的浮點數。如果你想要的是整數除法的話,在 Python 要使用兩個 /

>>> 10 // 2
5

>>> 10 // 3
3

但要注意用 // 算出來的答案不管有沒有整除,小數點部份都會直接被丟掉。我們以前學過的「先乘除、後加減,小括號先算」的規則也同樣適用在這裡:

>>> 1 + 2 * 3
7

>>> (1 + 2) * 3
9

那如果把數字跟...例如文字或是其他資料型態混在一起算呢?像 JavaScript 就可以把數字 1 跟文字 "1" 放在一起變成 "11",還能把數字跟陣列加在一起。但,這真的是個好主意嗎?你可能不會知道這種在 JavaScript 的強迫轉型(Type Coercion)的設計造成了多少不必要的麻煩。所以在 Python 如果你幹這件事,你會很直接的收到 Python 給你的抱怨:

>>> 1 + "1"
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: unsupported operand type(s) for +: 'int' and 'str'

看到了嗎?Python 沒這麼好講話,數字跟字串是不能放在一起做運算的。除了常見的四則運算外,還有幾個也算常用的數學運算,例如兩個星號 ** 是幾次方的意思:

# 3 的 2 次方
>>> 3 ** 2
9

# 5 的 3 次方
>>> 5 ** 3
125

使用 % 符號可以取餘數:

>>> 8 % 3
2

>>> 8 % 4
0

不只這些符號,Python 有內建一些還不錯用的數學計算函數,例如用來做四捨五入的 round()

# 四捨五入到整數位
>>> round(3.1415926)
3

# 四捨五入到小數點以下第 2 位
>>> round(3.1415926, 2)
3.14

如果沒帶第二個參數的話,就是計算到整數位。這個 round() 函數會算出離這個數字最近的整數,例如 0.4999 就會得到 0, 超過中間值一點點的 0.5001 就會得到 1

>>> round(0.4999)
0
>>> round(0.5001)
1

這四捨五入的概念我們小學都學過,沒什麼問題,但如果是剛好正中間的 0.5 呢?

>>> round(0.5)
0

咦?怎麼不是 1?這好像跟我們以前學的四捨五入不太一樣!

有個叫做「電機電子工程師學會(IEEE)」的單位,這個單位制定了許多工業界的標準,其中有一個編號 IEEE 754 號的標準定義了關於浮點數的表示法,也包括在計算浮點數的時候應該怎麼做捨入。

根據 IEEE 754 的規範,在做捨入計算的時候會計算出最接近的整數,但如果遇到中間值的時候,有兩種做法:

  1. 「ties to even」就是做捨入計算的時候,會選擇離這個數字最近的「偶數」。
  2. 「ties away from zero」跟上面不同,在做捨入計算的時候,會選擇離 0 比較遠的整數。

不同的程式語言的設計會有些不同,Python 選擇了第一種做法,這也是 IEEE 754 比較推薦的做法,而 Ruby 就是選第二種做法。我們先來看看第二種做法,什麼叫選擇「離 0 比較遠的整數」?我用 Ruby 舉個例子:

# 正數
>> (0.5).round
=> 1
>> (1.5).round
=> 2

# 負數
>> (-0.5).round
=> -1
>> (-1.5).round
=> -2

這比較接近我們平常講的「四捨五入」。而 Python 3 選擇了第一種做法,也就是選擇「比較近的偶數」:

# 正數
>>> round(0.5)
0
>>> round(1.5)
2

# 負數
>>> round(-0.5)
0
>>> round(-1.5)
-2

如果要給它個口訣的話,就是「四捨六入五成雙」,這種捨入法又稱「銀行家捨入法(Banker's Rounding)」。為什麼要這樣設計呢?這是因為這樣的設計可以減少四捨五入的時候造成的數字分佈偏差。什麼偏差?難道四捨五入不公平嗎?想想看,假設我有一顆九面骰子,上面分別刻了 1 到 9 的數字,如果丟出 1、2、3、4 算你贏,丟出 5、6、7、8、9 算我贏,這樣你覺得這樣公平嗎?

但為什麼我在上面特別提到 Python 3?這是因為在 Python 2 的 round() 是選擇第二種做法,就是我們小時候學的四捨五入!這也是 Python 2 跟 Python 3 不相容的原因之一。

另外,在 Python 的官方文件中有特別舉了一個例子:

# 你覺得答案是什麼?
>>> round(2.675, 2)

就我們前面學到的,應該會是 2.68 吧?但答案是卻是 2.67,這是因為在 IEEE 754 的規範中,浮點數的表示方式是有誤差的,其實我們肉眼看到的 2.675 並不是剛好在 2.672.68 的正中間,而是比較偏向 2.67 一點點,所以答案算出來是 2.67。關於浮點數的誤差待會就會知道是怎麼回事。

除了內建的 round() 之外,在 Python 有個專門放數學計算相關的 math 模組,裡面有無條件進位以及無條件捨去等函數。在 Python 要使用的時候,需要使用 import 語法明確的引入這個模組才能使用:

# 匯入 math 模組
>>> import math

# 圓周率
>>> math.pi
3.141592653589793

# 無條件進位
>>> math.ceil(3.14)
4

# 無條件捨去
>>> math.floor(3.14)
3

補充一個不太重要的冷知識,Ceil 的英文是「天花板」,而 Floor 是「地板」的意思。math.ceil()math.floor() 這兩個函數做的事情就是就是把數字往天花板跟地板方向推,往天花板推的就會變成「無條件進位」,而往地板推就是「無條件捨去」。

還記得在學生時期讓人很頭痛、學了也不知道以後要幹嘛的三角函數跟對數嗎?在 Python 也有:

# 三角函數
>>> math.sin(math.pi / 2)
1.0
>>> math.cos(math.pi)
-1.0

# 取 2 的對數
>>> math.log(8, 2)
3.0

更多關於 Python 的數學模組功能,可參閱官方文件說明。

網站連結

關於數字的最大值或最小值,有些程式語言在宣告整數或浮點數的時候,會根據不同型態而有不同的上下限,例如最大可能像是 2 的 64 次方之類的數字。畢竟電腦的記憶體有限,所以在 Python 的數字也不可能無限位數,只是上限還滿大的。以目前的 Python 版本的設定,最高可以有 4,300 位數,當然這用來算圓周率還是不夠用,但對一般的運算應該是相當夠用了。也就是說,如果我們故意挑戰 Python 的極限,可能會看到類似這樣的錯誤:

#  故意印出 10 的 4500 次方!

>>> 10 ** 4500
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
ValueError: Exceeds the limit (4300 digits) for integer string conversion; use sys.set_int_max_str_digits() to increase the limit

從錯誤訊息看的出來已經超過上限了,但如果想要提高額度,還是可以透過修改設定做到這件事,只是這就不是我們現在這種等級該煩惱的事。

型別轉換

雖然在 Python 的數字有整數跟浮點數之分,但彼此之間是可以透過內建的 int() 以及 float() 進行轉換的,例如:

# 整數
>>> age = 18

# 轉換成浮點數
>>> float(age)
18.0

# 浮點數
>>> pi = 3.14

# 轉換成整數
>>> int(pi)
3

a = a + 1?

如果你是數學系的學生,你看到 a = a + 1 這樣的數學式,大概會覺得怎麼可能會有哪個值等於自己加 1!當然以數學來說是不可能發生,但對 Python 以及目前大部份的程式語言來說,一個等號不是「等於」的意思,而是「指定(Assign)」的意思,a = a + 1 會先處理等號右邊的運算,把原本的變數 a 的值加 1 之後再指定給等號左邊變數 a,例如:

# 原本 18 歲
>>> age = 18
>>> age = age + 1

# 現在老一歲,變 19 歲了
>>> age
19

因為這種 a = a + 1 的寫法很常用,所以也有另一種更短的寫法:

age += 1

把加號移到等號的左邊,寫成 +=,這樣跟原本的效果是一樣的。看到這裡,聰明如你一定不只能舉一反三,還能舉一反五,以下這五行:

a -= 2
b *= 3
c /= 4
d **= 3
e //= 3

就等於以下這五行的效果:

a = a - 2
b = b * 3
c = c / 4
d = d ** 3
e = e // 3

另外,如果是 a += 1 這種一次只增加 1 的話,有些程式語言有 a++++a 這種更短的寫法,不過 Python 沒有這樣的設計。

科學記號表示法

我記得在我讀高中的時候,老師有時候會把一些特別大或是特別小數字用另一種方式來表示,例如在我高中化學曾經學過的亞佛加厥常數 6.02 × 1023 或是原子質量 1.66 × 10-27,老實說當時我不知道學這個常數或是原子質量要幹嘛,只知道用這種寫法可以讓數字看起來簡單一點,這樣的表示法稱之「科學記號表示法(Scientific Notation)」。使用科學計算表示法除了可以簡化原本很大或很小的數字外,在做運算的時候也挺方便,例如 30,000,000,000 乘以 0.000000015 等於多少?我相信這不難算,但那麼多個零看了眼睛都花了,如果改寫成科學記號表示法的話會變成 3 x 1010 乘以 1.5 x 10-8,這樣一來計算的時候就可以分開算,前面 3 x 1.5 = 4.5,而後面的 1010 x 10-8 就會得到 102,最後答案就是 4.5 x 102,也就是 450。

我們日常生活最常見的數字系統應該就是十進位了,我們每天能去超商買午餐付紙鈔會找零錢這件事就是十進位,剛剛講到的科學記號表示法 4.5 x 102 也是建立在十進位的系統之上。在 Python 中,科學記號表示法的寫法是用 e 來表示 10 的次方,例如 1.23 x 104,可以寫成:

>>> 1.23e4
12300.0

除了十進位外,常見的還有用在時間上的還有 24 進位以及 60 進位,而電腦主要使用二進位。接著我們來看看不同進位之間要怎麼進行轉換,例如十進位的數字 7.625 要怎麼轉成二進位?整數的部份比較簡單,整數 7 可以分解成:

1 x 22 + 1 x 21 + 1 x 20 = 4 + 2 + 1 = 7

所以 7 轉成二進位就是 111。小數 .625 的部份也是差不多的原理,只是指數要改用負數:

1 x 2-1 + 0 x 2-2 + 1 x 2-3 = 0.5 + 0 + 0.125 = 0.625

所以 7.625 轉成二進位就是 111.101。如果再用科學記號表示法就會變成 1.11101 x 22。7.625 一個是剛好可以完美轉換成二進位的數字,但不是每個數字都能這麼剛好轉換成二進位,如果再大一點點或小一點點,例如 7.626 呢?整數部份沒問題,還是 111,但小數部份就麻煩了,這有點難算,如果一直算下去你會得到一個超級長的結果 0.10100000010000011000100100110111010010111100011010101。事實上這根本算不完,就跟 10 除以 3 會得到 0.333333333... 一樣的無限循環,你在畫面上看到的只是一小部份。所以 7.626 轉換成二進位就變成 111.10100000010000011000100110...,轉換成科學記號表示法就會變成 1.1110100000010000011000100110... x 22,這看起來還是差不多囉嗦,沒什麼幫助。很多程式語言都是根據 IEEE 754 的規範來設計浮點數,IEEE 754 規範了幾種用來呈現浮點數的方式,其中 32 位元的就是「單精準度」,而 64 位元因為是 32 位元的兩倍,所以就是「雙精準度」。就以 32 位元的單精確度的遊戲規則來說:

第 1 位元是放正負數的符號(sign bit),0 表示正數,1 表示負數。 第 2 ~ 9 這 8 個位元是指數(exponent) 剩下第 10 ~ 32 這 23 個位元則是放實際的值(fraction)

也就是說,一個 32 位元的浮點數,只能存放 23 位有效數值。如果是雙精準度的 64 位元的話,它的指數部份佔 11 位元,所以實際能存放的有效位數只有 52 位數。

但問題是,後面會無限循環的數字就算是能放 1000 位數也沒用,再多位數就是不夠放。沒辦法顯示完整數字怎麼辦?這也沒辦法了,就只能算了吧。對,就是算了,就是因為這樣,所以常會說浮點數不是 100% 精準的原因。

正因為浮點數不是 100% 精準,所以在運算上也會有一些問題,例如當我們計算 0.1 + 0.2 的時候...

0.1 + 0.2 等於多少?

你也許會好奇,這種小學生的問題有什麼好問的,0.1 加 0.2 就是 0.3 啊,不然呢?其實這是個有趣的面試題,以人類的常識來說, 0.1 + 0.2 就是 0.3,但以電腦來說就不是這樣了。如前面所說,電腦裡存放的 0.1 跟 0.2 都不是剛好真的 0.1 跟 0.2,只是非常接近而已。所以在電腦上運算 0.1 + 0.2 的結果也會很接近 0.3,因為有效位數沒辦法存放所有的位數,剛好在相加進位之後變成 0.30000000000000004,所以你打開 Python 的 REPL 試一下就會知道:

>>> 0.1 + 0.2
0.30000000000000004

>>> 0.1 + 0.2 == 0.3
False

之前大家常會拿這個出來笑 JavaScript 是個爛程式設計語言,事實上只要浮點數是照 IEEE 754 標準實作的,像 Python 跟 Ruby 都是,印出來的答案都不會剛好等於 0.3。所以不是程式語言設計爛,而是浮點數本身就是這樣設計的,大家都只是照著規範做事而已。

位元運算

剛有稍微提到要怎麼把我們日常生活會用到的十進位數字轉換成二進位數字,例如十進位的 25 轉成二進位就是 11001,Python 有個內建的 bin() 函數可以把整數換成成二進位表示法:

>>> bin(25)
'0b11001'

前面的 0b 是 Python 的表示法,表示這是一個二進位的數字,這樣我們就不用自己算了。相對的,如果要把二進位數字換成十進位數字,可以用內建的 int() 函數:

# 第二個參數表示是二進位
>>> int("11001", 2)
25

# 預設是十進位
>>> int("100")
100

在二進位的世界裡有一種數字的運算方式叫做「位元移位(Bitwise Shift)」,例如把數字 25 的二進位 11001 整個「向右移」一個位數,就會變成 1100,最右邊的 1 會被擠掉。經過右移一位的結果再換算回十進位就會變成 12;同理,如果向右一次移動兩個位數,原本的 11001 就會變成 110,換算回十進位就會變成 6。

相對的,如果是「向左移」,就會在右邊補 0,例如 11001 向左移一位就會變成 110010,換算回十進位就會得到 50;如果是向左移兩位 110010,換算回十進位就會得到 100。

在 Python 可以使用 >><< 來做位元移位的計算:

# 向右移一位
>>> 25 >> 1
12

# 向右移兩位
>>> 25 >> 2
6

# 向左移一位
>>> 25 << 1
50

# 向左移兩位
>>> 25 << 2
100

有發現什麼有趣的地方嗎?位元移位其實就是在做乘法跟除法,往右移一個位置就是除以 2,往右移兩次就是除以 2 再除以 2;同理,向左邊移一位數就是乘以 2。

不是數字?

講到 IEEE 754,裡面有記載一個特別的值叫做「NaN」,它是「Not A Number」的縮寫,從字面上大概能猜出來它「不是數字」。但其實它是個數字,只是它用來表示一些不合理或沒意義的數學運算,例如無限大減無限大。有些人會覺得有點怪,明明就說不是數字,但竟然是個數字?這就像看到一個人舉著牌子說「這裡沒有人」,但這個人就站在你面前是一樣的概念,總得有個人舉著牌子才能說這裡沒有人,所以 IEEE 754 才設計出 NaN 這個特別的數字。

NaN 在 Python 的世界裡是一個浮點數,它有一些比較特別的特性。我們可以透過 float() 函數或是內建的 math 模組來產生一個 NaN:

import math

nan1 = float("nan")
print(nan1) # 印出 nan
print(type(nan1)) # 印出 <class 'float'>

nan2 = math.nan
print(nan2) # 印出 nan
print(type(nan2)) # 印出 <class 'float'>

NaN 就像黑洞一樣,大部份的數字運算遇到它都會變成 NaN,例如:

nn = float("nan")
print(nn + 1) # nan
print(nn - 20) # nan
print(nn * 100) # nan
print(nn / 100) # nan
print(nn % 2) # nan

雖然 NaN 很特別,但它就是一個數字,所以它也有一些數學運算的特性,例如:

# 任何數字的 0 次方都是 1
print(nn**0) # 1.0

# 1 的任意次方都是 1
print(1**nn) # 1.0

在 Python 所有的值都會等於自己,例如 1 == 1,這聽起來很像廢話,但 NaN 的特殊設計之一,就是它跟任何值都不會相等,包括它自己:

print(nn == 123)  # False

# 它瘋起來連自己都不認得!
print(nn == nn) # False

所以千萬不要拿它去跟任何值比較,答案永遠都是 False。如果想要判斷某個值是不是 NaN,應該使用 math 模組裡的 isnan() 函數:

>>> import math
>>> math.isnan(123)
False
>>> nn = float('nan')
>>> math.isnan(nn)
True

無限大?

另一個同樣也是記載在 IEEE 754 裡的特殊值叫做「無限大(Infinity)」。是的,無限大不只是個概念,它也是一個值。Python 按照 IEEE 754 的設計,無限大是一個浮點數,當某個浮點數超過它所能表示的範圍時,就會變成無限大。我們可先看一下 Python 裡關於浮點數的相關資訊:

>>> import sys
>>> sys.float_info
sys.float_info(max=1.7976931348623157e+308, max_exp=1024, ... 略 ...)

這裡的 import sys 就是匯入 sys 模組,這語法可先不管它,從上面顯示的資訊大概可以猜的出來浮點數的最大值 max1.7976931348623157e+308,也就是說如果浮點數超過最大值就會變成無限大,例如:

>>> 1e308
1e+308
>>> 1e309
inf

如果要直接產生一個無限大的值,可以使用 float() 函數搭配 inf-inf 字串,或是使用 math 模組裡的 inf 定義:

import math

# 正無限大
p_inf1 = float('inf')
print(type(p_inf1)) # 印出 <class 'float'>

p_inf2 = math.inf
print(type(p_inf2)) # 印出 <class 'float'>

# 負無限大
n_inf1 = float('-inf')
print(type(n_inf1)) # 印出 <class 'float'>

n_inf2 = -math.inf
print(type(n_inf2)) # 印出 <class 'float'>

這裡其實要用 float('Infinity') 也行,只是用 "inf" 比較短一點。如果要拿任何數值跟無限大進行比較,任何數值跟它比都比它小,相對的,任何數值跟負無限大比都比它大:

p_inf = float("inf")
n_inf = float("-inf")

print(123 < p_inf) # True
print(123 > n_inf) # True
print(p_inf > n_inf) # True

這看起來很合理。無限大一樣可以進行數學運算,有些還算容易想像,例如:

p_inf = float("inf")
n_inf = float("-inf")

# 無限大的 100 萬倍還是無限大
print(p_inf * 1000000) # inf

# 無限大加上或乘以無限大還是無限大
print(p_inf + p_inf) # inf
print(p_inf * p_inf) # inf

# 無限大的無限大次方!
print(p_inf**p_inf) # inf

有一些比較特別的操作但也還能理解,例如:

p_inf = float("inf")
n_inf = float("-inf")
nan = float("nan")

print(p_inf**0) # 1.0,任何數字的 0 次方都是 1
print(1**p_inf) # 1.0,1 的任意次方都是 1
print(p_inf - n_inf) # inf,無限大減掉負無限大是無限大
print(p_inf - p_inf) # nan,無限大減掉無限大沒有定義
print(p_inf + n_inf) # nan,負無限大加上無限大沒有定義
print(p_inf + nan) # nan,任何數跟 NaN 進行運算都是 NaN

最後,跟 NaN 一樣,如果要判斷某個值是不是無限大,可以使用 math 模組裡的 isinf() 函數:

import math

p_inf = float("inf")
n_inf = float("-inf")

print(math.isinf(p_inf)) # True
print(math.isinf(n_inf)) # True

對我來說,無限大是個很酷的概念,但因為我的工作或專案幾乎都是跟網站有關,在我過往二十幾年的工作生涯裡我好像沒什麼機會在工作上用到這麼酷的東西就是了。

文字

除了數字之外,在大部份的程式語言裡最常用的資料型態就是文字了。不過「文字(Text)」是一般人在講的,在工程師的程式世界裡通常會叫它「字串(String)」,用來表示一連「串」的文「字」。在 Python 裡的字串會使用成對的引號把它包起來,例如:

print('hello world')
print("你好")

你想用單引號或雙引號都可以,在其他程式語言的單引號跟雙引號可能會有功能上的差別,但在 Python 這兩種引號並沒有什麼差別,想用哪種都行。Python 官方的 PEP(Python Enhancement Proposals)文件有提到

In Python, single-quoted strings and double-quoted strings are the same. This PEP does not make a recommendation for this. Pick a rule and stick to it.

想要用單引號或雙引號都好,只要選一個規則然後堅持用下去就好。雖然單引號或雙引號沒什麼差別,但如果混著用會出問題:

print('hello world")  # 語法錯誤
print("你好') # 這樣也不行

像上面這樣寫會造成語法錯誤,字串需要用成對的單引號或雙引號包起來才行。在單引號裡可以包雙引號,在雙引號裡也可以包單引號,例如:

print("Hi, I'm Kitty")            # 印出 Hi, I'm Kitty
print('He said "Hello World"') # 印出 He said "Hello World"

如果要在單引號裡再放一個單引號,或是在雙引號裡再放一個雙引號,因為程式無法判斷到底這是一般文字裡的引號還是要用來包字串的引號,我們可以用個特別的符號來做個標記:

print('Hi, I\'m Kitty')   # 印出 Hi, I'm Kitty

這個 \ 符號就是告訴 Python 說「它就只是個一般的引號而已,不是給你當作字串結束用的」。這個 \ 符號又稱「跳脫字元(Escape Character)」。除了剛才的用途外,在 \ 後面加上一些字還有特別的效果,例如 \n 是換行的意思,\t 是 Tab 的意思:

>>> print("hi, \n 我們不同行")
hi,
我們不同行

>>> print("hi, \t 這裡會跳一大格")
hi, 這裡會跳一大格

既然 \ 符號可以用來「跳脫」字元,但如果我們真的需要 \ 這個字,例如想要印出 C:\Windows 這樣的字串怎麼辦?跟跳脫單引號、雙引號的概念一樣,你也可以多加一個跳脫符號,來跳脫 \ 這個跳脫符號:

>>> print("C:\\Windows")
C:\Windows

這樣就行了 :)

多行文字

在 Python 使用單引號或雙引號包起來字串是不能換行的,像這樣:

message = "Hello
Kitty" # 語法錯誤

print(message)

這樣寫會造成語法解析錯誤而無法執行。如果你真的想在字串裡加入換行的話,可以使用剛剛學到的 \n。Python 有提供一個特別的字串寫法,可以讓你在字串裡直接換行,就是用 3 個成對的單引號或雙引號:

message = """
Hello
Kitty
"""

print(message)

前面我們曾經學過 Python 使用 # 符號來做為註解,不過一個 # 符號的作用範圍只有一行,如果想要多行註解也很簡單,就是每一行都加 #。這聽起來好像有點麻煩,但在大部份的編輯器(例如 VSCode)只要按下 CMD + /(在 Windows 則是 Ctrl + /)就可以快速的切換註解的開關,所以其實也沒多麻煩。

也許你會在其他的教學資料裡看到利用三個引號可以做多行註解的效果,像這樣:

print("Hello")

"""
這是一行註解
這是第二行註解
這是第三行註解
"""

print("World")

但這裡我得特別跟各位說,三個引號的寫法並不是註解,它就只是可以換行的多行文字而已。因為這個多行文字我們並沒有指定變數給它,就像是船過水無痕的概念,執行完之後這段文字就消失了,不會影響執行結果,所以有時候在一些 Python 的原始碼裡面會看到用這種方式來撰寫文件或是測試。

再跟大家說個不重要的冷知識,Python 的編譯器在執行上面這幾行程式的時候,由於中間的多行文字並沒有指定給任何變數,在編譯的過程就會直接被拿掉了,如果用更專業的說法,就是這些東西根本不會被編譯到 Bytecode 裡。

轉換型別

前面提到的 int()float() 可進行整數及浮點數型態的轉換,而 Python 內建的 str() 函數可以把其他型別轉成字串:

>>> str(18)
'18'
>>> str(True)
'True'
>>> str([])
'[]'

我們在上個章節介紹到 print() 函數的時候也有提過類似的情況,在 Python 有些內建函數的名字雖然可以但不應該拿來當做變數名稱的,像 intfloatstr 以及在下個章節會介紹到的 bool 這種用來轉換型別的函數(嚴格說來是類別)都是,像這樣:

str = "hello"
age = 18
print(str(age))

在其他程式語言蠻常看到有人會用 str 來命名字串,在 Python 拿 str 來命名不會出錯,但在這之後如果呼叫 str() 函數來做字串轉型的時候就會出錯:

TypeError: 'str' object is not callable

因為 str 已經不是原本的 str() 函數啦,已經被我們重新指定成一個字串了,這要特別注意,通常遇到這種問題的時候都會覺得莫名奇妙然後要抓問題抓很久。

字串與數字

數字雖然不能跟字串做加減運算,但可以做乘法運算,例如:

>>> 'a' * 3
'aaa'
>>> 'abc' * 3
'abcabcabc'

拿字串跟數字相乘會建立一個重複 N 次的字串,這種寫法不只用在字串可行,只要是序列型的資料,像是後面章節才會介紹到的串列以及 Tuple 都可以這樣做:

# 串列
>>> [1, 4, 5, 0] * 2
[1, 4, 5, 0, 1, 4, 5, 0]

# Tuple
>>> (9, 5, 2, 7) * 3
(9, 5, 2, 7, 9, 5, 2, 7, 9, 5, 2, 7)

在 Python 把字串跟數字乘在一起會建立重複的字串,如果是把字串跟字串相加等於把兩個字串組合在一起:

>>> "hello" + " kitty"
'hello kitty'

甚至,在 Python 的字串相加,可以連中間的 + 都省略:

>>> "hello" " kitty"
'hello kitty'

# 甚至中間連空白都不用
>>> "hello"" kitty"
'hello kitty'

在別的程式語言這樣寫大概直接得到語法錯誤,但在 Python 裡是可以的,這算是 Python 的特性之一。我是不建議這樣寫,因為不只程式碼的可讀性不太好,而且不小心還會看錯,你猜猜看以下這段程式碼會印出什麼結果:

heroes = [
'悟空',
'克林',
'魯夫'
'鳴人'
'五條悟'
]

print(len(heroes)) # 會印出什麼?

在後面章節應該也會看到當在串列裡元素比較多的時候,有時候會習慣把每個元素分行寫,目測有 5 個元素,但執行之後會發現只會印出 3,這正是因為最後三個元素中間沒有逗號,所以會連成一個字串,實際上只有三個元素在裡面。

字串跟字串組合在一起還算容易理解,但如果是把字串跟數字加在一起會發生什麼事?像這樣:

age = 18
name = "Kitty"

print("我的名字是" + name + ",我今年" + age + "歲") # 會印出什麼?

執行之後就會得到型別錯誤(TypeError):

TypeError: can only concatenate str (not "int") to str

因為型別不同,所以不能把字串跟數字加在一起,Python 在這方面可不像某些程式語言那麼隨性。如果要組合在一起,得先想辦法用內建函數 str() 把它轉換成同一種型別:

age = 18
name = "Kitty"

print("我的名字是" + name + ",我今年" + str(age) + "歲")

不過這樣組合加上轉換型別的寫法看起來實在有點醜,醜就算了,一個不小心加號沒寫好還會出錯。Python 還有幾種比較簡單的寫法,可以直接把變數塞進字串裡,而且還不用先轉換型態。第一種是用 % 符號:

age = 18
name = "Kitty"

print("我的名字是 %s,我今年 %s 歲" % (name, age))

字串裡的 %s 代表要替換的位置,然後在 % 後面的括號裡按照順序把要替換的內容放進去就行了。不過這種 % 的寫法還是有些不好寫,Python 的字串有提供一個 .format() 方法可以做到類似的事:

age = 18
name = "Kitty"

print("我的名字是 {},我今年 {} 歲".format(name, age))

先在字串裡把打算替換的內容用 {} 大括號包起來,然後在 .format() 方法裡面把要替換的內容按照順序放進去就行了。這樣寫起來雖然比較簡單,但跟 % 的寫法一樣還是得注意順序,放錯可能會得到不對的結果。在 Python 3.6 之後有推出更簡單而且更直覺的寫法...「格式化字串」。

好用的 F 字串!

格式化字串(Formatted String),又稱之「F 字串(f-strings)」,是 Python 3.6 之後推出的新功能,使用 F 字串的寫法,上面的例子可以改成這樣:

age = 18
name = "Kitty"

print(f"我的名字是 {name},我今年 {age} 歲")

有看到在字串前面多了 f 字樣嗎?這表示這是一個要用來做「格式化」的字串,在這個字串裡,變數或是其他值可以直接在字串裡透過大括號 {} 進行組裝,看起來更直覺、更好寫了。

有些時候我們會利用 F 字串把變數的值印出來看看是不是跟我們想像的是一樣的,我們可能會這樣寫:

a = 1
b = 2

print(f"a={a}, b={b}") # 印出 a=1, b=2

這樣就會知道變數 ab 的值是多少。F 字串有個可能比較少人知道的小技巧,就是只要變數後面加上等號,可以直接把變數名稱跟值一起印出來:

a = 1
b = 2

print(f"{a=}, {b=}") # 印出 a=1, b=2

這樣就能少打幾個字,對我這種喜歡偷懶的人來說也算是一種生活中的小確幸。

F 字串的進階用法

F 字串不只是可以把變數塞到字串裡而已,它還有一些有趣的用法,像是我們常會遇到需要幫錢錢加上千位數的逗點,在別的程式語言可能需要透過其他函數或是另外自己寫函數來處理,用 F 字串可以輕鬆做到這件事,例如:

my_money = 1000000

print(f"{my_money:,}") # 印出 1,000,000
print(f"{my_money:.2f}") # 印出 1000000.00
print(f"{my_money:,.2f}") # 印出 1,000,000.00

在 F 字串裡的變數後面加上 : 可以幫我們做格式化,這裡的 , 就是在千位數字的地方加上逗點,.2f 是指可以讓原本的整數顯示到小數點以下第 2 位,如果組合在一起 ,.2f 就會變成整數部份加上千位數逗號,小數部份則呈現到小數點以下 2 位數。

有時候在做一些數學計算之後得到的結果想要以百分比的形式呈現,你可能會先把它乘以 100 然後在後面加個 % 符號,像這樣:

ratio = 0.315
print(f"{ratio * 100}%") # 印出 31.5%

這沒什麼問題,在別的程式語言也差不多是這麼做,但 Python 的 F 字串可以更簡單幫我們做到這件事:

ratio = 0.315
print(f"{ratio:.1%}") # 印出 31.5%

.1 表示顯示到小數點以下第 1 位,後面的 % 就是直接以百分比的形式呈現,至於這樣寫有沒有比較簡單或直觀可能因人而異了,有些人可能認為少了乘以 100 會覺得哪裡怪怪的。

如果有需要的話,我們可以透過 : 來設定字串的「寬度」,例如:

pi = 3.1415926

print(f"|{pi:5}|") # 印出 |3.1415926|
print(f"|{pi:20}|") # 印出 | 3.1415926|

我刻意在字串的兩邊加上 | 是為了讓大家看得更清楚一點,: 後面接的數字就是這個 {} 字串的寬度,如果字串本身的字數超過寬度(例如 5),字串還是會把設定的寬度給撐開,如果我設定寬度大一點,例如設定成 20,這樣會在字串的左邊補上空白,呈現出來的效果變成靠右對齊。

有靠右對齊,自然也有靠左對齊跟置中對齊,使用的符號是 ^> 以及 < 符號,例如:

pi = 3.1415926

print(f"|{pi:<20}|") # 靠左對齊
print(f"|{pi:>20}|") # 靠右對齊(預設值)
print(f"|{pi:^20}|") # 置中對齊

印出來的結果會是:

|3.1415926           |
| 3.1415926|
| 3.1415926 |

以做網站來說,瀏覽器不太會理會 HTML 裡多餘的空白字元,所以這種格式化對齊大概沒什麼機會派上用場,但如果是用在做一些資料統計或分析的小程式,並打算在終端機畫面呈現或是列印報表的時候,這種對齊就挺方便的了,例如:

print(f"1. {'為你自己學 Python':<20} NTD 600")
print(f"2. {'為你自己學 Git':<20} NTD 500")
print(f"3. {'為你自己學 Ruby on Rails':<20} NTD 480")
print(f"4. {'為你自己學 Rust':<20} NTD 650")

印出來的結果會變這樣:

1. 為你自己學 Python         NTD 600
2. 為你自己學 Git NTD 500
3. 為你自己學 Ruby on Rails NTD 480
4. 為你自己學 Rust NTD 650

填補的字串預設是使用空白字元,但如果你喜歡也可以換成別的字,例如:

pi = 3.1415926

print(f"|{pi:x<20}|") # 靠左對齊
print(f"|{pi:x>20}|") # 靠右對齊
print(f"|{pi:x^20}|") # 置中對齊

這裡我使用英文字母 x 來填補空白,印出來的結果會變成:

|3.1415926xxxxxxxxxxx|
|xxxxxxxxxxx3.1415926|
|xxxxx3.1415926xxxxxx|

透過 F 字串可以在數字前面補 0,例如:

score1, score2 = 123, 1450

print(f"{score1:08}") # 00000123
print(f"{score2:08}") # 00001450

hour, minute, second = 3, 12, 7
print(f"{hour:02}:{minute:02}:{second:02}") # 03:12:07

F 字串也能幫我們把整數轉換成二進位、八進位或十六進位,例如:

# 格式化成二進位
print(f"{10:b}") # 印出 1010

# 格式化成八進位
print(f"{10:o}") # 印出 12

# 格式化成十六進位
print(f"{255:x}") # 印出 ff

還記得前面提到的「科學記號表示」嗎?F 字串也能幫我們搞定,例如:

speed_of_light = 299792458      # 光速
print(f"{speed_of_light:.3e}") # 印出 2.998e+08

.3 代表小數點後面要顯示幾位數,e 代表科學記號。最後,F 字串還能順便幫我們搞定日期格式:

from datetime import date

the_day = date(1947, 2, 28) # 228

print(f"{the_day:%Y/%m/%d}") # 印出 1947/02/28

其中 %Y%m%d 是分別代表代表年份、代表月份以及日期,這些格式化參數的用法跟其他程式語言的格式化差不多,應該不難查到分別代表哪些意思。雖然在其他程式語言也有類似這種把變數塞進字串裡的「字串安插(String Interpolation)」功能,但 Python 的 F 字串還能加上格式化工具,能玩的把戲就更多了。

索引與切片

索引

字串,顧名思義,就是一串字元的集合,每一個字元都有它的位置,這個位置就叫做「索引」(index)。在 Python 這個程式語言,索引值是從 0 開始算,也就是說第一個字元的索引值是 0,第二個字元的索引是 1,依此類推。我們可以透過索引值來取得字串裡的某個字元:

>>> message = "Hello Kitty"

>>> message[0]
'H'

>>> message[1]
'e'

如果索引值是負的話,表示是從字串的尾巴算回來,例如:

>>> message = "Hello Kitty"

>>> message[-1]
'y'

>>> message[-2]
't'

索引值 -1 表示最後一個,-2 表示倒數第二個,依此類推。但如果超過範圍的索引值,就會得到「索引超出範圍」的錯誤(IndexError):

>>> message = "Hello Kitty"

# 發生錯誤
>>> message[1450]

既然字串可以透過索引值的方式來取得某個字元,你可能也會想是不是也能透過索引值把某個字元換掉。的確有些程式語言可以這樣做,但在 Python 的字串是不可變的(Immutable),所以不能透過任何方法更換裡面的內容,像這樣的操作是會出錯的:

message = "Hello Kitty"

# 試著把 H 換成 "X"
message[0] = "X"

這樣的操作是不行的,這麼做會得到錯誤訊息:

TypeError: 'str' object does not support item assignment

也就是說,字串一旦建立之後就不能再改變裡面的內容。看到這裡,你可能會想說舉像這樣的例子:

message = "Hello World"
message = "Hello Kitty"

這樣變數 message 的值不就變成 Hello Kitty 了嗎?如果你覺得這就是字串是可以被修改的話,那就是你誤會了上面這兩行語法的意思了。第一行是建立了一個字串 Hello World 並把它指定給變數 message,第二行是建立了一個新的字串 Hello Kitty,然後把這個字串指定給變數 message,這樣的操作並不是對「原本」的字串進行修改,而是建立了一個新的字串,然後把 message 變數指過去而已,原來的字串 Hello World 並沒有被修改,只是現在沒有任何變數指向它而已,這個行為叫做「重新指定(Reassign)」。

除了透過索引值的方式取值,還可以透過字串的「切片(Slice)」的功能來把字串的部份「切出來」...

切片(Slice)

字串切片的用法跟索引值有點像,也都是使用中括號 [] 的寫法,在中括號裡有三個欄位並使用分號 : 分隔,這三個欄位分別代表「起始位置(Start)」、「停止位置(Stop)以及「移動距離(Step)」,而且這些欄位都可以視情況省略。我們先來看看三個欄位都有寫的情況:

#   +---+---+---+---+---+---+---+---+---+---+
# | h | e | l | l | o | k | i | t | t | y |
# +---+---+---+---+---+---+---+---+---+---+
# 0 1 2 3 4 5 6 7 8 9 10
# -10 -9 -8 -7 -6 -5 -4 -3 -2 -1

>>> message = "hellokitty"

>>> message[0:2:1]
'he'

>>> message[2:8:2]
'loi'

>>> message[8:3:-1]
'ttiko'

說明如下:

  1. [0:2:1] 代表從索引值 0 的位置開始,到索引值 2 之前(自己不算),每次移動 1 個索引值,所以得到的結果是 he
  2. [2:8:2] 同理,從索引值 2 開始,到索引值 8 之前(自己不算),但每次移動 2 個索引值,所以得到的結果是 loi
  3. 起始位置不一定要小於停止位置,例如 [8:3:-1] 就是指從索引值 8 開始,到索引值 3 之前(一樣自己不算),每次移動 -1 個索引值,意思是從後面往前移動,所以最後得到的結果是 ttiko

省略欄位的話?

切片裡的三個欄位都可以視情況省略,省略的欄位會有預設值,但至於預設值是什麼得看是怎麼省略的。在 Python 有一個特別的值叫做 None,在下個章節才會更詳細說明,但這裡大家可以把它當做「沒有」的意思。在使用切片的時候,如果省略了某個欄位,Python 會填上 None,至於填了 None 是什麼效果可能會跟是兩個或三個欄位的寫法有關,我們先來看看兩個欄位的寫法。

在只有兩個欄位的情況下,例如 [x:y],這時候如果省略了 x 或是主動給它 None,起始位置(Start)會被設定成 0;如果省略 y 或是主動給它 None 的話,停止位置(Stop)會被設定成這個字串的長度(也就是總字數 len() 算出來的結果)。在兩欄式的寫法,原本第三欄位的移動距離(Step)會被設定成 1:

#   +---+---+---+---+---+---+---+---+---+---+
# | h | e | l | l | o | k | i | t | t | y |
# +---+---+---+---+---+---+---+---+---+---+
# 0 1 2 3 4 5 6 7 8 9 10
# -10 -9 -8 -7 -6 -5 -4 -3 -2 -1

>>> message = "hellokitty"

# 省略 Start,Start = 0
>>> message[:5]
'hello'

# 跟上面一樣效果
>>> message[None:5]
'hello'

# 省略 Stop,Stop = len(message)
>>> message[5:]
'kitty'

# 跟上面一樣效果
>>> message[5:None]
'kitty'

# 都省略,Start = 0, Stop = len(messages)
>>> message[:]
'hellokitty'

# 跟上面一樣效果
>>> message[None:None]
'hellokitty'

如果是三個欄位的寫法,像這樣 [x:y:z],遊戲規則就會比較複雜一點了。省略 xy 或是主動給它 None 的話,xy 會是「邊界值(end value)」,至於是哪一邊的邊界,要看 z 的值而決定。

如果 z 是正數的話,表示方向是往右,x 會是左邊界,y 會是右邊界;反之 z 是負數的話,表示方向是往左,x 會是右邊界,y 會是左邊界。要特別注意的是這個「邊界值」指的不是索引值 0 或是 len() 算出來的數字,這裡說的邊界值就的就是實際上字串的邊邊角角的那個字,直接來看幾個例子:

#   +---+---+---+---+---+---+---+---+---+---+
# | h | e | l | l | o | k | i | t | t | y |
# +---+---+---+---+---+---+---+---+---+---+
# 0 1 2 3 4 5 6 7 8 9 10
# -10 -9 -8 -7 -6 -5 -4 -3 -2 -1

>>> message = "hellokitty"

## 省略 Start
# Step 是正數,Start = 左邊界值 h
>>> message[:5:1]
'hello'

# Step 是負數,Start = 右邊界值 y
>>> message[:5:-1]
'ytti'

## 省略 Stop
# Step 是正數,Stop = 右邊界值 y
>>> message[5::1]
'kitty'

# Step 是負數,Stop = 左邊界值 h
>>> message[5::-1]
'kolleh'

>>> message[5:0:-1]
'kolle'

## Start 跟 Stop 都省略
# Start = 左邊界值 h, Stop = 右邊界值 y
>>> message[::1]
'hellokitty'

# Start = 右邊界值 y, Stop = 左邊界值 h
>>> message[::-1]
'yttikolleh'

# 全部都省略
# Start = 左邊界值 h, Stop = 右邊界值 y,Step = 1
>>> message[::]
'hellokitty'

# 同上效果
>>> message[None:None:None]
'hellokitty'

我曾經看過有網路上的資料會說沒給值就是就是預設值 0,其實並不是:

>>> message[5::-1]
'kolleh'

>>> message[5:0:-1]
'kolle'

沒給值就是 None,而不是 0,而且索引值 0 跟邊界值也是不一樣的。

但不要被「切片」的「切」字給騙了,再次提醒,在 Python 的字串是不可被修改的,所以就算使用切片也不會改變原來的字串。後面章節介紹到的串列(List)也有切片的功能,串列的切片用起來跟字串幾乎一模一樣,不同的是串列的內容是可以被修改的,但字串切片不行。

切片物件

會覺得 [::-1] 難寫或是看起來不直覺嗎?在 Python 有提供了一個 slice 類別,可以用來建立切片物件(Slice Object),這個切片物件不只可以用在字串上,只要能做切片操作的資料像是串列或 Tuple 等都可以這樣玩,例如:

reverse = slice(None, None, -1)
all = slice(None, None, None)
last_five = slice(-5, None)

message = "hellokitty"
print(message[reverse]) # 印出 yttikolleh
print(message[all]) # 印出 hellokitty
print(message[last_five]) # 印出 kitty

至於這樣有沒有比較容易閱讀,就看個人喜好了。

常用字串方法

Python 的字串有很多好用的方法,這裡列出一些常用的方法供大家參考。

大小寫轉換

>>> message = "HelloKitty"

# 全部轉大寫
>>> message.upper()
'HELLOKITTY'

# 全部轉小寫
>>> message.lower()
'hellokitty'

# 首字轉大寫
>>> message.capitalize()
'Hellokitty'

# 大小寫交換
>>> message.swapcase()
'hELLOkITTY'

比對、判斷

比對或是判斷的方法,主要是問有或沒有、是或不是,所以這些方法通常得到真的(True)還是假的(False)結果:

>>> message = "HelloKitty"

# 是否 H 開頭
>>> message.startswith("H")
True

# 是否 Hello 開頭
>>> message.startswith("Hello")
True

# 是否 y 結尾
>>> message.endswith("y")
True

# 是否 kitty 結尾(注意大小寫不符)
>>> message.endswith("kitty")
False

# 是否全大寫
>>> "HELLO".isupper()
True

# 是否全大寫
>>> "Hello".isupper()
False

# 是否全小寫
>>> "hello".islower()
True

# 是否全小寫
>>> "Hello".islower()
False

# 是否全都是數字
>>> "123".isnumeric()
True

# 是否全都是數字
>>> "a123".isnumeric()
False

TrueFalse 在 Python 是一種布林型態(Boolean)的資料型態,更多細節會在下個章節介紹。

搜尋、取代

使用字串的 .index() 方法,可以找出指定的字元在字串裡的索引值,如果在字串裡同時有好幾個符合的字元,只會列出第一個找到的字的索引值,但如果一個都找不到會發生錯誤:

#   +---+---+---+---+---+---+---+---+---+---+
# | h | e | l | l | o | k | i | t | t | y |
# +---+---+---+---+---+---+---+---+---+---+
# 0 1 2 3 4 5 6 7 8 9 10
# -10 -9 -8 -7 -6 -5 -4 -3 -2 -1

>>> message = "hellokitty"

# k 在字串裡的索引值
>>> message.index("k")
5

# l 在字串裡的索引值
>>> message.index("l")
2

# l 在字串裡的索引值(從右邊開始找)
>>> message.rindex("l")
3

# z 在字串裡的索引值,找不到!
>>> message.index("z")
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
ValueError: substring not found

這裡需要說明的是 .index().rindex() 方法,以字元 l 來說,它在 hellokitty 字串裡出現過兩次,使用 .index() 方法會從字串開頭開始找,使用 .rindex() 方法則是從字串結尾開始找,所以 .rindex() 方法會找到比較後面的那個 l 的索引值,也就是 3。除了 .index() 方法外,也可以用 .find() 來找:

>>> message = "hellokitty"

# k 在字串裡的索引值
>>> message.find("k")
5

# l 在字串裡的索引值
>>> message.find("l")
2

# l 在字串裡的索引值(從右邊開始找)
>>> message.rfind("l")
3

# z 在字串裡的索引值,找不到!
>>> message.find("z")
-1

.find().rfind() 方法的概念跟前面一樣,不過找不到指定字元的時候並不會出錯,而是得到 -1

也許看到這裡會好奇,為什麼有 .find() 還需要 .index() 方法,用 .find() 方法找不到不會出錯,只是靜靜的給我們 -1 而已,這好像比較好用?不要看這兩個的執行結果有點像,但設計概念是不同的。

如果以會出現錯誤訊息的角度來看,.index() 方法有可能會出錯,為了保險起見會需要加上後面章節才會介紹到的 try...except... 語法把它包起來才不會出事,這的確比較麻煩一點沒錯。但如果你使用 .find() 方法找索引值的話,萬一找不到符合的字元得到 -1,然後拿這個 -1 去取值,剛好在 Python 裡 -1 是個有效的索引值,對字串來說是字串的最後一個字:

message = "hellokitty"
found = message.find("z") # 這裡會得到 -1

if found:
print(message[found]) # 這裡會印出最後一個字 y

這反而會造成更大的誤會,所以 .find() 方法通常用來「判斷」某個字元或字串是否存在,比較簡單好用,但如果你要的是取得索引值的話,建議使用 .index() 方法比較合適。

字串的取代也很直覺,就是使用 .replace() 方法:

>>> message = "hellokitty"

# 把 t 換成 _
>>> message.replace("t", "_")
'helloki__y'

# 把 t 換成 _,但只換 1 個
>>> message.replace("t", "_", 1)
'helloki_ty'

# 把 kitty 換成 nancy
>>> message.replace("kitty", "nancy")
'hellonancy'

# 把 kitty 換成空字串
>>> message.replace("kitty", "")
'hello'

.replace() 的第 3 個參數不一定要給,如果沒給的話就是全部取代,如果有給數字就是表示要取代幾個。再再再次提醒,在 Python 的字串是不可變的,所以使用了 .replace() 方法也不會直接改變原本的字串,而是得到一個新的字串。

另外,有些時候我們會請使用者輸入文字,這些文字的頭尾可能會有一些多餘的換行或空白字元,可使用字串的 .strip() 方法來清掉這些頭尾的空白字元:

# 刪除頭尾的空白字元
>>> " hellokitty ".strip()
'hellokitty'

大部份時候我們大概只會拿它來刪除頭尾的空白字元,但其實 .strip() 方法不只可以刪頭尾的空白,還可以刪除指定的字元:

# 刪除頭尾的 h 字元
>>> "hellokitty".strip("h")
'ellokitty'

# 刪除頭尾的 h、y、i、t 字元,不分順序
>>> "hellokitty".strip("hyit")
'ellok'

拆解、合併字串

字串的拆解與合併也是常見的操作,拆解就是把一個字串拆開成一個一個字元,合併就是反過來把多個字元組合成一個字串。拆解使用的是字串的 .split() 方法:

>>> data = "a,b,c,e,f,g"

# 沒有給拆解字元
>>> data.split()
['a,b,c,e,f,g']

# 使用 - 做為拆解字元,沒東西拆
>>> data.split("-")
['a,b,c,e,f,g']

# 使用逗號做為拆解字元
>>> data.split(",")
['a', 'b', 'c', 'e', 'f', 'g']

# 使用逗號做為拆解字元,但只拆 2 個出來就好
>>> data.split(",", 2)
['a', 'b', 'c,e,f,g']

# 使用字元 c 做為拆解字元
>>> data.split("c")
['a,b,', ',e,f,g']

合併是使用字串的 .join() 方法:

# 使用 - 字元來組合
>>> "-".join("abcdefg")
'a-b-c-d-e-f-g'

# 使用 / 字元來組合
>>> "/".join("abcdefg")
'a/b/c/d/e/f/g'

# 使用空字串組合
>>> "".join("abcdefg")
'abcdefg'

Python 的字串可看成把字元串在一起的組合,所以在這裡學到的一些操作,在後面章節講到串列(List)的時候會發現不少概念甚至連方法都是共通的,連用起來的樣子都一模一樣。更多關於字串的方法,可以參考官方文件,特別是在睡不著的夜裡,翻閱官方網站的文件應該能起到助眠的效果 :)

位元組

如果這是你第一次接觸程式語言,接下來要介紹的內容對新手來說可能會稍稍復雜,或是有些奇幻,你可以當故事看,看不懂也沒關係,先跳過這個段落等之後對程式比較熟悉之後再回來看也不遲。

在 Python 的字串,其實是一連串的 Unicode 字元,所以我們在前面才可以用索引值或切片的方式來取得其中部份字元。但,你知道其實「字串是不能存成檔案的」這句話是什麼意思嗎?不急,讓我先來科普幾個專有名詞。

Unicode 與 UTF

什麼是 Unicode?在講這故事之前得先看一個叫做 ASCII 的東西,它的全名有夠長,是 American Standard Code for Information Interchange。簡單的說,ASCII 就是一種用來表示文字的編碼方式,它一開始是用 7 個位元,也就是 7 個 0 跟 1 的排列組合,用來表示每個字元,後來是有擴充到 8 個位元,但就算 8 位元最多也就 2 的 8 次方,也就是 256 種排列組合而已。有看到前面的「American Standard」嗎?是的,這個標準就是針對美國量身打造的,這對使用英文的人來說沒什麼問題,不管是原本的 128 或是後來擴充到 256 種組合,已經足夠用來表示所有大小寫英文字母外加上其他符號,但其他文字呢?日文、印度文、土耳其文以及中文呢?中文還有再細分繁體跟簡體,ASCII 只有少少的 256 種組合,沒辦法表示這些文字,所以後來大家就各自定義自己的編碼方式,像是日文的 Shift_JIS、繁體中文的 big5。但這樣又產生新的問題,就是如果在一份文件裡同時出現中文跟日文的時候怎麼辦?為了解決這個問題,便推出了 Unicode。

Unicode 又稱「萬國碼」,不像 ASCII 只能支援少少的字元,Unicode 一開始設計就使用 16 個位元,也就是 2 的 16 次方,最多可以表示 65,536 個字元,一開始想說這可能夠,但光是康熙字典記載的漢字就超過四萬字了,這六萬多字就有點不太夠用了。後來有陸陸續續的擴充到 21 個位元,以現在來說應該可以裝的下全世界各種語言的所有文字,包括表情符號 Emoji 😀。為什麼表情符號也要?你應該會希望用手機傳給你朋友一個可愛的笑臉或愛心符號,在對方的手機上也能正常顯示吧 :)

知道了 Unicode 是什麼之後,那麼常聽到的 utf-8 又是什麼?如上面所述,Unicode 是一種字元集合,它只定義了每個字元代表的數字是什麼,但沒有規定要如何呈現這些字元。UTF(Unicode Transformation Format) 是一種編碼方式,UTF 編碼方式也有因為不同的位元數有分 utf-8utf-16utf-32,以目前最常見的是 utf-8

位元組與字串

講古講的差不多,該回來看看位元組(Byte)了。位元組是 Python 3 新增的資料型態,跟字串一樣,它是一個不可變的序列,裡面的元素是 0 到 255 之間的整數。位元組的表示方法跟字串很像,也是用單引號或雙引號包起來,只是在前面加上一個 b

>>> message = b"hello world"
>>> message
b'hello world'
>>> type(message)
<class 'bytes'>

或是使用 bytes() 函數也行,只是需要額外加上編碼方式:

>>> message = bytes("hello world", "utf-8")

位元組跟字串一樣也是序列,所以可以用索引值或切片的方式來取得其中的元素,只是拿到的東西可能跟你想像的不太一樣:

>>> data = b"hello"
>>> data[0]
104
>>> data[1]
101

怎麼不是拿到 he?這是因為 b'hello' 位元組其實是一堆數字的集合,把它轉成串列可以看的更清楚一點:

>>> list(data)
[104, 101, 108, 108, 111]

這些數字是就是剛才提到的 ASCII 碼,104 表示 h101 表示 e,依此類推,所以當你試著使用索引值拿位元組的資料的時候,會拿到數字而不是字元本身。

也因為位元組裡面放的是 ASCII 碼,所以如果你想要把中文或日文字透過 b'...' 的寫法產生位元組的話會出現錯誤:

>>> b"悟空"
File "<stdin>", line 1
b"悟空"
^^^^^
SyntaxError: bytes can only contain ASCII literal characters

不過沒關係,字串跟位元組之間還是可以透過某些方法互相轉換的:

>>> name = "悟空"
>>> encoded_name = name.encode()
>>> encoded_name
b'\xe6\x82\x9f\xe7\xa9\xba'
>>> encoded_name.decode()
'悟空'

只要用字串的 .encode() 方法以及位元組的 .decode() 方法就可以進行字串與位元組之間的互相轉換。不管是編碼或解碼,這兩個方法預設的編碼方式都是用 utf-8,如果要使用其他的編碼方式,可以帶入編碼方式,但到時候要解碼的時候也要用相同的編碼方式,否則會轉不回來。

前面說過,字串是用 Unicode 來「表示」的,但有時候我們需要「處理」的資料是二進位的,這時候就需要用到位元組(Byte)了。更直白的說,字串是給人看的,所以我們在瀏覽器頁面上、在編輯器上、在終端機上或是任何地方,看到的都是字串。但如果是要給電腦看的,或是把它存成檔案的話,就需要使用位元組(Byte)了。這是在說什麼呢?字串不能存成檔案?不然我們平常寫的程式碼或輸入的文字,不都是存成檔案的嗎?

事實上,在電腦裡所有的檔案,都是用二進位的方式在存放,包括看起來很簡單的文字檔也不例外。也就是說,你以為的字串,最後都是被轉換成位元組(Byte)以二進位的方式來存放在儲存裝置上。但為什麼我們用文字編輯器打開它,看起來還是正常的文字呢?這是因為文字編輯器在開啟檔案的時候,會自動選擇適當的編碼方式,有點像剛才我們用 .decode() 方法來幫你把位元組轉換成字串,所以我們看到的就是一般的文字。這就是我在前面說到的「字串是不能存成檔案的」,回想一下,大家是否曾經有遇過使用編輯器打開某個檔案結果看到一堆亂碼?這就是因為編輯器使用了不正確的編碼方式來解碼,導致亂碼的出現。

等等,如果說位元組不是給我們人看的,為什麼我們一開始在建立位元組的時候:

>>> message = b'hello world'
>>> message
b'hello world'

這裡看起來還是一般的文字啊!這是因為 Python 在顯示位元組的時候會自動幫我們解碼成字串讓我們看的懂而已,本質上它還是位元組。

位元組跟字串一樣也是可以做一些搜尋的功能,只是因為位元組裡面放的是數字,所以你需要用數字來搜尋:

>>> data = b'hello'

# 直接搜尋字元會出錯
>>> data.find('h')
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: argument should be integer or bytes-like object, not 'str'

# 使用數字 101,也就是字母 e 進行搜尋
>>> data.find(101)
1

# 也可以使用位元組來搜尋
>>> data.find(b'e')
1

再補充個不太重要的豆知識,剛說到位元組是 Python 3 才有的資料型態,但其實 Python 2 就有出現 bytes 這個東西了,只是它在 Python 2 的時候就只是字串:

# 注意,這是 Python 2
$ python
Python 2.7.18 (default, Dec 12 2023, 19:58:19)
>>> bytes
<type 'str'>
>>> str
<type 'str'>

你會看到其實它跟字串一樣都是 str,也就都是字串而已。不信的話,你可以這樣試試看:

>>> message1 = bytes('hello')
>>> message2 = 'hello'
>>> message1 == message2
True

在 Python 2 時代還有個特別的型態叫 unicode,是在字串前面加上 u,可用來表示 Unicode 字串。

# 這是 Python 2 的 unicode
>>> u1 = u'hello'
>>> type(u1)
<type 'unicode'>

在 Python 3 也有 u 的語法,但其實已經沒有特別的意義了,因為在 Python 3 裡面字串就是 Unicode 字串,所以 u 反而有些多餘:

# 這是 Python 3 的 unicode
>>> u2 = u'hello'
>>> type(u2)
<class 'str'>

這也是為什麼 Python 2 跟 Python 3 不相容的原因之一。

就以一般做網站的工程師來說,可能比較少機會使用位元組這樣的資料型態,但如果有機會遇到要處理二進位的資料,例如影音串流之類的,或是要處理一些編碼的問題,位元組就會是一個很重要的資料型態了。

工商服務

想學 Python 嗎?我教你啊 :)

想要成為軟體工程師嗎?這不是條輕鬆的路,除了興趣之外,還需要足夠的決心、設定目標並持續學習,我們的ASTROCamp 軟體工程師培訓營提供專業的前後端課程培訓,幫助你在最短時間內建立正確且扎實的軟體開發技能,有興趣而且不怕吃苦的話不妨來試試看!