電腦科學在研究什麼
電腦科學是研究問題與解法的學問;演算法是有限步驟的解法,而有些問題根本無解。
先讀原文開場,旁邊就是白話
這是一本英文書。左邊放原文、右邊放白話導讀——你既讀得懂,也順手碰了原文。
電腦科學是研究問題與解法的學問;演算法是有限步驟的解法,而有些問題根本無解。
問題、演算法與可計算性
電腦科學是研究問題與解法的學問;演算法是有限步驟的解法,而有些問題根本無解。
深度探秘
電腦科學其實不是『學電腦』
名字騙了你
「電腦科學 (Computer Science)」這個名字其實很容易誤導人。它不是研究電腦這台機器本身的學問,電腦只是過程中很重要的工具而已。
電腦科學是研究問題 (problems)、解決問題的過程 (problem-solving),以及這個過程產出的解法 (solutions) 的學問。
演算法 = 解法
面對一個問題,電腦科學家的目標是發展出一個 演算法 (algorithm):
- 一份一步一步的指令清單
- 能解決這個問題的任何一個實例 (instance)
- 而且是有限的過程(照著做一定會停、會得到答案)
換句話說,演算法就是解法本身,它跟用哪台機器、哪種程式語言無關。
有些問題根本無解
要小心一件事:不是所有問題都有解法。有些問題已被證明不存在任何演算法可以解它。所以更完整的定義是:電腦科學同時研究『有解的問題』與『無解的問題』。
電腦科學研究的是問題與解法,演算法就是解法,電腦只是工具。
生活妙喻
演算法就像一份食譜
食譜與料理
把問題想成「我想吃一盤蛋炒飯」,那麼演算法就是那份食譜:
- 食譜有明確的步驟:先打蛋、熱油、下飯、翻炒、調味、起鍋。
- 它能應付任何一鍋飯(不同分量、不同鍋子)——這就是『解決任何實例』。
- 它一定會結束:照著做完就有飯吃,不會永遠炒下去。
而真正的爐子、鍋子就像電腦——只是讓食譜得以執行的工具。同一份食譜換一個廚房也能做出同樣的菜。
有些料理做不出來
如果有人要你『煮出一鍋永遠吃不完又永遠是熱的飯』,再厲害的食譜也寫不出來——這就對應到『無解的問題』。電腦科學家的一部分工作,就是分辨哪些『料理』根本做不出來。
演算法像食譜:明確步驟、能應付任何實例、保證會結束,而電腦只是廚房。
可計算性
computable 是什麼意思
可計算 (computable)
當一個問題存在能解它的演算法時,我們說這個問題是可計算的 (computable)。於是電腦科學還有另一種定義方式:
電腦科學是研究『可計算與不可計算問題』、研究『演算法存在與不存在』的學問。
注意:在這整個定義裡,『電腦』這個詞根本沒出現過——因為解法與機器是獨立的。
三種問題,三種命運
flowchart TD
A[一個問題] --> B{存在演算法嗎}
B -->|存在且夠快| C[可計算且實用]
B -->|存在但太慢| D[難解 intractable]
B -->|不存在| E[無解問題]
學會分辨這三種,是後面學分析演算法的重要起點:有解、無解、以及『有解但代價太高』。
問題若存在解它的演算法就是可計算的;解法獨立於機器之外。
食譜用明確且有限的步驟,能料理任何一鍋食材,正如演算法用有限步驟解決問題的任何實例。
爐具只是讓食譜得以執行的工具,同一份食譜換廚房也能做菜,正如解法獨立於機器之外。
本節字彙
抽象化:邏輯觀點與實體觀點
用開車與數學模組的比喻,分清楚『介面』與『底層細節』,理解程序抽象化與黑盒子概念。
深度探秘
把『怎麼用』和『怎麼做』分開
抽象化 (abstraction)
解決問題時,複雜度常常讓人迷失在細節裡。抽象化是電腦科學最重要的武器之一,它讓我們把同一件事拆成兩種觀點:
- 邏輯觀點 (logical perspective):使用者怎麼用這個東西。
- 實體觀點 (physical perspective):底層怎麼運作的所有細節。
介面是兩者的橋
連接使用者與底層複雜度的橋樑,就是介面 (interface)。只要你懂介面怎麼用,就不需要知道內部細節。
使用抽象的人有時稱為客戶 (client)。
| 觀點 | 是誰 | 關心什麼 |
|---|---|---|
| 邏輯觀點 | 使用者 / 客戶 | 介面怎麼用、能做什麼 |
| 實體觀點 | 實作者 / 維護者 | 內部如何運作的細節 |
抽象化把『邏輯觀點(怎麼用)』與『實體觀點(怎麼做)』分開,介面是兩者的橋。
生活妙喻
開車的人 vs. 修車的技師
同一台車,兩種眼光
想想你今天開的那台車:
- 身為駕駛,你只需要轉鑰匙、踩油門、打方向盤——你看到的是車子的邏輯觀點,用設計者提供的功能把自己從 A 載到 B。這些功能就是車子的介面。
- 而技師看到的是實體觀點:引擎怎麼運轉、變速箱怎麼換檔、溫度怎麼控制——也就是『引擎蓋底下』的細節。
共通點
兩個角色的共通點是:駕駛(客戶)不需要懂引擎蓋底下的事,只要會用介面就好。
flowchart LR
A[駕駛 客戶] -->|透過介面 油門方向盤| B[車子]
B -.隱藏.-> C[引擎 變速箱 等實體細節]
駕駛只需懂介面(邏輯觀點),技師才需懂引擎蓋底下(實體觀點)。
實用超能力
程序抽象化與黑盒子
數學模組的例子
在 Python 裡呼叫平方根,就是一個程序抽象化 (procedural abstraction) 的範例:
>>> import math
>>> math.sqrt(16)
4.0
我們不知道它內部到底怎麼算平方根,但我們知道:
- 函式叫什麼名字(
sqrt) - 要餵它什麼(參數 16)
- 它會回傳什麼(4.0)
黑盒子 (black box)
這就是所謂的黑盒子觀點:只描述介面(名稱、參數、回傳值),把細節藏在裡面。
這是你未來寫程式的超能力:把複雜的東西包成一個你信任、會用就好的盒子,專心去解更大的問題。
程序抽象化讓你像用黑盒子一樣:只需知道名稱、參數、回傳值就能使用。
駕駛只透過油門方向盤等介面用車(邏輯觀點),技師才需要懂引擎蓋底下的運作(實體觀點)。
math.sqrt 像個黑盒子,你只看得到輸入與輸出,看不到也不需要看到內部如何運算。
本節字彙
為什麼要學資料結構與演算法
ADT 是資料的邏輯描述(封裝與資訊隱藏),資料結構則是它的實作;兩者分離帶來實作獨立性。
抽象資料型別 ADT 與資料結構
ADT 是資料的邏輯描述(封裝與資訊隱藏),資料結構則是它的實作;兩者分離帶來實作獨立性。
深度探秘
ADT 是『資料的黑盒子』
從程序抽象化到資料抽象化
前面看到的程序抽象化是『隱藏一個函式的細節』;現在我們把同樣的想法用在資料上,這叫資料抽象化 (data abstraction)。
抽象資料型別 (Abstract Data Type, 簡稱 ADT) 是:
對『我們如何看待這份資料』以及『允許對它做哪些操作』的邏輯描述,完全不管它將來怎麼被實作。
換句話說,ADT 只關心資料代表什麼、能做什麼,不關心怎麼蓋出來。
封裝與資訊隱藏
- 封裝 (encapsulation):在資料外圍包一層殼,使用者只透過殼上的操作來互動。
- 資訊隱藏 (information hiding):把實作細節藏在殼裡,使用者看不到。
flowchart TD
U[使用者 client] -->|只用介面操作| S[ADT 外殼 介面]
S -.資訊隱藏.-> I[實作 資料結構]
ADT 是資料的邏輯描述:只說『是什麼、能做什麼』,用封裝與資訊隱藏把細節藏起來。
生活妙喻
點餐單 vs. 廚房
餐廳的菜單與廚房
把 ADT 想成餐廳的菜單,把資料結構想成廚房:
- 菜單 (ADT) 告訴你:有哪些餐點、你可以點什麼、會端出什麼。這是『邏輯描述』。
- 廚房 (資料結構) 才是真正用鍋碗瓢盆把菜做出來的地方,那是『實體實作』。
你身為客人(client),只看菜單點餐,完全不需要知道廚房怎麼運作——這就是資訊隱藏。
換廚房,不換菜單
最妙的是:餐廳今天換了一批新廚具、改了烹調流程(換掉資料結構的實作),只要菜單不變,你點餐的方式完全不用改。這就是接下來要講的『實作獨立性』。
ADT 像菜單(你怎麼點、會得到什麼),資料結構像廚房(實際怎麼做出來)。
實用超能力
資料結構與實作獨立性
資料結構 = ADT 的實作
資料結構 (data structure) 就是 ADT 的實作:它用程式語言的建構與基本型別,提供資料的實體觀點。
| 概念 | 對應觀點 | 比喻 |
|---|---|---|
| ADT | 邏輯觀點(是什麼、能做什麼) | 菜單 |
| 資料結構 | 實體觀點(怎麼蓋出來) | 廚房 |
實作獨立性 (implementation independence)
一個 ADT 通常有很多種實作方式。把邏輯與實體分開,帶來一個超能力:
程式設計師可以換掉內部實作的細節(換一種更快的資料結構),而完全不影響使用者與資料互動的方式。
這讓使用者能持續專注在解題本身,而不被底層細節綁架。
資料結構是 ADT 的實作;分離兩者帶來實作獨立性,可換實作而不影響使用者。
菜單(ADT)描述你能點什麼、會得到什麼;廚房(資料結構)才是真正做菜的地方。換廚房不換菜單,點餐方式照舊。
客人看不到也不需要看到廚房內部,正如資訊隱藏把實作細節擋在介面之外。
本節字彙
為什麼要研究演算法
同一個問題常有多種解法,學會分析與比較演算法的好壞,並認識難解 (intractable) 問題與取捨。
深度探秘
同一題,多種解法
解法不只一種
電腦科學家是從經驗中學習的:看別人解題、自己動手解題,累積出模式辨識 (pattern recognition) 的能力,下次遇到類似問題就能更快上手。
關鍵體悟是:同一個問題往往有很多不同的演算法。
以前面的 sqrt 為例,計算平方根可以有很多種實作方式:
- 有的演算法用掉少很多的資源。
- 有的演算法可能要花10 倍的時間才回傳結果。
兩個都『能跑、都對』,但其中一個顯然更好。
我們需要一把尺
既然解法不只一種,我們就需要一套方法來比較與評估它們的好壞。
同一問題常有多種演算法,效率可能天差地遠,所以我們需要方法來比較好壞。
生活妙喻
從家到公司的多條路線
導航 App 的多條路線
『從家到公司』是一個問題,而每一條路線都是一個演算法:
- 走高速公路:里程長但通常快。
- 走市區小路:里程短但一堆紅綠燈。
- 走山路:風景好但耗油又耗時。
每一條路線都能到達公司(都正確),但成本(時間、油錢)差很多。導航 App 做的事,就是幫你比較與評估這些路線。
公平的比較
更重要的是:我們想比較的是路線本身的特性(距離、紅綠燈數),而不是你今天開的是哪台車或路上塞不塞——也就是要排除掉與『解法本身』無關的干擾因素。
分析演算法,就是只看演算法本身的特性來比較,不被『程式怎麼寫』或『用哪台機器』影響。
多條路線都能到達,但成本不同;分析演算法就是比較路線本身,而非車子或路況。
實用超能力
難解問題與取捨
難解 (intractable) 問題
最糟的情況是:問題有解法,但這個解法需要不切實際的大量時間或資源才能跑完。這種問題稱為難解 (intractable)。
所以我們要能分辨三種狀況:
flowchart TD
P[一個問題] --> A{有演算法嗎}
A -->|有且實用| OK[可有效解決]
A -->|有但太耗資源| HARD[難解 intractable]
A -->|沒有| NO[無解]
取捨 (trade-offs)
真實世界裡常有取捨要做:
- 一個演算法跑得快,但很吃記憶體。
- 另一個省記憶體,但比較慢。
身為解題者,除了會解問題,還要懂解法評估技術:找到一個解法之後,再判斷它是不是一個『好』解法——這件事你會一遍又一遍地做。
要分辨有解、無解與難解三種問題,並學會在時間與空間等取捨間做評估。
從家到公司有很多路線,都能到達但成本不同;分析演算法就是比較路線本身的特性,而非車子或路況。
難解問題有解法,但需要不切實際的時間,就像一條理論存在卻久到沒人走得完的路。
本節字彙
Python 資料型別大複習
int、float、bool 與運算子,以及『變數持有的是資料的參考』這個關鍵觀念。
數值、布林與變數參考
int、float、bool 與運算子,以及『變數持有的是資料的參考』這個關鍵觀念。
深度探秘
int、float、bool 與運算子
Python 的原子型別
Python 把資料看成核心,是物件導向語言。最基本的『原子 (atomic)』型別有:
- int:整數(如 23、-19)
- float:浮點數(如 6.5)
- bool:布林值,只有
True與False
算術運算子的眉角
print(2 + 3 * 4) # 14, 先乘後加
print((2 + 3) * 4) # 20, 括號改變順序
print(2 ** 10) # 1024, ** 是次方
print(6 / 3) # 2.0, 兩整數相除得 float
print(7 // 3) # 2, // 是整數除法 取整
print(7 % 3) # 1, % 是取餘數 modulo
重點:兩個整數用
/相除,結果是浮點數;想取整數部分要用//,想取餘數要用%。
布林與比較
比較運算(==、>、<= 等)的結果是布林值,可用 and、or、not 組成更複雜的判斷:
print((5 >= 1) and (5 <= 10)) # True
print(not (False or True)) # False
int、float、bool 是原子型別;// 取整、% 取餘、** 次方,比較與邏輯運算產生布林值。
生活妙喻
變數是貼在物品上的標籤
變數持有的是『參考』
這是 Python 一個關鍵又容易誤會的觀念:變數持有的不是資料本身,而是指向資料物件的『參考 (reference)』。
把變數想成一張便利貼標籤,資料物件則是倉庫裡的箱子:
the_sum = 0:貼一張寫著the_sum的標籤到『0』這個箱子上。the_sum = the_sum + 1:算出『1』這個新箱子,把標籤撕下來改貼到『1』上。the_sum = True:再把同一張標籤改貼到『True』這個箱子上。
flowchart LR
L[標籤 the_sum] -->|現在指向| B1[資料物件 True]
B0[資料物件 0]
B2[資料物件 1]
動態型別
因為標籤可以隨時改貼到不同型別的箱子上,所以同一個變數可以先是整數、後變布林。這就是 Python 的動態 (dynamic) 特性。
變數像可重貼的標籤,指向資料物件;改值就是改貼標籤,型別也隨之改變。
實用超能力
命名與賦值的好習慣
識別字 (identifier) 規則
變數名稱(識別字)的規則:
- 以字母或底線
_開頭 - 大小寫有別(
Sum與sum是兩個不同名字) - 長度不限
取有意義的名字
# 不好:別人看不懂
x = 3.14159 * r * r
# 好:一看就懂
area = 3.14159 * radius * radius
用能表達意義的名字,程式碼會好讀很多——這是寫給『未來的你』看的禮物。
賦值的運作順序
賦值時,Python 會先算右邊,再把結果物件的參考『指派』給左邊的名字:
the_sum = the_sum + 1 # 先算右邊 舊值+1, 再把標籤改貼過去
識別字以字母或底線開頭、大小寫有別;賦值先算右邊,再把名字指向結果。
變數是貼在資料箱子上的標籤,改值就是把標籤撕下來改貼到另一個箱子,因此同一變數能指向不同型別的資料。
標籤可貼整數箱也可貼布林箱,所以變數型別會隨它當下指向的資料而改變。
本節字彙
有序集合:list、string、tuple
三種有序序列共用索引、切片、串接等操作;list 可變、string 與 tuple 不可變。
深度探秘
三種有序序列
序列家族
Python 有三種有序集合 (ordered collection),又稱序列 (sequence):
| 型別 | 寫法 | 範例 | 可變? |
|---|---|---|---|
| list | 方括號 [] |
[1, 3, True, 6.5] |
可變 |
| string | 引號 '' "" |
'David' |
不可變 |
| tuple | 圓括號 () |
(2, True, 4.96) |
不可變 |
list 與 tuple 是異質的 (heterogeneous)——同一個集合裡可以放不同型別的資料。
序列共用的操作
因為都是序列,它們共用一組操作:
my_list = [1, 3, True, 6.5]
my_list[0] # 索引: 取第 0 個
my_list[1:3] # 切片: 取索引 1 到 2
len(my_list) # 長度
[1, 2] + [3, 4] # 串接 -> [1,2,3,4]
[0] * 6 # 重複 -> [0,0,0,0,0,0]
6.5 in my_list # 成員測試 -> True
索引從 0 開始;切片
[1:3]取到索引 3 之前(不含 3)。
list、string、tuple 都是有序序列,共用索引、切片、串接、重複、len、in 等操作。
生活妙喻
可變 vs. 不可變:白板與石碑
mutability(可變性)
list 與 string/tuple 最大的差別是可變性 (mutability):
- list 是可變的 (mutable):像一塊白板,可以隨時擦掉某格重寫。
- string 與 tuple 是不可變的 (immutable):像刻好的石碑,刻上去就不能改某個字。
my_list[0] = 1024 # OK, list 可改某格
my_name = 'David'
my_name[0] = 'X' # 報錯 TypeError: 不支援 item assignment
為什麼要分這個?
想改某一格內容時,用 list;想要一份保證不會被偷改的資料時,用 tuple 或 string 更安全。這個差別在後面實作資料結構時會非常重要。
list 可變(像白板可改某格),string 與 tuple 不可變(像石碑不能改某字)。
實用超能力
list 方法、range 與小陷阱
常用的 list 方法
my_list = [1024, 3, True, 6.5]
my_list.append(False) # 尾端加一個
my_list.insert(2, 4.5) # 在索引 2 插入
my_list.pop() # 移除並回傳最後一個
my_list.pop(1) # 移除並回傳索引 1
my_list.sort() # 就地排序
my_list.reverse() # 就地反轉
my_list.index(4.5) # 找第一個出現的索引
有些方法(如
pop)會回傳值並修改 list;有些(如reverse)只修改、不回傳。
range 產生數列
list(range(10)) # [0,1,...,9]
list(range(5, 10)) # [5,6,7,8,9]
list(range(5, 10, 2)) # [5,7,9] 每次跳 2
list(range(10,1,-1)) # [10,9,...,2] 倒著走
重複運算子的陷阱
my_list = [1, 2, 3, 4]
A = [my_list] * 3 # A 裝的是 3 個指向同一個 list 的參考
my_list[2] = 45 # 改一個, 三份都跟著變!
因為 * 複製的是參考而非資料本身,這是初學者很容易踩到的坑。
list 有 append/insert/pop/sort 等方法;range 產生數列;重複運算子複製的是參考要小心。
list 像白板可隨時擦改某一格;string 與 tuple 像刻好的石碑,內容固定不能改某個字。
[my_list]*3 像三面鏡子映出同一個人,本人一動三個影像都跟著動,因為它們指向同一份資料。
本節字彙
無序集合:set 與 dictionary
set 不允許重複、支援集合運算;dictionary 以 key-value 配對儲存、用鍵存取。
深度探秘
set:不重複的無序集合
set 是什麼
set(集合) 是一個無序、不允許重複的集合,元素必須是不可變的物件。寫法是用大括號 {} 包起來、逗號分隔:
my_set = {3, 6, 'cat', 4.5, False}
注意:空集合要寫 set(),不能寫 {}(那會變成空字典)。
set 的運算(像數學集合)
my_set = {3, 6, 'cat'}
your_set = {99, 3, 100}
my_set | your_set # 聯集 union
my_set & your_set # 交集 intersection -> {3}
my_set - your_set # 差集 difference
{3, 100} <= your_set # 子集判斷 -> True
len(my_set) # 元素個數
3 in my_set # 成員測試
還有 add、remove、pop、clear 等方法。因為不重複,set 很適合用來『去除重複』。
set 是無序、不重複的集合,支援聯集、交集、差集、子集等數學運算;空集合用 set()。
生活妙喻
dictionary:通訊錄查號簿
dictionary(字典)
dictionary(字典) 是無序的集合,由一組組鍵-值配對 (key-value pair) 組成,寫成 key:value:
capitals = {'Iowa': 'DesMoines', 'Wisconsin': 'Madison'}
print(capitals['Iowa']) # 用鍵查值 -> DesMoines
capitals['Utah'] = 'SaltLakeCity' # 新增一對
像一本通訊錄
把字典想成通訊錄查號簿:
- 鍵 (key) 是『人名』(Iowa、Wisconsin)。
- 值 (value) 是對應的『電話/首府』(DesMoines、Madison)。
- 你用人名(鍵)去查電話(值),而不是用編號。
flowchart LR
K1[鍵 Iowa] --> V1[值 DesMoines]
K2[鍵 Wisconsin] --> V2[值 Madison]
字典沒有固定順序,鍵的擺放位置取決於一種叫雜湊 (hashing) 的技術(第 4 章會細講)。
dictionary 用鍵-值配對儲存,像通訊錄用人名查電話;無固定順序,靠雜湊決定位置。
實用超能力
字典的方法與安全取值
常用操作與方法
phone_ext = {'david': 1410, 'brad': 1137}
'brad' in phone_ext # 鍵是否存在 -> True
del phone_ext['david'] # 刪除一對
list(phone_ext.keys()) # 所有鍵
list(phone_ext.values()) # 所有值
list(phone_ext.items()) # 所有 鍵,值 配對
get:安全地取值
直接用 my_dict[k] 取一個不存在的鍵會報錯。get 比較安全:
phone_ext.get('kent') # 鍵不存在 -> 回傳 None, 不報錯
phone_ext.get('kent', 'NO ENTRY') # 鍵不存在 -> 回傳預設值 'NO ENTRY'
小技巧:當你不確定鍵在不在時,用
get搭配預設值,可以避免程式因 KeyError 中斷。
用 in 判斷鍵、del 刪除、keys/values/items 取內容;get 可安全取值並設預設,避免報錯。
用人名(鍵)查電話(值),而非用流水號;字典也是用鍵去查對應的值。
同一個人不管被邀請幾次,名單上只會出現一次,正如 set 自動去除重複元素。
本節字彙
Python 控制流程與程式組織
input 讀進來都是字串、print 的彈性輸出,以及用格式運算子排版輸出。
輸入輸出與字串格式化
input 讀進來都是字串、print 的彈性輸出,以及用格式運算子排版輸出。
深度探秘
input 讀進來永遠是字串
input 函式
程式常需要跟使用者互動。Python 用 input 函式請使用者輸入資料,它接受一個字串參數作為提示 (prompt):
user_name = input('請輸入你的名字:')
使用者打的東西會存進 user_name。
關鍵陷阱:回傳的永遠是 str
input回傳的永遠是字串,即使使用者打的是數字!
想當數字算術,必須自己明確轉型:
user_radius = input('請輸入半徑:')
radius = float(user_radius) # 字串轉浮點數
diameter = 2 * radius # 現在才能算術
如果忘了轉型直接 user_radius * 2,會變成把字串重複兩次而不是乘以二,這是很常見的 bug。
input 回傳的永遠是字串,要做數字運算前必須用 int() 或 float() 明確轉型。
生活妙喻
print 像可調整的印章
print 的彈性
print 可印零個或多個值,預設用一個空白分隔、結尾換行。這兩個行為都能調:
print('Hello', 'World') # Hello World
print('Hello', 'World', sep='***') # Hello***World
print('Hello', 'World', end='***') # Hello World***(不換行)
像一台可換設定的印章機
把 print 想成一台印章機:
sep(分隔):決定多個字之間蓋什麼分隔符(預設是空格)。end(結尾):決定每次蓋完印章後接什麼(預設是換行)。
換掉這兩個設定,就能控制輸出長相,而不必改動要印的內容本身。
print 可印多個值,用 sep 調整分隔符、end 調整結尾(預設換行),靈活控制輸出外觀。
實用超能力
格式字串:填空模板
格式字串 (formatted string)
想更精細控制輸出排版時,用格式字串:一個帶佔位符的模板,再把變數填進去。% 是格式運算子。
name = 'Ann'
age = 18
print('%s is %d years old.' % (name, age))
# Ann is 18 years old.
- 左邊是模板,含轉換規格:
%s(字串)、%d(整數)、%f(浮點數)。 - 右邊是要填入的值(依序對應)。
加上格式修飾子
在 % 與轉換字元之間可加修飾子,控制欄寬與小數位:
item = 'banana'; price = 24
print('The %+10s costs %5.2f cents' % (item, price))
# %5.2f: 欄寬 5、小數 2 位 -> 24.00
| 規格 | 意思 |
|---|---|
%d |
整數 |
%f |
浮點數 |
%s |
字串 |
%5.2f |
欄寬 5、小數 2 位 |
右邊值的個數要與模板中
%的個數一致,否則會出錯。
格式字串用 % 把變數填進模板,%s/%d/%f 指定型別,並可加修飾子控制欄寬與小數位。
sep 決定字與字之間蓋什麼分隔、end 決定每次印完接什麼,換設定就能改變輸出外觀而不動內容。
格式字串像一張留好空格的模板,%s、%d 是空格的型別標示,右邊的值依序填進去。
本節字彙
迭代與選擇
while 與 for 兩種迴圈、if/else 選擇,以及一行搞定的串列推導式。
深度探秘
兩種迴圈:while 與 for
迭代 (iteration)
演算法需要『重複』的能力。Python 有兩種迴圈:
while:條件成立就一直做
counter = 1
while counter <= 5:
print('Hello, world')
counter = counter + 1
每次重複前都會先檢查條件,True 才執行本體。條件可以是複合的:
while counter <= 10 and not done:
...
for:走訪序列或 range
for item in [1, 3, 6, 2, 5]:
print(item)
for item in range(5): # 定次迭代 0~4
print(item ** 2)
for 可走訪任何序列(list、tuple、string),也常搭配 range 做定次迭代 (definite iteration)。
注意 Python 用縮排而非大括號來標示迴圈本體,縮排是語法的一部分。
while 依條件重複(每次先檢查),for 走訪序列或用 range 做定次迭代;縮排標示本體。
生活妙喻
選擇就是路口的紅綠燈
選擇 (selection)
選擇讓程式問問題、再依答案做不同的事。把它想成路口的紅綠燈與分岔路。
if/else:雙向分岔
if n < 0:
print('抱歉,值是負數')
else:
print(math.sqrt(n))
像路口問『紅燈嗎?』——是就停,否就走,兩條路擇一。
巢狀選擇:一連串的分岔
if score >= 90:
print('A')
else:
if score >= 80:
print('B')
else:
if score >= 70:
print('C')
else:
print('D 或 F')
單向 if:只有『要不要做』
if n < 0:
n = abs(n) # 是負數才取絕對值
print(math.sqrt(n)) # 不管怎樣都會執行這行
單向 if 像『前方施工才繞道,否則直接直行』。
if/else 是雙向分岔、可巢狀串接多個條件;單向 if 只決定『要不要做某事』,之後照常往下。
實用超能力
一行搞定的串列推導式
串列推導式 (list comprehension)
當你想『用迭代+選擇建立一個新 list』,串列推導式能把好幾行濃縮成一行。
從迴圈版到推導式版
# 迴圈版:前 10 個完全平方數
sq_list = []
for x in range(1, 11):
sq_list.append(x * x)
# 推導式版:一行搞定
sq_list = [x * x for x in range(1, 11)]
# [1, 4, 9, ..., 100]
加上篩選條件
sq_list = [x * x for x in range(1, 11) if x % 2 != 0]
# 只留奇數的平方 -> [1, 9, 25, 49, 81]
結尾加 if 條件 就能只把符合的項目放進新 list。
[ch.upper() for ch in 'comprehension' if ch not in 'aeiou']
# ['C','M','P','R','H','N','S','N'] 去掉母音並轉大寫
任何支援迭代的序列都能用在推導式裡,是非常實用、Pythonic 的寫法。
串列推導式把迭代與選擇濃縮成一行來建立新 list,結尾加 if 可篩選項目。
程式在路口問一個問題,依答案走不同的路;if/else 是雙向擇一,巢狀則是一連串分岔。
for 走訪序列就像老師拿點名簿一個個唱名,每個元素都被輪到處理一次。
本節字彙
例外處理
分清語法錯誤與邏輯錯誤,用 try/except 攔截例外,用 raise 主動拋出例外。
深度探秘
兩種錯誤:語法 vs. 邏輯
寫程式常見的兩類錯誤
語法錯誤 (syntax error)
程式碼結構寫錯,違反語言規則,連跑都跑不起來。例如 for 後面忘了冒號:
>>> for i in range(10)
SyntaxError: invalid syntax
剛學語言時最常犯這種錯。
邏輯錯誤 (logic error)
程式跑得起來,卻給出錯的結果——可能是演算法本身有問題,或翻譯成程式時出錯。
有些邏輯錯誤更嚴重,會在執行時引發致命狀況,例如除以零、或存取超出範圍的索引。這類執行期錯誤就稱為例外 (exception)。
flowchart TD
E[寫程式的錯誤] --> S[語法錯誤 結構寫錯 跑不起來]
E --> L[邏輯錯誤 跑得起來但答案錯]
L --> X[執行期例外 如除以零 索引越界]
語法錯誤是結構寫錯跑不起來;邏輯錯誤是跑得起來卻答案錯,嚴重時引發執行期例外。
生活妙喻
try/except 是安全網
例外被『raise(拋出)』
當例外發生,我們說它被**『raise(拋出)』了。預設情況下,例外會讓程式中斷**。
例如對負數開根號:
>>> print(math.sqrt(-23))
ValueError: math domain error
try/except:接住失誤的安全網
你可以用 try 區塊『嘗試』做某事,再用 except 區塊**『接住』萬一發生的例外,像走鋼索下方的安全網**:
try:
print(math.sqrt(a_number))
except:
print('開根號的值不合法')
print('改用絕對值')
print(math.sqrt(abs(a_number)))
如果 sqrt 拋出例外,程式不會中斷,而是掉進 except 區塊處理善後,再繼續往下跑。
安全網的價值:即使踩空了,表演(程式)也能優雅地繼續,而不是當場墜落(崩潰)。
例外發生時會被 raise,預設讓程式中斷;用 try/except 可像安全網般接住例外並優雅善後。
實用超能力
主動 raise 自己的例外
你也可以主動拋出例外
除了被動接住,程式設計師也能用 raise 主動拋出例外——當你偵測到一個不該發生的狀況時:
if a_number < 0:
raise RuntimeError('不可以用負數')
else:
print(math.sqrt(a_number))
這時程式一樣會因為例外而終止,但這個終止是你刻意設計的,而且訊息清楚說明了原因。
為什麼這是超能力
- 提早攔截:在錯誤資料造成更大災難前就喊停。
- 訊息明確:自訂的例外訊息讓除錯快很多。
- 除了
RuntimeError,Python 還有ValueError、TypeError等許多內建例外型別可用,也能自訂。
主動 raise 等於在程式裡裝『警報器』:一偵測到不合理的狀況,立刻響鈴並說清楚哪裡出問題。
用 raise 可主動拋出例外,在偵測到不合理狀況時提早喊停並給出清楚的錯誤訊息。
try 區塊是走鋼索,except 是下方的安全網;萬一失誤(拋出例外),網子接住你,表演還能繼續而非墜落崩潰。
偵測到不合理的狀況就主動拉警報(raise),立刻喊停並說明原因,避免錯誤資料釀成更大災難。
本節字彙
函式與類別
用函式隱藏計算細節,用 class 定義新型別來實作 ADT,認識建構子與 self。
深度探秘
用函式把細節裝進盒子
定義函式
函式讓我們把一段運算的細節藏起來(還記得程序抽象化嗎?)。一個函式定義需要:
- 名稱
- 一組參數 (parameters)
- 本體 (body)
- 可選擇地回傳 (return) 一個值
def square(n):
return n ** 2
square(3) # 9
square(square(3)) # 81 回傳值可再傳入
n 是形式參數 (formal parameter);呼叫時傳入的 3 是實際參數 (actual parameter)。
用函式實作演算法
def square_root(n):
root = n / 2 # 初始猜測為 n 的一半
for k in range(20): # 牛頓法 反覆逼近 20 次
root = (1 / 2) * (root + (n / root))
return root
牛頓法的更新公式為:
$$new_guess = \frac{1}{2}\left(old_guess + \frac{n}{old_guess}\right)$$
使用者只要呼叫 square_root(9) 得到 3.0,完全不需要知道牛頓法的細節——這就是抽象化的威力。# 之後的文字是註解,會被忽略。
函式有名稱、參數、本體與可選回傳值,能把運算細節藏進盒子,呼叫者不必懂內部實作。
生活妙喻
類別是『餅乾模具』
用 class 定義新型別
物件導向最強的能力,是讓你自訂新的類別 (class) 來模擬要解的資料。回想前面:ADT 描述資料的狀態 (state) 與行為 (method),而用 class 實作 ADT,就把抽象變成可用的東西。
餅乾模具與餅乾
把 class 想成餅乾模具,把 object(物件) 想成用模具壓出來的一塊塊餅乾:
- 模具(class)定義了餅乾長什麼樣、能做什麼。
- 每壓一次就產生一個實例 (instance)——一塊獨立的餅乾(object)。
class Fraction:
def __init__(self, top, bottom):
self.num = top # 狀態 分子
self.den = bottom # 狀態 分母
flowchart LR
C[class Fraction 模具] -->|壓一次| O1[物件 三分之五]
C -->|再壓一次| O2[物件 二分之一]
同一個模具能壓出無數塊餅乾,每塊餅乾有自己的狀態(各自的分子分母)。
class 像餅乾模具,定義資料的狀態與行為;每個 object 是壓出來的一塊獨立餅乾(實例)。
實用超能力
建構子 __init__ 與 self
建構子 init
所有類別都該提供的第一個方法是建構子 (constructor),它定義物件如何被建立。在 Python 中建構子固定叫 __init__(init 前後各兩個底線):
class Fraction:
def __init__(self, top, bottom):
self.num = top
self.den = bottom
my_fraction = Fraction(3, 5) # 呼叫類別名 即觸發建構子
注意:我們用類別名來建立物件,而不直接呼叫 init。
self 是什麼
self是特殊參數,永遠是第一個形式參數,代表物件自己。- 呼叫時不需要手動給 self 傳值。
self.num = top表示『在這個物件身上,建立一個叫 num 的內部狀態』。
為什麼物件要會『自我介紹』
直接 print(my_fraction) 會印出像 <__main__.Fraction object at 0x...> 這種看不懂的東西,因為物件還不知道怎麼把自己變成漂亮的字串。要讓它好好顯示(如印成 3/5),就得告訴類別怎麼把自己轉成字串——這正是用方法擴充物件行為的起點。
建構子 __init__ 定義物件如何被建立並初始化狀態;self 永遠是第一個參數,代表物件自己。
class 是模具,定義餅乾的樣子與功能;每壓一次就產生一塊獨立的餅乾(object),各有自己的狀態。
你投錢按鈕(給參數)就拿到飲料(回傳值),不必知道機器內部怎麼運作,正如函式把實作藏在裡面。