【Python基礎教學】遞迴(下)&陣列&類別【part-11】
【Python基礎教學】遞迴(下)&陣列&類別【part-11】
哈囉大家好,很感謝你點進本文章,我是一名高中生,是電腦社社團的社長,由於我並不是 Python 這方面非常精通的專家,所以若文章有些錯誤請見諒,也可向我指正錯誤。另外本文章的用意是作為電腦社社團的教材使用而編寫的。
上次我們講到了基本演算法的概念跟介紹,以及遞迴的概念及實作,今天讓我們繼續完成遞迴的實作,以及說明遞迴的限制及複雜度問題,順便跟大家介紹類別語法。
接下來,讓我們進入正題。
遞迴(Recursion)
遞迴,簡單來說,就是一個函數呼叫自己的函數,像是這樣:
1 | def a(b): |
上次我們講解遞迴時,以階乘跟費氏數列當作例子進行實作,同時也替大家複習了高一下數學學過的遞迴關係式,我們可以利用這個遞迴關係式來進行解題。
另外遞迴的特點是淺顯易懂,且設計方便簡易,能夠將大問題分解成多個子問題,這凸顯了演算法中的分治法(Divide and Conquer)之精神。
分治法的意思是把一個複雜的問題分成兩個或更多的相同或相似的子問題,直到最後子問題可以簡單的直接求解,這點是不是跟遞迴很像呢?
最大公因數(GCD:Greatest Common Factor)
說到最大公因數,相信這是大家在國小、國中階段時共同擁有的回憶,雖然各位可能知道最大公因數的定義,但我在這邊還是再重述一次。
最大公因數:取兩整數,找到兩整數間的最大因數。
例如:整數 3, 9,他們之間的最大公因數是 3。
我們一般在尋求最大公因數時,通常都會使用短除法這個工具來求最大公因數,但到了程式語言,就有很多種不同的方式能夠求解最大公因數,例如:遞迴。
在程式語言中,我們通常會使用輾轉相除法(Euclidean Algorithm:又稱歐幾里得演算法)來求解最大公因數的題目。
輾轉相除法只要反覆進行除法,就能求出最大公因數。即使運算的兩個數很大,也能用明確步驟有效率地求出最大公因數。
以下是使用 while 迴圈來求最大公因數的程式碼:
1 | def gcd(a, b): |
若以 a = 9、b = 3,直接帶入來看的話,其實早就在第一步判斷的時候就已經輸出結果了,相當的快速,以上的方法就是輾轉相除法。輾轉相除法其實就是在求兩數除法後的餘數,再將所得的餘數跟上一次除法的除數進行除法,這樣一直除、除到最後餘數 = 0 時,即為最大公因數。
例如:gcd(20, 70) -> 70 / 20 -> 得到餘數 10 -> 20 / 10 -> 餘數 0 -> 代表 10 即為最大公因數
接下來,讓我們談談有關於最大公因數的遞迴程式:
1 | def gcd(m, n): |
用遞迴的方式來重新檢視輾轉相除法後,是不是覺得直觀多了呢?剛剛前面所說:輾轉相除法是將兩數除法過後的餘數,餘數再來跟上一次除法中的除數進行除法,以此類推。
在上面的遞迴程式中,我們可以將 m 視為被除數,n 為除數,至於條件的部分我相信各位有眼睛都看得出來這是什麼意思,所以我們不多做解釋。最下面是 gcd(n, m%n),也是我們前面所說的,將上一次除法過後的除數換到被除數的位置,然後上一次的除法之餘數值,放到除數的位置,也就形成了「除數 / 餘數」的情形。
透過遞迴不斷縮小問題的特性,我們最後可以得知條件若偵測 n = 0(m%n = 0)就代表餘數 = 0,那麼 m 就是最大公因數了。
陣列(Array)
陣列(Array),它的功能基本上來說就跟列表沒什麼大不同,但差別就在於它的執行效率比列表來得快多了。
以下是陣列的特性:
- 陣列不像是列表,在陣列中的資料其資料型態(data type)要一致。
- 陣列和列表一樣,每個元素在記憶體裡是連續存放的。
- 陣列的每個元素有自己的索引(unique index)方便索引。
陣列中的資料我們稱其為元素(Element),這點與列表是相同的。
陣列基礎操作
由於陣列的各種存取、插入等方式在我們前面學習列表時十分相似,在這邊我就不再贅述其原理,直接進入實作。
在我們使用 array 之前,我們須引入 array 模組:
1 | from array import * |
當我們引入模組後,就可以準備來建立 array 囉!以下是 array() 方法的格式:
1 | array(typecode[, initializer]) |
typecode -> 指所建立陣列的資料型態,第二個參數是指所建立的陣列內容(放入資料)。
有關於 typecode 的部分,在 array 有特殊用法,以下是關於 typecode 的表格:
| Type code | 資料型態 | 長度(byte) | 說明 |
|---|---|---|---|
| ‘b’ | int | 1 | 1 個 byte “有”號整數 |
| ‘B’ | int | 1 | 1 個 byte “無”號整數 |
| ‘h’ | int | 2 | 有號短整數 signed short |
| ‘H’ | int | 2 | 無號短整數 unsigned short |
| ‘i’ | int | 2 | 有號整數 signed int |
| ‘I’(大寫i) | int | 2 | 無號整數 unsigned int |
| ‘l’(小寫L) | int | 4 | 有號長整數 signed long |
| ‘L’ | int | 4 | 無號長整數 unsigned long |
| ‘q’ | int | 8 | 有號長長整數 signed long long |
| ‘Q’ | int | 8 | 無號長長整數 unsigned long long |
| ‘f’ | float | 4 | 浮點數 float |
| ‘d’ | double | 8 | 浮點數 double |
表格來源:書籍《演算法:圖解邏輯思維 + Python程式實作王者歸來》。
以下是一個建立陣列,並且迭代陣列並印出的程式碼:
1 | from array import * |
若我們要存取一個陣列值,我們可以直接存取它所在的索引位置:
1 | from array import * |
若我們要將資料插入陣列當中,可以使用 insert() 方法(基本上與列表相同):
1 | insert(i, x) |
說明:在索引 i 位置中插入 x 資料。
以下是針對方法 insert() 的範例:
1 | from array import * |
同樣地,我們也可以使用方法 append() 在陣列尾端插入資料,大家可以自己試試看。
若我們要刪除陣列元素,可以使用 remove(x) 方法或 pop(i) 方法來進行刪除。需要注意的是 remove() 括號內是針對資料來進行刪除,而 pop() 是針對資料的索引位置來進行刪除。
以下是一個範例:
1 | from array import * |
輸出結果:
1 | 2 |
若我們想要搜尋陣列元素的話呢,其實也跟列表的方法是一樣的,我們可以使用 index(x) 方法來搜尋資料所在的索引位置,在這邊我們就不再多示範了,還請各位自行練習。
至於修改陣列內容的部分,也是跟列表一樣的,能夠進行修改,方式也都一樣,像下面這樣:
1 | x[0] = 100 |
其他陣列模組(Numpy)
最後我們再來講解一個在我們科學及人工智能領域內常用的模組:Numpy,裡面的陣列運算會是我們比較常用到的。
如果我們要使用 numpy 模組,我們可以這樣寫:
1 | import numpy |
不過 numpy 這個名字似乎有點長,我們可以讓它簡寫為 np,這樣的動作只要加一個 as 即可,以便在撰寫方法等作業上較為方便。
1 | import numpy as np |
這樣我們就可以建立一個陣列了:
1 | import numpy as np |
接下來的方法其實都跟我們上面所學都差不多,但仍有些部分還是不太相同的,以下的網站給各位同學作為課後補充,有興趣的人可以自學看看:Array/ Python List 陣列與列表 - iT 邦幫忙::一起幫忙解決難題,拯救 IT 人的一天
類別(Class)
在我們撰寫 Python 程式時,都一定會使用到模組(module)這個東西,我們平常都使用 import 或 from … import 指令將模組給導入到我們現在的程式當中。
那麼 Class 的功用基本上就是用於建構出一個模組化的程式碼,然後能夠導入到其他程式碼上進行使用。
當我們創建一個模組時,就等同於還要新增一個 .py 文件來撰寫模組,我們可以在搜尋引擎中搜尋 python 的官方模組文件,例如打上:random.py github,就會出現 python 官方模組文件的開源碼了。github 開源:Python-2.7.3/Lib/random.py
ok,問題來了,Class 到底是什麼呢?簡單來說 Class 就是模組本身,能夠定義相同特性的物件。
白話一點,我們可以將 Class 想像成是一個模型,比如說一個星星的模型好了,然後我們可以在裡面加入奶油或其他食材,放進烤箱烤完後就變成一個星星形狀的蛋糕了。
如果我們有十個星星的模型,那麼產出的結果就有十個星星形狀的蛋糕,當然你也可以使用除了星星以外的形狀,這就需要由我們在 Class 下面進行定義跟宣告(函數)。
光說這個例子你可能還是不太清楚,接下來讓我們以手機型號的例子來做說明:
我們要對手機本身的規格做「分類」,而「手機」本身就是在我們分類的樹狀圖的最頂層,做分類這件事情,我們可以針對手機的名稱、處理器、圖形處理器、記憶體容量、儲存空間等來做分類,而名稱、處理器等等這些都是屬於手機本身的屬性或特性,我們針對手機規格進行分類這件事情,也就是 Class 正在做的事情。
以下我們舉 iphone 15 為例:
假設我們有一台手機叫做 iphone 15,我們想要對他的規格進行一個分類:
iphone 15 規格(1):容量 -> 128GB、256GB、512GB
iphone 15 規格(2):晶片 -> A16 仿生晶片
iphone 15 規格(3):CPU -> 6 核心,包含 2 效能核心、4 節能核心
iphone 15 規格(4):GPU -> 5 核心
…
還有更多規格我們就不一一列舉,接下來我們就利用 iphone 15 的規格來寫一個程式吧!
類別與方法(Class&Method)
以下是類別的語法定義:
1 | class ClassName: |
1 | class phone(): |
類別有一個名為
__init__()的特殊方法(建構子、建構方法、構造方法),該方法在類別實例化時會自動調用,像下面這樣:
1 | def __init__(self): |
:::success
建構子(Constructor):用來初始化物件的特殊方法。如 Python 中的 __init__()
:::
這個 init 其實就是英文中的 initialization,也就是初始化的意思,初始化這個名詞我們常常會在程式語言領域中經常看到,初始化代表著剛開始的意思,所以我們會在 __init__ 裡面設置一些基本設定,這些設定就由你自己去定義。
比如說,你正在開發一個遊戲,創建了一個 Player 類別,而在 __init__ 函數裡面,你一定會給予 Player 的基本屬性嘛,像是給他血量、魔力值、金錢等等這些屬性,如果寫成程式碼的話會像是下面這樣:
1 | class Player(): |
至於剩下的內容就由各位同學發揮你的想像力跟創意,由你們來自行添增。
而上面我們在說明 __init__ 方法時,說到「實例化」類別,許多同學可能對於這個名詞會看不太懂或是完全一頭霧水,其實這個實例化非常簡單,我們再往回看 iphone 15 的範例:我們可以看到有個變數叫做 i15,賦值為 iphone15(256, “A16”, 6, 5),這一條式子就叫做實例化類別。
:::success
實例化(Instantiation):指基於一個 Class 創建一個實際的物件(實例)的過程。
因為在 Class 裡面的任何物件、東西都只是我們「設計」、「想」出來的,並沒有實際去「生產」、「創造」它。像是函數內的任何運算等,我們必須要「呼叫」函數才可以進行運算,而實例化就有點像是呼叫函數的概念,把 Class 內的物件(實例)創造出來。
簡言之:實例化就是把 Class 裡面的物件「創造」出來,因為 Class 內的任何東西向函數一樣,是被我們自己設計出來的,要實際用到這些物件就必須把它實例化。
:::
類別的方法與普通的函數只有一個特別的區別-它們必須有一個額外的第一個參數名稱,按照慣例它的名稱是 self。
註:方法(method)就是類別底下中的那些函數(def)。
self 從英文字面上來看,代表自我,我們可以將 self 這個概念想像成是一個「自我」的標籤,self 能夠讓物件知道自己到底是誰。而 self 通常被放在方法參數的最前面,它是一個約定俗成的名稱,你也可以取其它名字,不過建議還是用 self,因為 self 是 Python 社群的標準,通常不會變。
說到物件,在這邊作者我要再次強調,在 Python 中,所有東西都是物件,資料型態本身就是一個物件,Class、Function 等也都是一個物件。
小結
- 類別語法定義:
1 | class ClassName: |
__init__方法:稱為建構子(Constructor),在 class 中最為常見也是最為常用的一個方法,用於設定初始屬性。我們在前面透過例子:Player 類別,在__init__你可以為 Player 設定初始能力值,像是 hp、mp、coin、attack 等。方法(Method):在類別底下的函數。
實例(Instance):基於某個類別(Class)創建的具體物件。你可以想像成是類別版本的物件。
實例化(Instantiation):依據類別而創建物件的過程。基本上就是將類別賦值給變數 t。
1 | t = Test() |
- 創建方法時,與普通函數不一樣的是,第一個參數「必須」寫上 self。(很重要!但是 self 寫法並不是唯一的,你也可以寫其他名稱替代 self,但建議都寫 self,為的是程式碼的可讀性跟一致性,還有這是 Python 社群統一的標準)
類別(Class)就像是一張藍圖,是被我們設計、發想出來的,並沒有被我們實際創造出來,而要實際創造這些物件(實例)出來,就必須要將其實例化。
補充:類的私有屬性與私有方法
__private_attrs:兩個底線開頭,宣告該屬性為私有,不能在類別的外部被使用或直接存取。在類別內部的方法中使用時,self.__private_attrs。
__private_method:兩個底線開頭,宣告該方法為私有方法,只能在類別的內部呼叫,不能在類別的外部呼叫。self.__private_methods。
以下是類的專屬方法:
__init__: 建構子(又稱構造函數,Constructor),在產生物件時呼叫,簡言之:初始化專用。__del__: 解構子(又稱析構函數,Destructor),釋放物件時使用。__repr__: 列印,轉換。__setitem__: 依照索引賦值。__getitem__: 依照索引取得值。__len__: 獲得長度。__cmp__: 比較運算。__call__: 函數呼叫。__add__: 加運算。__sub__: 減運算。__mul__: 乘運算。__truediv__: 除運算。__mod__: 求餘運算。__pow__: 乘冪。
以下是一個範例,說明了以上所有方法:
1 | class MyClass: |
繼承(Inheritance)
繼承(Inheritance),就如同字面上的意思,在程式中,我們會以父子關係來稱呼,如:父類別(基本類別:BaseClassName)、子類別(派生類別:DerivedClassName)。而繼承就是子類別可以沿用父類別的所有物件及方法。
派生(Derived):通常指的是從一個已存在的類別(Class)創建一個新的類別(Subclass),新的類別會繼承原有類別的特性,並可以增加或修改特性。
以下是一個範例(範例引用自:繼承 inheritance - Python 教學 | STEAM 教育學習網):
1 | class father(): # fatehr 類別 |
首先我們來看到父類別,也就是 father() 的部分,他只有設置 __init__ 方法而已,然後裡面設有初始屬性 eye、ear、nose、mouth。
到了子類別,也就是 son(),你可以看到他的括號內多加了一個 father,使得變成 class son(father),這就觸發了繼承的機制。
剛剛說了,繼承能夠沿用父類別的所有物件跟方法。觸發繼承機制的條件就是在括號內加入那個類別,而加入了以後,那一個類別就會變成子類別。
繼承過後,兒子不一定要全部依靠財產過活嘛,對不對XD,我們也可以做其他的事情,比如說上面的範例中就額外加入了 language 方法在裡面。如果我們不使用繼承的話,程式碼可能會像以下這樣冗長:
1 | class father(): |
如果所繼承的父類別的資料量是多麼的龐大,那麼子類別不用繼承的話後果可顯而見。
繼承時會覆寫方法
在繼承時,如果子類類別裡某個方法的名稱和父類別相同,則會完全複寫父類別的方法,下面的程式碼,son 類別使用了 init 的方法,就覆寫了原本 fatehr 的 init 方法,導致讀取 oxxo.ear 時發生錯誤(因為 son 的方法裡不存在 ear 的屬性)。
1 | class father(): |
簡單來說就是一個類別裡面容不下兩個相同名稱的方法,像是變數也是一樣,假設有個變數 a:
1 | a = 1 |
由於程式是由上往下執行的,所以後面相同名稱的變數會取代前面變數的值,印出來也理所當然的就是 2 了。
但是如果我們想繼承父類別 father,又不想要被覆寫的話,我們可以使用 super() 函數。
在上面類別的範例當中,我們只需要在子類別 class son(father) 的 __init__ 方法加入
1 | super().__init__() |
即可,這樣就不會受到覆寫的影響了。
多重繼承(MI:Multiple Inheritance)
Python 同樣有限的支援多繼承形式。多重繼承的類別定義形式如下例:
1 | class DerivedClassName(Base1, Base2, Base3): |
以下是一個多重繼承的範例(範例改自菜鳥教程):
1 | class people: |
這個範例中,定義了四種類別:people、student、programmer、sample。
我們一行一行來看:首先第一行定義了類別 people,具有三個屬性:name、age 跟 __weight。其中 __weight 為私有屬性,只能在類別內部存取。
在 people 類別中也定義了 __init__ 構造方法,用於給實例屬性設置初始值。
看到這裡大家肯定會有一個疑惑:為什麼不要只寫 __init__ 就好,而要額外寫 name、age、__weight 這三個屬性呢?
因為 name、age、__weight 是類別屬性,屬於類別,所有在類別裡面的實例都會共享這三個屬性,如果我們去改變了這三個屬性的值,那麼所有實例都會被這三個值給影響到。
而在 __init__ 方法中定義的屬性是實例屬性,它們是實例的一部分,每個實例都有自己的一份屬性。如果我們改變了一個實例的實例屬性,那麼它只會影響到那一個實例而已。
這個就需要說到命名空間(NameSpace)的全域變數(global variable)跟局域變數(local variable)了,不過礙於篇幅關係,我們下一篇在說。
然後繼續看到 people 類別中還有一個方法叫 introduction(self),會印出 __init__ 的初始值。
接下來我們看到 student 類別,這個類別繼承自 people 類別,並新增了一個新的屬性 grade。另外它還覆寫了父類別的 introduction 方法,讓輸出包含了年級的資訊。
而 programmer 類別,什麼都沒有繼承,是等下用於多重繼承使用的類別,不過剩下的代碼基本上都跟上面的解釋是差不多的。
之後就是我們多重繼承的重點:sample 類別了,不過要注意的是多重繼承是有順序先後的,可以看到 sample 先繼承了 programmer,之後才繼承 student。
sample 類別在初始化時會分別呼叫這兩個父類別的構造方法(__init__)。由於 student 和 programmer 類別都有 introduction 方法,所以在 sample 類別中呼叫 introduction 方法時,會優先呼叫在括號中排在前面的父類別的方法。
而在最後程式碼創建了一個 sample 類別的實例叫做 test,並呼叫 sample 類別中的 introduction 方法。因為 sample 類別在定義時先繼承了 programmer 類別,所以這裡呼叫的是 programmer 的 introduction 方法。
物件導向程式設計(OOP:Object-oriented programming)
物件導向程式設計(Object-Oriented Programming,簡稱 OOP)是一種以「物件」為基礎的程式設計範式。強調使用「類別」和「物件」來組織程式碼,以模擬和處理現實世界中的事物。OOP 是一種更具結構性、模組化和可擴展性的方式來開發軟體。
類別(Class)就是實現物件導向程式設計的工具。
一個物件(Object)會有以下兩種性質:
- 屬性(Attribute):物件的特徵或狀態。
- 方法(Method):物件可以執行的操作或行為。
而類別(Class)的功用就是要去定義一個物件的屬性及方法,如以下程式碼就說明了一切:
1 | class phone(): |
建構子(Constructor -> 指 __init__),底下定義一個物件叫 Phone 的屬性,也就是一台手機的名字、儲存空間、使用的晶片等,而 call(self) 則是一台手機的方法,即它的功能、行為等。
總之,物件導向的基本概念如下:
- 物件(Object):為 OOP 的核心單元。物件可以被視為現實世界中的實體,具有屬性(狀態)和方法(行為)。每個物件都是某個類別的實例。
- 類別(Class):創建物件的藍圖或模板,用來定義物件的屬性和方法。前面我們說過,類別裡面的任何東西都是被我們所設計、發想出來的,並沒有實踐出來,所以它基本上就是個設計圖。
- 屬性(Attribute):描述物件特徵、狀態的變數。
- 方法(Method):物件可以執行的操作或行為。
OOP 四大基本特性
- 封裝(Encapsulation):指的是將物件的屬性和方法封裝在內部,使得外部程式無法直接存取或修改。封裝有助於保護資料的完整性,且使得程式碼更容易維護。
- 繼承(Inheritance):指可以從父類別的屬性及方法繼承自子類別,讓程式碼具重複使用和結構化設計的性質。
- 多型(Polymorphism):指不同的類別對相同的方法名稱做出不同的結果。提供更大的靈活性和擴展性。(簡言之:一個事件能有不同結果,比如說滑鼠點擊的事件,點不同的按鈕會有不同結果。)
- 抽象(Abstraction):指隱藏整體系統的複雜性,只需顯現出必要的部分供使用者使用。使用者只需知道如何使用公開的介面(例如方法和屬性)即可,不需要了解其背後的運作細節。(比如說你在用一台飲水機,你會希望看到一堆線路裸露在外面嗎?)
封裝(Encapsulation)
以下是封裝的範例:
1 | class BankAccount: |
封裝目的用於保護程式碼不輕易被外部程式修改,以上建構子的地方,__balance 的部分是私有屬性,也可以改成公開的,也符合封裝的定義。
不懂?把封裝想像成一個保險箱,把錢(物件屬性)塞進去,然後鎖起來,這就是封裝。
繼承(Inheritance)
以下是一個繼承的範例:
1 | class Animal: |
繼承的功能在於能夠很方便的使程式碼重複使用,以上範例的兩種子類別皆繼承自父類別的建構子跟 self.name = name、def speak(self)。
多型(Polymorphism)
以下是一個多型的範例:
1 | from math import pi |
所謂多型就是同一種方法,產生不同的結果,像上面的兩種子類別皆是同一種方法(印出面積),但是礙於圖形的不同(一個是圓形、一個是長方形),所以輸出結果會不同,這就是多型。
抽象(Abstraction)
1 | from abc import ABC, abstractmethod |
:::success
ABC 是 Python 標準函式庫 abc 模組中的一個父類別。ABC 是 “Abstract Base Class” 的縮寫,主要用來定義抽象類別。
當我們創建一個類別且繼承自ABC(class myclass(ABC)),該類別就會成為抽象類別。抽象類別不能被直接實例化。
:::
from abc import abstractmethod:
- abstractmethod:是 abc 模組中的一個裝飾器,用於標記方法為抽象方法。
- 抽象方法(abstract method):指在抽象類別(就是繼承自 ABC 的類別)中定義但不實現(實現可理解為明確、具體要執行的程式)的方法。子類別必須去實現這些抽象方法,變成具體的類別,否則該子類別也會被視為抽象類別,不能被實例化。
看到這裡,你可能心裡會想說:「靠!這是什麼鬼」,沒關係,當初筆者在學這東西的時候也是滿臉問號,那接下來我就來透過以上例子跟你好好敘述一番:
在 Animal 類別中,make_sound 是抽象方法,方法沒有被實現(即啥東西都沒,就放個 pass)。
Dog 和 Cat 類別實現了 make_sound 方法(有明確寫出要執行的東西),這樣就成為具體的類別,可以被實例化。
以下同樣是有關抽象的範例:
1 | from abc import ABC, abstractmethod |
參考資料
abc — Abstract Base Classes — Python 3.12.5 documentation
abc — 抽象類別 — 你所不知道的 Python 標準函式庫用法 03 | louie_lu’s blog
Python 微進階 Day22 - class(類別) - 6 - @abstractmethod、polymorphism - iT 邦幫忙::一起幫忙解決難題,拯救 IT 人的一天
物件導向(Object Oriented Programming)概念 | by Po-Ching Liu | Medium
Python 初學第八講 — 遞迴. 遞迴 Recursion:將大問題切成小問題 | by Yu-Hsuan Chou | ccClub | Medium
Day29:輾轉相除法(Euclidean algorithm) - iT 邦幫忙::一起幫忙解決難題,拯救 IT 人的一天
最大公因數 ( 多個數字 ) - Python 教學 | STEAM 教育學習網
Array/ Python List 陣列與列表 - iT 邦幫忙::一起幫忙解決難題,拯救 IT 人的一天
關於Python的類別(Class)…基本篇. 我覺得Class是Python速成班最重要的一環… | by 張凱喬 | Medium
iPhone 15 與 iPhone 15 Plus - 技術規格 - Apple (台灣)



