數字與文字
大部份的程式語言都有設計不同的資料型態,有簡單型態的也有複雜的集合型態,這個章節先來看看最常見的數字與文字。
數字
對一般人來說,數字就是數字,這是我們每天眼睛張開就要面對的東西,再熟悉不過了。大部份的程式語言都有數字可以用,在 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 的規範,在做捨入計算的時候會計算出最接近的整數,但如果遇到中間值的時候,有兩種做法:
- 「ties to even」就是做捨入計算的時候,會選擇離這個數字最近的「偶數」。
- 「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.67
跟 2.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 的數學模組功能,可參閱官方文件說明。
Python 數學模組 https://docs.python.org/library/math.html
關於數字的最大值或最小值,有些程式語言在宣告整數或浮點數的時候,會根據不同型態而有不同的上下限,例如最大可能像是 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
模組,這語法可先不管它,從上面顯示的資訊大概可以猜的出來浮點數的最大值 max
是 1.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 有些內建函數的名字雖然可以但不應該拿來當做變數名稱的,像 int
、float
、str
以及在下個章節會介紹到的 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
這樣就會知道變數 a
跟 b
的值是多少。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"