2021年4月11日 星期日

p5.js 學習筆記 (二) : 基本繪圖 (上)

布置好 p5.js 的執行環境後就可以開始在畫布上繪圖了, 除了繪圖與互動應用, 其實透過 p5.js 來學習 Javascript 非常有效, 因為它可以將學習轉化成玩, 玩的樂趣無形中會沖淡學習必有的痛苦. 本系列之前的文章參考 :



1. 網頁模板  : 

以下測試將使用上一篇 "環境配置" 中的 CDN 網頁模板 : 

<!doctype html>
<html>
<head>
  <meta charset="utf-8">
  <meta http-equiv="cache-control" content="no-cache">
  <meta name="viewport" content="width=device-width,initial-scale=1">
  <title>p5.js test</title>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.3.1/p5.min.js"></script>
</head>
<body>
  <script>
    function setup() {
      createCanvas(400, 300);
      }
    function draw() {
      background(200, 200, 200);
      }
  </script>
</body>
</html>

p5.js 的程式架構很簡單, 只有兩個函式 (與 Arduino 的程式架構類似), 由只執行一次的 setup() 與預設每秒執行 60 次的 draw() 函式組成, 所以初始設定的指令碼要放在 setup() 內, 而需重複執行的指令碼則放在 draw() 內. 上面的模板網頁中, 如果畫布的背景不需要改變, 則可將 background() 移到 setup() 內. 


2. 偵錯工具 console.log() : 

p5.js 為 Javascript 網頁應用程式的互動繪圖函式庫, 程式開發與結果驗證都是在瀏覽器上, 開發中的偵錯除錯主要是利用 Javascript 的 console.log() 將變數輸出到瀏覽器的控制台觀察變數之值以資判斷, 在 Chrome 瀏覽器上按 F12 (筆電要同時按 Fn 鍵) 會在頁面右邊顯示開發介面, 切到 Console 頁籤即可下達 Javascript 指令 :




例如下面的範例是呼叫 p5.js 的內建函式 frameRate() 取得 draw() 的迴圈頻率, 然後再用 console.log() 輸出此頻率值:



<!doctype html>
<html>
<head>
  <meta charset="utf-8">
  <meta http-equiv="cache-control" content="no-cache">
  <meta name="viewport" content="width=device-width,initial-scale=1">
  <title>p5.js test</title>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.3.1/p5.min.js"></script>
</head>
<body>
  <script>
    function setup() {
      createCanvas(200, 200);
      }
    function draw() {
      background(100);
      console.log(frameRate());      //輸出 draw() 的迴圈頻率預設值
      }
  </script>
</body>
</html>

結果如下 :




可見 draw() 大約每秒會被呼叫 60 次左右. 


3. 內建常數, 變數, 與函式 : 

撰寫 sketch 程式時除了會用到 Javascript 本身的常數, 變數, 與函式外, p5.js 函式庫也提供了很多與繪圖相關的常數, 變數, 與函式, 只要熟悉這些內建函式的呼叫方式與常數變數所代表的意義, 就能用少許的程式碼在畫布上繪圖或產生互動效果. 以下摘要整理常用的常數, 變數, 與函式 : 


常用的內建常數如下表 :

 p5.js 內建常數 說明
 PI 圓周率, 約 3.1415926
 HALF_PI 半圓周率, 約 1.5707963
 QUARTER_PI 四分之一圓周率, 約 0.7853982
 TWO_PI 兩倍圓周率, 約 6.2831853
 DEGREES 角度, 作為 angleMode() 函式之參數, 設定為角度模式
 RADIANS 弧度, 作為 angleMode() 函式之參數, 設定為弧度模式
 CENTER 前兩個參數為圓心, 後兩個參數為寬度 w 與高度 h (或置中對齊)
 RADIUS 前兩個參數為圓心, 後兩個參數為寬度半徑 w/2 與高度半徑 h/2
 CORNER 前兩個參數為左上角座標, 後兩個參數為寬度 w 與高度 h
 CORNERS 前兩個參數為橢圓外方框一角之座標, 後兩個參數為對角座標
 LEFT 向左對齊 (水平, 垂直)
 RIGHT 向右對齊 (水平)
 BOTTOM 向下對齊 (垂直)
 CLOSE 傳入 endShape() 會使形狀之終點與起點相連成封閉圖形


常用的內建變數如下表 :

 p5.js 內建變數 說明
 width 畫布寬度 (px)
 height 畫布高度 (px)
 frameCount draw() 的累計呼叫次數
 mouseIsPressed 滑鼠左鍵是否被按下 (true/false)
 mouseX 滑鼠的 X 座標
 mouseY 滑鼠的 Y 座標


常用的內建函式如下表 :

 p5.js 內建函式 說明
 createCanvas(w, h) 以網頁左上角為起點建立寬 w, 高 h 之畫布 (單位 px)
 background(R [, G, B]) 設定畫布背景色 (值 0~255), 只傳入一個參數時表示 R=G=B
 fill(R [,G, B]) 設定封閉圖形之填滿顏色直到呼叫 noFill() 為止
 noFill() 取消上一次的 fill() 填滿設定
 stroke(R [, G, B]) 設定線條或封閉圖形框邊顏色
 noStroke() 取消上一次 stroke() 所設定之邊框顏色
 strokeWeight(px) 設定線條或封閉圖形框邊寬度 (px)
 point(x, y [, z]) 在座標上繪製一個點, 顏色大小用 stroke() 與 strokeWeigth()
 line(x1, y1, x2, y2) 在座標 (x1, y1) 與 (x2, y2) 間繪製直線
 quad(x1,y1,x2,y2,x3,y3,x4,y4) 用 (x1,y1), (x2,y2), (x3, y3), (x4,y4) 作頂點繪製四邊形
 square(x, y, s [, r]) 在 (x, y) 座標繪製邊長 s 的正方形 (r=圓角半徑)
 triangle(x1,y1,x2,y2,x3,y3) 用 (x1,y1), (x2,y2), (x3, y3) 作頂點繪製三角形
 rect(x, y, w [, h]) 繪製一個左上角座標為 (x, y), 寬 w 高 h 的矩形 (CENTER 模式)
 rectMode(mode) 設定矩形繪圖函式 rect() 的參數模式, CENTER=(x, y, w, h)
 ellipse(x, y, w [, h]) 繪製圓心為 (x, y), 寬徑 w, 高徑 h 之橢圓 (w=h 時為正圓)
 ellipseMode(mode) 設定橢圓函式 ellipse() 的參數模式, 預設 CENTER=(x, y, w, h)
 arc(x, y, w, h, start, stop) 繪製圓心為 (x, y), 寬徑 w, 高徑 h, 起訖弧度 start~stop 之弧形
 textSize(size) 設定字型大小 (px)
 textAlign(halign [,valign]) 文字對齊 halign=LEFT/CENTER/RIGHT valign=TOP/BOTTOM
 text(str, x, y) 在座標 (x, y) 處開始輸出字串 str
 beginShape([kind]) 開始記錄形狀 kind (LINES/QUADS/TRIANGLES 等) 之頂點
 vertex(x, y [, z]) 用來在 beginShape() 與 endShape() 之間指定頂點座標 
 endShape([CLOSE]) 終止紀錄頂點 (傳入 CLOSE 會連接起點與終點形成封閉形狀)


事實上 p5.js 所提供的函式非常豐富, 但上面所列的常用常數, 變數, 與函式已足夠來作測試了, 完整的 p5.js 說明文件參考 :



5. 顏色設定 : 

在 p5.js 中用來設定顏色的函式主要有下面五個 : 
  • background(R [,G, B, alpha]) : 畫布的背景色
  • stroke(R [,G, B, alpha]) : 線條或圖形之邊框顏色
  • noStroke() : 取消前一次的 stroke() 設定
  • fill(R [,G, B, alpha]) : 圖形內之填滿顏色
  • noFill() : 取消前一次的 fill() 設定
這些函式的參數型態其實有多種模式, 通常 RGB 三原色模式較常用, 備選的第四參數 alpha 可設定顏色的透明度, 其值為 0 (不透明) ~ 255 (全透明). 

顏色色碼可用 Mozilla 的顏色選擇器來選定 : 


另外一個常用的顏色指定法為傳入 CSS 的顏色字串, 例如 :

background('red');
fill('ivory');
stroke('cyan'); 

完整的 CSS 顏色名稱字串參考 :



6. 用 point() 繪製點 : 

呼叫 point() 函式可繪製一個點, 預設大小是一個 1px 的點, 其顏色與大小可分別用 stroke() 與 strokeWeight() 設定, 例如 : 



<!doctype html>
<html>
<head>
  <meta charset="utf-8">
  <meta http-equiv="cache-control" content="no-cache">
  <meta name="viewport" content="width=device-width,initial-scale=1">
  <title>p5.js test</title>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.3.1/p5.min.js"></script>
</head>
<body>
  <script>
    function setup() {
      createCanvas(200, 200);      
      background(0, 0, 0);    //背景=黑色
      }
    function draw() {
      stroke(255, 255, 0);     //設定顏色=黃色
      strokeWeight(5);          //設定大小=5px
      point(100, 100);           //在 (100, 100) 繪製一個點
      }
  </script>
</body>
</html>

此例在 setup() 中建立一個 200*200 px 的畫布, 並設定背景色為黑色, 然後在 draw() 迴圈函式中先用 stroke() 設定前景色為黃色, 用 strokeWeight() 設定畫筆粗細為 5px, 最後呼叫 point() 在 (100, 100) 座標處繪製一個點, 結果如下 :



可見在畫布中央繪製了一個黃點. 


7. 用 line() 繪製線 : 

呼叫 line(x1, y1, x2, y2) 可以在畫布的兩個坐標點 (x1, y1) 與 (x2, y2) 之間繪製一條直線, 例如 :


<!doctype html>
<html>
<head>
  <meta charset="utf-8">
  <meta http-equiv="cache-control" content="no-cache">
  <meta name="viewport" content="width=device-width,initial-scale=1">
  <title>p5.js test</title>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.3.1/p5.min.js"></script>
</head>
<body>
  <script>
    function setup() {
      createCanvas(200, 200);      
      background(0, 0, 0);
      }
    function draw() {
      stroke(255, 255, 0);       //前景色=黃色
      strokeWeight(2);            //畫筆=2px
      line(0, 0, 200, 200);       //左上至右下畫一直線
      stroke(255, 255, 255);   //前景色=白色
      strokeWeight(6);            //畫筆=6px
      line(200, 0, 0, 200);       //右上至左下畫一直線
      }
  </script>
</body>
</html>

此例先設定前景色為黃色畫筆為 2px, 然後呼叫 line() 於 (0, 0) 與 (200, 200) 間繪製直線 (左上到右下); 然後更改前景色為白色, 畫筆改為 5px, 再次呼叫 line() 於 (200, 0) 與  (0, 200) 間繪製直線 (右上到左下), 結果如下 : 




8. 用 quad() 繪製四邊形 : 

呼叫 quad(x1, y1, x2, y2, x3, y3, x4, y4) 函式繪製四邊形必須傳入四個頂點的坐標 : (x1, y1), (x2, y2), (x3, y3), (x4, y4), 連接此四點即為四邊形, 例如 :



<!doctype html>
<html>
<head>
  <meta charset="utf-8">
  <meta http-equiv="cache-control" content="no-cache">
  <meta name="viewport" content="width=device-width,initial-scale=1">
  <title>p5.js test</title>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.3.1/p5.min.js"></script>
</head>
<body>
  <script>
    function setup() {
      createCanvas(200, 100);      
      background(200, 200, 200);
      }
    function draw() {  
      fill('red');           //設定填滿顏色為紅色
      quad(38, 31, 86, 20, 69, 63, 30, 76);    //繪製四邊形
      noFill();            //取消上一次填滿顏色設定
      fill('yellow');     //設定填滿顏色為黃色
      quad(128, 45, 170, 34, 180, 85, 158, 88);     //繪製四邊形
      noFill();            //取消上一次填滿顏色設定
      }
  </script>
</body>
</html>

此例繪製了兩個四邊形, 同時利用 CSS 顏色字串呼叫 fill() 來填滿四邊形內的顏色, 注意, 雖然可以直接設定新的填滿顏色, 但先用 noFill() 取消設定是個好習慣, 結果如下 :




9. 用 square() 繪製正方形 : 

呼叫 square(x, y, s [, r]) 繪製正方形需傳入左上角頂點坐標 (x, y) 與邊長 s, 若傳入備選的第四參數 r 則可將正方形圓角化, 此第四參數為圓角之半徑 (px), 例如 : 


測試 5 : 繪製正方形 [看原始碼]  

<!doctype html>
<html>
<head>
  <meta charset="utf-8">
  <meta http-equiv="cache-control" content="no-cache">
  <meta name="viewport" content="width=device-width,initial-scale=1">
  <title>p5.js test</title>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.3.1/p5.min.js"></script>
</head>
<body>
  <script>
    function setup() {
      createCanvas(200, 100);      
      background(200, 200, 200);
      }
    function draw() {  
      fill('cyan'); 
      square(25, 25, 50);           //繪製正方形
      noFill();
      fill('gold');
      square(125, 25, 50, 10);    //繪製有 10px 圓角之正方形
      noFill();
      }
  </script>
</body>
</html>

此例繪製了兩個正方形, 其中第二個使用第四參數指定了半徑為 10 px 之圓角, 結果如下 :




2021年4月9日 星期五

p5.js 學習筆記 (一) : 環境配置

今天在 blogger 中發現前年 (2018/12/6) 未寫完的 p5.js 函式庫的草稿, 花了點時間整理完後, 覺得這個開源專案很有趣, 有很多玩家是藝術界人士, 他們並非網頁或 IT 技術背景出身, 但卻能用 p5.js 開發出令人讚嘆的作品, 可見 p5.js 是一個非常容易上手的網頁多媒體互動工具, 所以今天打鐵趁熱, 繼續來測試.  

p5.js 好用好學的原因是開發者將許多 Javascript 與繪圖所需的物理數學細節都封裝在好用的函式中, 只要用文字編輯軟體開啟一個載入 p5.js 函式庫的網頁檔, 撰寫 Javascript 程式碼呼叫 p5.js 的函式即可在網頁上製作圖像, 動畫, 以及互動應用程式, 成果發布到網站就可以讓全世界的人用瀏覽器觀賞, 不須安裝任何製作軟體. 


1. 下載 p5.js 函式庫 :    

如果要在本機環境下測試 p5.js, 需到官網下載 p5.js 函式庫 :


在下載頁底下按中間的 "p5.min.js" 鈕下載壓縮後的函式庫 (約 795 KB), 若要研究或改寫原始碼也可下載左邊未壓縮的 p5.js (約 4.2 MB) :




將下載的 p5.min.js 放在工作目錄下, 然後開啟文字編輯器 (記事本, EditPlus 或 NotePad++), 建立如下測試網頁模板, 與 p5.min,js 同樣存放於工作目錄下 (用 utf-8 編碼存檔) :

<!doctype html>
<html>
<head>
  <meta charset="utf-8">
  <meta http-equiv="cache-control" content="no-cache">
  <meta name="viewport" content="width=device-width,initial-scale=1">
  <title>p5.js test</title>
  <script src="p5.min.js"></script>
</head>
<body>
  <script>
    function setup() {    //只執行一次
      createCanvas(400, 300);   //(x, y)
      } 
     
    function draw() {     //迴圈函式, 預設每秒執行 60 次
      background(200, 200, 200);    //(R, G, B)
      }   
  </script>
</body>
</html>

此處內嵌於網頁中的兩個 Javascript 函式就是 p5.js 的繪圖函式, 其中 setup() 只會執行一次, 而 draw() 則會被 p5.js 以預設每秒 60 次的頻率持續被呼叫, 也是 p5.js 最主要的繪圖程式, 這與 Arduino 的 setup() 與 loop() 功能是一樣的. 

此模板網頁的 setup() 函式中呼叫 p5.js 的 createCanvas() 函式在網頁中建立一個 400px*300px 大小的畫布, 然後進入 draw() 迴圈呼叫 background() 函式將畫布背景顏色設為 (R, G, B)=(200, 200, 200), 其中 R/G/B 為三原色色碼, 範圍為 0~255. 將此模板存檔為 p5.js_template_1.htm, 以瀏覽器開啟結果如下 : 




可見在網頁左上角開始的位置出現了一塊 400*300 的灰色畫布. 也可以將網頁與 Javascript 程式完全分離, 把 setup() 與 draw() 寫在另一個 Javascript 檔案 (副檔名 .js, 例如 sketch.js), 然後放在 p5.js 後面匯入網頁中 :

//sketch.js
function setup() {    //只執行一次
  createCanvas(400, 300);   //(x, y)
  } 
     
function draw() {     //迴圈函式, 預設每秒執行 60 次
  background(200, 200, 200);    //(R, G, B)
  }   

這時網頁要修改為 :

<!doctype html>
<html>
<head>
  <meta charset="utf-8">
  <meta http-equiv="cache-control" content="no-cache">
  <meta name="viewport" content="width=device-width,initial-scale=1">
  <title>p5.js test</title>
  <script src="p5.min.js"></script>
  <script src="sketch.js"></script>
</head>
<body>
</body>
</html>

結果與上面是一樣的. 


2. 使用 CDN 提供的函式庫 : 

如果網頁是要放在公開的伺服器上, 則可以直接使用 CDN 網站所提供的 p5.js 函式庫, 這樣就不需要下載函式庫自備, 按下載網頁右邊的 "CDN" 按鈕會顯示函式庫的 CDN 網址 :





通常使用 p5.min.js, 按右邊的鍊條複製其 URL :

https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.3.1/p5.min.js

然後修改上面的模板網頁, 用此 URL 替換 script 標籤中的 src 屬性值 :

<!doctype html>
<html>
<head>
  <meta charset="utf-8">
  <meta http-equiv="cache-control" content="no-cache">
  <meta name="viewport" content="width=device-width,initial-scale=1">
  <title>p5.js test</title>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.3.1/p5.min.js"></script>
</head>
<body>
  <script>
    function setup() {
      createCanvas(400, 300);
      }

    function draw() {
      background(200, 200, 200);
      }
  </script>
</body>
</html>

將此網頁以 utf-8 編碼存檔為 p5.js_template_cdn.htm, 然後用瀏覽器開啟就可以看到與上面相同的網頁. 也可以上傳到伺服器, 例如 GitHub 的個人網站上, 例如 :



3. 使用 p5.js 線上編輯器 : 

如果只是測試學習, 最方便的工具是 p5.js 官網提供線上編輯器, 其網址為 : 




線上編輯器使用網頁與程式分開的結構, 左上角編輯框用來編輯名為 sketch.js 的 Javascript 程式, 預設已經打上 setup() 與 draw() 兩個函式, 直接在此兩函式內輸入程式碼, 然後按 Play 鍵就能在右方的 preview 預覽區看到效果. 

按預覽區右上方的 Signup 鈕加入會員可自訂編輯器屬性 (例如布景, 字型大小等), 也可將程式儲存在雲端 :








也可以將自己的 sketch 程式分享 (按 share), 例如下面這個可用滑鼠操控頻率的範例 :


下面範例修改自官方教學文件 :


<!doctype html>
<html>
<head>
  <meta charset="utf-8">
  <meta http-equiv="cache-control" content="no-cache">
  <meta name="viewport" content="width=device-width,initial-scale=1">
  <title>p5.js test</title>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.3.1/p5.min.js"></script>
</head>
<body>
  <script>
    function setup() {
      createCanvas(200, 200, WEBGL);
      }
    function draw() {
      background(0);
      noFill();
      stroke(100, 100, 240);
      rotateX(frameCount * 0.01);
      rotateY(frameCount * 0.01);
      box(90, 90, 90);
      }
  </script>
</body>
</html>

結果如下 :



Processing 的網頁版函式庫 ps.js

2018 年底我在一本 Arduino 的書上看到用 Processing 語言顯示感測器資料的介紹, 在搜尋相關資料時找到 p5.js 函式庫, 這是 Processing 的 Javascript 網頁版本, 因為 Processing 本身是 Java 寫的, 所以使用 Processing 顯示Arduino 蒐集的感測器資料必須使用 Java 語法, 移植到 Javascript 後就可以在網頁上繪圖, 製作動畫或遊戲了, 不再需要 Java Applet 或 Flash 外掛, 這真的太棒了! 

p5.js 的前身是 John Rezig (jQuery 之父) 於 2008 年開發的 Processing.js 開放原始碼函式庫, 此函式庫用來在瀏覽器上顯示圖表與互動內容, 但此專案已在 2018 年底停止繼續開發了. 美國藝術家與電腦科學家 Lauren McCarthy 獲得 Processing 基金會的官方支援, 以 Processing.js 的成果為基礎於 2013 年開發了 p5.js 作為 Processing.js 的繼承者, 其命名源自 Processing 最早的網域名稱 proce55ing (因為 processing.org 當時已被註冊), 參考 :


p5.js 的官網與 GitHub 原始碼寄存參考 :


目前市面上關於 p5.js 的書籍不多, 只有如下數本 : 



Source : 博客來


此書由淺入深循序漸進, 透過 p5.js 來學習 Javascript 語法, 是很不錯的入門書.



Source : 博客來


此書是 p5.js 開發者 Lauren McCarthy 所著, 著重於利用演算法製作數學或藝術圖形, 例如碎形, 葉片等等, 需要一些代數, 超越函數等數學基礎. 



Source : 博客來


此書也是 p5.js 的藝術應用, 屬於實用級. 




Source : Amazon


此書似乎連 Amazon 都買不到. 

參考 : 


2021年4月6日 星期二

Python 學習筆記 : 基本語法 (五) : 錯誤與例外

本篇繼續來複習整理 Python 基本語法之例外處理, 本系列之前的文章參考 :


參考書籍 :
  1. 人工智慧 Python 基礎課 (碁峰, 陳會安)
  2. 精通 Python (碁峰, 賴屹民譯)
  3. Python 程式設計入門與運算思維 (新陸, 曹祥雲)
  4. 一步到位! Python 程式設計 (旗標, 陳惠貞)
  5. Python 程式設計入門指南 (碁峰, 蔡明志譯)
  6. Python 也可以這樣學 (博碩, 董付國)

十一. 例外處理 :   

程式開發過程中會遇到下列三種錯誤 : 


 程式的三種錯誤 偵錯難易 說明
 語法錯誤 容易 違反語法規則, 例如縮排, 關鍵字拼寫等.
 執行時期錯誤 (例外) 容易 程式符合語法, 但執行時出現錯誤, 例如除以 0 等.
 邏輯錯誤 困難 程式符合語法, 執行時也無例外出現, 但結果不正確.


有語法錯誤的程式無法順利執行, 直譯器會顯示錯誤原因並指出錯誤之程式碼位置, 因此這種錯誤很容易即能除錯. 

有些程式語法並無問題, 但有時可順利執行, 有時卻突然終止執行 (閃退) 並顯示錯誤訊息, 通常是因為不同的輸入條件或中間運算結果所致, 此種錯誤稱為執行時期錯誤, 又特稱為例外 (exception), 其除錯因為有錯誤訊息因此也不難, 但能否偵錯卻要靠測試條件是否能充分觸發可能之例外而定. 

邏輯錯誤又稱語意錯誤 (semantic error), 存在邏輯錯誤的程式雖均可順利執行, 但執行結果卻不正確 (錯誤), 或者令程式陷入無窮迴圈, 此種錯誤來自程式設計師的思考邏輯不周全或演算法錯誤, 邏輯錯誤很難偵錯與除錯. 
 

1. 語法錯誤 :    

語法錯誤包括 SyntaxError, KeyError, IdentationError, IndexError, NameError, 與 AttributeError 等, 出現語法錯誤時必須修改程式碼使其符合 Python 語法規則後才能順利執行, 常見的語法錯誤列舉如下 : 
  • 使用關鍵字當識別字 (變數, 函式, 類別之名稱)
  • 關鍵字拼寫錯誤
  • 程式區塊忘記冒號直接跳行
  • 同一個區塊內之縮排不一致
  • 字串的括號不成對或不匹配 (例如前面用單引號, 後面用雙引號)
  • 元組 (), 串列 [], 字典 {} 內的元素或項目沒有用逗號隔開
  • 虛數使用了數學的 i, 在 Python 應該用 j 表示虛數
  • 存取字串, 元組, 串列時索引超出範圍 (IndexError)
  • 存取字典時所用之鍵不存在 (KeyError)
  • 存取不存在的物件屬性或方法 (AttributeError)
  • 存取目前命名空間中不存在的變數 (NameError)
參考 :


例如 :  

>>> else=123                          # else 是關鍵字, 不可以用作識別字
  File "<pyshell>", line 1
    else=123
       ^
SyntaxError: invalid syntax   
>>> Else=123                          # Else 不是關鍵字, 是合法識別字
>>> Else
123
>>> for i In range(1, 10):        # 關鍵字拼寫錯誤 (In 應為 in)
    print(i)
    
  File "<pyshell>", line 1
    for i In range(1, 10):
           ^
SyntaxError: invalid syntax
>>> a=10
>>> if a < 0:       
    print('negative')
elseif a == 0:                           # 關鍵字拼寫錯誤 (elseif 應為 elif)
    print('zero')
else:
    print('positive')
    
  File "<pyshell>", line 3
    elseif a == 0:
           ^
SyntaxError: invalid syntax
>>> def foo()                           # 函式標頭應以冒號結束, 開啟內縮區塊 (缺了冒號)
   print('bar')
   
  File "<pyshell>", line 1
    def foo()
            ^
SyntaxError: invalid syntax
>>> def foo(a):
    print(a)
        return a+1                        # 區塊內縮不一致 (return 應與 print 齊頭)
    
  File "<pyshell>", line 3
    return a+1
    ^
IndentationError: unexpected indent
>>> print("Hello World)         # 字串的括號不成對
  File "<pyshell>", line 1
    print("Hello World)
                      ^
SyntaxError: EOL while scanning string literal
>>> print('Hello World")       # 字串的括號前後不一致
  File "<pyshell>", line 1
    print('Hello World")
                       ^
SyntaxError: EOL while scanning string literal
>>> a=[1, 2, 3  4]
  File "<pyshell>", line 1
    a=[1, 2, 3  4]
                ^
SyntaxError: invalid syntax
>>> a=1+2i                             # 在 Python 中以 j 表示虛數, 不是數學中慣用的 i 
  File "<pyshell>", line 1
    a=1+2i
         ^
SyntaxError: invalid syntax
>>> "tony"[4]                         # 索引超出範圍 (此例索引 0~3)
Traceback (most recent call last):
  File "<pyshell>", line 1, in <module>
IndexError: string index out of range
>>> {'foo':1, 'bar': 2}['tony']   # 字典的鍵不存在
Traceback (most recent call last):
  File "<pyshell>", line 1, in <module>
KeyError: 'tony'
>>> "tony".length                   # 字串物件無 length 屬性
Traceback (most recent call last):
  File "<pyshell>", line 1, in <module>
AttributeError: 'str' object has no attribute 'length'


2. 邏輯錯誤 : 

邏輯錯誤來自程式設計師本身的疏忽, 思考邏輯不正確, 或解決問題的演算法有誤, 此種錯誤很難偵錯, 因為程式都能順利執行不會有任何錯誤訊息, 只是結果不正確, 但有時也無法從結果看出正確與否, 只能一行一行檢視原始碼仔細推敲才可能發現錯誤之處. 

導致邏輯錯誤的可能原因如下 :
  • 使用了錯誤的變數導致運算式結果不正確
  • 誤用了整數除法使得小數部分被略去
  • 忽略了 0 起始或 1 起始差異導致差一錯誤 (off-by-one error), 例如迴圈終止值設定
  • 忽略了運算子的優先順序導致運算式得出錯誤結果
  • 搞錯了縮排的層次
例如計算 1+2+3+4+5 之和可用 for 迴圈搭配 range(from, to) 函式, 但要注意 range() 傳回的可迭代序列事實上只到 to-1 而非 to, range(1, 5) 傳回的序列為 1, 2, 3, 4, 所以得到的結果是錯的 :

>>> sum=0
>>> for i in range(1, 5):    # 這只加到 4 而已 : 1+2+3+4
    sum += i
    
>>> print(sum)                  # 1+2+3+4+5=15, 故計算結果錯誤
10


3. 執行時期錯誤 (例外) :    

程式語法正確, 但執行中因為無法順利處理資料而發生的異常錯誤稱為例外 (exception), 將導致程式崩潰而終止執行並輸出錯誤訊息, 但這些技術性資訊通常會使用者丈二金剛摸不著頭腦, 為了避免這種情況, 程式設計師必須捕捉這些可能的例外並做適當處理以增進程式的強固性. 

常見的執行時期錯誤例如 :
  • 除以零
  • 將字串與數值相加
  • 呼叫有必要參數之函式時未傳入引數
  • 欲開啟之檔案不存在
  • 取得網路文件時連線中斷
其中有些可能的錯誤只要例外處理最常用於程式需要與外部交換資料的時候, 例如讀寫外部檔案或從網路下載資料等, 這些操作都有環境不確定因素 (檔案不存在或網路連線異常), 其它可用因此必須程式設計師必須捕捉這些可能之例外並加以處理, 否則程式會因異常而突然終止, 使用者會對這些情況感到不知所措. 

除了開發時期程式員本身常遇到外, 軟體上線後也常因為以 input() 或 GUI 介面從使用者輸入取得之資料運算時造成例外, 故開發時需注意此類 Bug. 

語法錯誤與執行時期錯誤這兩種錯誤都有提示訊息可用做偵錯的參考, 而邏輯錯誤是最難被偵錯的, 因為問題出在對程式處理邏輯的想法不正確所致, 有時也出於程式員誤用運算子, 例如在條件式中使用了 = 運算子做比較而非 ==, 這不會出現語法錯誤, 因為在條件式中用 = 進行指派總是傳回 True. 


(1). 例外處理類別 :   

Python 提供了以 BaseException 為根類別的例外處理類別, 用來處理被拋出了各式例外, 常見的例外類別繼承關係如下圖所示 : 





BaseException 類別是 Python 的內建類別 (可匯入 builtins 後用 dir 檢視), 也是所有例外類別的最上層父類別. 程式設計師除了直接使用這些內建之例外類別來捕捉例外, 也可以自訂例外類別, 但自訂例外類別時並非直接繼承 BaseException, 而是應該繼承其子類別 Exception, 參考 :


Python 的例外處理語法為如下之 try except 結構 (try-and-error 模式) : 

try: 
    可能觸發例外之程序
except 例外類別名稱1 [as 別名1]: 
    例外處理程序1
except 例外類別名稱2 [as 別名2]: 
    例外處理程序2
.....
except:
    所有其他例外處理程序
[else:
    沒有觸發任何例外時之處理程序]
[finally:
    不論有無觸發例外必定執行之程序]

說明如下 : 
  • try 與至少一個 except 區塊是必須的, else 與 finally 則是可有可無的. 
  • 將可能發生例外的程式碼放在 try 區塊中, 一旦發生執行時期錯誤就會停止 try 區塊內程序的執行並拋出例外, 此例外會由底下的 except 區塊依序捕捉, 首先由排在前面的指名例外類別捕捉, 一旦捕捉到一個例外類別後, 其它的例外類別就不會再嘗試捕捉. 如果前面的指名例外類別都沒有捕捉到此例外, 就由最後面的 except 區塊概括承受全部捕捉. 
  • 若沒有發生例外, 則 try 區塊程序執行完畢後會執行 else 區塊, 不管有無發生例外, finally 區塊都會被執行, finally 通常用來處理資源清理工作, 釋放 try 區塊中所占用之記憶體資源, 例如關閉檔案等.
  • 如果 try 區塊發生之例外沒有被任何一個 except 區塊捕捉到, 或者連 else 區塊也發生例外, 則這些例外會等 finally 區塊執行完畢後才被拋出, 亦即發生例外並不會影響 finally 區塊的程序, 它一定會被執行. 因此若在函式中使用了異常處理結構, 切勿在 finally 區塊中使用 return 傳回結果, 因為這會讓該函式不管執行結果如何都傳回相同結果, 變成一個難以發現的邏輯錯誤. 
  • 例外類別名稱可以用 as 關鍵字取別名 (例如 e) 以節省打字長度, 在例外處理程序中可直接呼叫 print(e) 輸出例外物件內容, 也可以用 print(e.args) 檢視, args 屬性值為一個包含例外原因的 tuple.   
 此語法結構的演算法可用下圖表示 :




如果某些例外要共用相同的處理程序, 則可將這些例外放在 tuple 中, 結構如下 :

try: 
    可能觸發例外之程序
except (例外類別名稱a1,  例外類別名稱a2, ... ) [as 別名a]: 
    共用的例外處理程序a
except (例外類別名稱b1,  例外類別名稱b2, ... ) [as 別名b]: 
    共用的例外處理程序b
except:
    其它例外處理程序
else:
    沒有觸發任何例外時之處理程序
finally:
    不論有無觸發例外必定執行之程序

參考 : 


例如 : 

>>> try:
    a,b=input("請輸入被除數與除數(以空格隔開之整數)").split()
    a=int(a)
    b=int(b)
    print("被除數/除數=", a/b)
except ZeroDivisionError as e:
    print("發生除以零例外:", e)
    print(e.args)
else:
    print("沒有發生例外")

請輸入被除數與除數(以空格隔開)10 2
被除數/除數= 5.0
沒有發生例外

請輸入被除數與除數(以空格隔開之整數)10 0        # 除以零
發生除以零例外: division by zero
('division by zero',)

此例透過 input() 讓使用者輸入以空格隔開的兩個整數 a, b, 然後計算 a 除以 b, 當除數 b 不為零時不會發生例外, 當 b 為 0 時則拋出 ZeroDivisionError 的例外被 except 捕捉. 注意, 發生例外時的那行程式碼與以下的程式碼都會被終止, 故不會輸出 "被除數/除數=" 字串. 

但上面的程式只捕捉了 ZeroDivisionError 這個例外, 如果輸入資料中有一個為非數值將使程式於 try 區塊拋出另外一個例外 ValueError, 但因為程式沒有捕捉此例外因此會導致異常終止 : 

請輸入被除數與除數(以空格隔開)123 abc           # 輸入非數值觸發了未被捕捉之例外
Traceback (most recent call last):
  File "<pyshell>", line 4, in <module>
ValueError: invalid literal for int() with base 10: 'abc'

程式應捕捉所有可能出現的例外, 因此添加捕捉 ValueError 例外如下 : 

>>> try:
    a,b=input("請輸入被除數與除數(以空格隔開)").split()
    a=int(a)
    b=int(b)
    print("被除數/除數=", a/b)
except ZeroDivisionError as e:
    print("發生除以零例外:", e)
except ValueError as e:     
    print("發生值的例外:", e)
else:
    print("沒有發生例外")

請輸入被除數與除數(以空格隔開)123 abc       # 拋出的 ValueError 被捕捉
發生值的例外: invalid literal for int() with base 10: 'abc'

也可以將這兩個例外以 tuple 組合在一起合併處理, 例如 :

>>> try:
    a,b=input("請輸入被除數與除數(以空格隔開)").split()
    a=int(a)
    b=int(b)
    print("被除數/除數=", a/b)
except (ZeroDivisionError, ValueError):              # 合併處理兩個例外
    print("發生例外")
else:
    print("沒有發生例外")

請輸入被除數與除數(以空格隔開)10 0             # 發生除以零例外
  發生例外

請輸入被除數與除數(以空格隔開)123 abc        # 發生值的例外
  發生例外                                                            

若這樣攏統, 也可以用內建函式 isinstance() 來分辨例外類別, 例如 : 

>>> try:
    a,b=input("請輸入被除數與除數(以空格隔開)").split()
    a=int(a)
    b=int(b)
    print("被除數/除數=", a/b)
except (ZeroDivisionError, ValueError) as e:      # 合併處理兩個例外
    if isinstance(e, ZeroDivisionError):                # 用 isinstance() 分辨例外類別
        print("第二個整部不可為 0")
    else:
        print("請輸入兩個以空格隔開的整數")
else:
    print("沒有發生例外")

請輸入被除數與除數(以空格隔開)10 0   
第二個整部不可為 0

請輸入被除數與除數(以空格隔開)123 abc   
請輸入兩個以空格隔開的整數

2021年4月5日 星期一

遊岡山之眼

因為沒去過岡山之眼, 恰好菁菁說六月底之前免門票 (全票 60 元), 趁著連假最後一天就去看看唄, 順便訂了五點的岡山逐鹿炭烤.  下午兩點出發走國一至燕巢下交流道, 至阿公店水庫管理局轉搭高雄客運的接駁車 (整點與 30 分一班, 但滿 19 人即開車), 管理局免費停車, 停好就到旁邊候車. 這停車場很有特色, 上面是太陽能板, 既可發電又能給車子遮陰. 

登上瞭望台之前有個賣飲料冰品食物的小市集, 霜淇淋三支 100 元還蠻濃郁的. 走過一連串之字形步道後就到達岡山之眼的瞭望台, 站在上面可以俯瞰大半個阿公店水庫, 但上面風大, 戴帽子要小心別被吹落. 菁菁主要是去拍照的, 我則主要是看風景. 如果沒有在上面駐足太久的話, 其實 30 分鐘內就走完了. 

離開岡山之眼回到水庫開車前往岡山的逐鹿炭烤, 據說高雄市區當天訂幾乎都訂不到, 岡山還蠻好訂的. 但炭烤大多是肉, 雞肉, 豬肉, 蝦, 魚肉, 牛肉 .... 菁菁一直烤給我吃, 不到三十分鐘我已覺得有點飽了. 雖然我比較喜歡火鍋勝過炭烤, 但這家整體來說是很不錯的餐廳, 服務態度尤其親切. 

好文 : 終於要上雲端了嗎?

今天突然想到 Google App Engine (GAE, 現已納入 GCP 中) 是否已支援 Python 3? 馬上查谷歌大神, 果然有了, 同時找到一篇很不錯的文章, 介紹如何將網頁專案 (Django/Flask) 佈署在 GCP : 


這篇文章特地提醒使用者在註冊 GCP 使用其雲端服務時首先要設定自己的預算上限, 否則一旦你的網路服務出奇地大受歡迎, 暴增的流量會讓你看到帳單說不出話來. 

這篇文章是作者一系列 "不做怎麼知道" 系列中的一篇, 完整目錄參考 :

 
作者透過在 iT 邦幫忙一天寫一篇自己在不熟悉領域的學習心得來逼自己向前進, 嗯, 這構想很有意思, 先記下來, 有空再回頭好好來施展吸星大法. 

我已多年未玩 GAE, 自從被納入 GCP 後, 更覺得麻煩, 不像以前 GAE 免費用時代方便. 但是檢視自己之前在 GAE 上佈署的測試網頁發現它們居然還活著耶! 谷歌就這個好處, 不像那些免費虛擬主機, 流量太高說你 abuse, 流量太低又給你砍帳號. 

2021年4月4日 星期日

2021 年第 14 周記事

本周與下周因為清明節連假補假關係都只上四天班, 但周五第一天假在花蓮清水隧道居然發生火車撞卡車導致嚴重死傷事故, 這幾天看了新聞報導真的很心酸, 有帶孩子快樂出遊的, 有趕回家鄉掃墓的, 有放假回家看家人的, 都被可惡的漫不經心給摧毀, 受害者家屬的人生從彩色變黑白, 這種傷口也許會隨時間而癒合, 但人生的遺憾卻會帶上一輩子. 害這麼多人這樣, 這罪過可不輕啊!

我週五晚上從鄉下回高雄時, 在離家不遠處的第一個路口停下等紅燈, 從後照鏡注意到後面那台車看到我停下便打左方向燈越過雙黃線超車直接闖紅燈揚長而去, 一點都沒有羞恥心, 或許還笑我是傻瓜, 這種鄉下停甚麼紅燈, 沒車就過啦! 或許惡政先生說得也有點道理, 這個國家上上下下都有問題, 但我認為無羞恥心應該擺第一. 

本周暫時停下網頁爬蟲的學習, 回頭複習整理 Python 基本語法, 以前都是邊看邊用, 沒有扎扎實時去學基礎的東西, 例如例外, 檔案讀寫, 正規式等等, 希望這次能利用整理筆記的機會重頭再複習一遍. 清明連假原本應該會留在鄉下好好看書, 但菁菁說想去七股鹽田與岡山之眼看看, 所以我周五回去, 週六回來, 今天行程式中午出發去七股. 

七股鹽山以前二哥與菁菁幼稚園與小學戶外教學曾去過, 十幾年之後重遊居然忘了當時的時空場景, 努力從四周建物回想, 終於拼湊出那時幫二哥排隊買鹹蛋黃冰, 陪菁菁爬上鹽山, 以及在廟口吃便當的情景, 歲月不饒人, 新手爸爸已然變成中年大叔. 看到菁菁偷拍我在瓦盤鹽田涼亭打盹的模樣 (說像財神爺), 不禁幻想撿到一個奇怪的相機, 在好奇按下快門之後突然就回到 35 歲時的模樣 ....  (韓劇是不是看太多了).

2021年4月2日 星期五

市圖還書 1 本 (Python 機器學習)

本周市圖還下列書籍 : 
這本有人預約, 目前沒時間看先還, 此書是第二版 (母校也有此書), 我有買封面很像的 "Python 深度學習" (也是博碩出版, 劉立民翻譯), 但這本我沒買, 用借的就好, 此書主要講 Scikit-learn 跟 Tensorflow ( v1).

2021年4月1日 星期四

好聽的 "海底"

這兩天菁菁又有了喜愛的新歌 "海底", 乍聽有古典風味道, 細聽又覺得像韓劇 Voice 片尾曲, 旋律好美, 歌手獨特的嗓音聽來很有療癒感 : 





可是當我仔細看了歌詞, 卻又覺得詞意充滿著淒涼, 悲觀, 與灰色 : 

作詞作曲 : PSROSIE 

散落的月光穿過了雲
躲著人羣
鋪成大海的鱗
海浪打溼白裙
試圖推你回去
海浪清洗血跡 妄想溫暖你
往海的深處聽 誰的哀鳴在指引
靈魂沒入寂靜 無人將你吵醒
你喜歡海風鹹鹹的氣息
踩著溼溼的沙礫
你說人們的骨灰應該撒進海裡
你問我死後會去哪裡
有沒有人愛你
世界能否不再 總愛對涼薄的人扯著笑臉
岸上人們臉上都掛著無關
人間毫無留戀 一切散為煙
散落的月光穿過了雲
躲著人群溜進海底
海浪清洗血跡 妄想溫暖你
靈魂沒入寂靜 無人將你吵醒
你喜歡海風鹹鹹的氣息
踩著溼溼的沙礫
你說人們的骨灰應該撒進海裡
你問我死後會去哪裡
世界已然將你拋棄
總愛對涼薄的人扯著笑臉
岸上人們臉上都掛著無關
人間毫無留戀 一切散為煙
來不及 來不及
你曾笑著哭泣
來不及 來不及
你顫抖的手臂
來不及 來不及
無人將你打撈起
來不及 來不及
你明明討厭窒息

參考 : 

对一支榴莲的《海底》歌词的理解

歌手 "一支榴槤" 是誰?

高科大還書 2 本 (Python 網路爬蟲)

因為有人預約, 需還下面兩本書 : 
  1. Python網路文字探勘入門到上手
  2. Python網路爬蟲 : 大數據擷取、清洗、儲存與分析 : 王者歸來
這兩本書都不錯, 但我覺得 No.1 這本陳寬裕寫的更佳, 篇幅不大, 但行文流暢且編排較順, No,2 則蒐羅較廣, 都值得一看. 

2021年3月31日 星期三

購買 Logitech 藍芽滑鼠 M337

兩年前換 Swift 5 筆電時同時買的羅技 M337 藍芽滑鼠被我摔了好幾次後左鍵變得不太靈敏, 後來就買 kYNYO GBM-1800 代替, 但價差反應品質, 還是 M337 好用, 所以趁有免運券上露天買了新的 M337 :





2021-04-01 補充 :

昨天下標後到今天才回應說已無貨, 直接幫我取消訂單. 

2021年3月30日 星期二

比莉的 Dear John (你的愛會將我灌醉)

最近菁菁在車上放了這首歌, 我覺得這歌手聲線好有魅力, 有時輕柔, 更多時候是撕裂的爆發, 還以為是哪位中國新銳歌手唱的, 查了一下原來是比莉在 1999 年唱的 "Dear John", 年代太久遠我早就忘了, 就只是有點印象而已, 沒想到 20 年後在中國抖音爆紅 :





學生時代對比莉的印象就是在舞台上擅長勁歌熱舞, 跟高凌風類似風格的藝人, 說實在不是我欣賞的那型 (我喜歡的是李碧華). 但多年後再來聽這首歌, 領略已有所不同 : 

"每次戀愛像是賭注, 付出全部拿回甚麼不清楚"
"愛過至少學會感動與知足, 難說當愛落幕贏或輸"
"太過清醒怎麼陶醉"

哈哈, 我那時太嫩了, 無法理解. 

2021年3月29日 星期一

試做紅燒雞肉

前幾天在塔塔的頻道看了她示範的紅燒雞腿料理後就躍躍欲試 : 





去過一趟附近的家樂福超市沒看到去骨的雞腿肉, 但昨天掃墓剛好有整隻雞可用, 於是早上拜完家祠回來就趕緊到鎮上的全聯買了所需要的食材與醬料, 主要是缺了辣椒, 蒜苗, 豆瓣醬, 花椒粒這四樣. 回到家又再看一次影片複習程序, 整理如下 :

1. 薑切片, 蒜打碎, 蒜苗切段取莖部備用.
2. 將雞腿肉洗淨切塊放入適量鹽, 糖, 與白胡椒醃製一下.
3. 起油鍋放入花椒粒, 煎至變黑後撈起花椒粒. 
4. 放入薑片, 蒜, 蒜苗, 炒至薑片微焦後加入辣豆瓣醬與米酒續炒一下.
5. 加入雞腿肉煎至兩面微焦, 加白胡椒粉, 糖, 蠔油, 醬油調味, 再加些米酒翻炒約十分鐘. 
6. 最後加入辣椒, 青椒翻炒至青椒半熟收汁即可. 







想說順便幫爸做便當, 所以就整雞 all-in, 但醬汁實在太美味, 又留來整理菜園的小舅吃午飯, 整鍋雞肉一餐幾乎全部吃光光, 只留兩三塊給掃墓回來就補眠到一點的菁菁, 哈!  好有成就感. 其實嚴格來說這道菜應該算是紅燒回鍋肉, 因為掃墓用的雞肉已煮過一次, 所以肉質沒有用新鮮雞肉做的這麼嫩, 下次去全聯買新鮮雞腿肉來重做看看. 

2021年3月28日 星期日

2021 年第 13 周記事 : 掃墓

本周重點活動是今天的掃墓, 因為最近太忙, 往年掃墓前一周的整理墓園是我的工作, 那才是真正在掃墓, 不僅掃, 還要砍樹除草, 因為經過一年風吹雨打, 雜草藤蔓恣意盤據整個墓園, 不花個兩小時是無法完工的. 今天出發前往墓園前有再翻出 2015 掃墓後編寫的備忘錄, 所以沒有發生忘東忘西的問題, 同時也修改了部分內容以符合現況, 並用三顆心列為最重要備忘錄, 參考 : 


下午掃 20 世來台祖墓園時, 東森的記者架了攝影機在拍我們掃墓過程, 因為堂弟阿明務農務出名堂來, 所栽培的稻米多次獲得稻米競賽, 種橙蜜也是, 記者聽說來台祖墓園在他田上, 有獲得祖上保佑云云, 所以或許這幾天我會出現在東森新聞也說不定, 哈哈哈. 

本周終於複習完 Python 的物件導向, 寫完筆記總有暢快淋漓之感, 也證明之前走馬看花用眼睛寫程式其實並不紮實, 沒有真正寫點程式碼測試都不算學過了. 接下來終於可以專心弄爬蟲了. 

姊姊為了掃墓周六下午回到高雄, 但仍心繫畢展, 昨天回鄉下前還載她去小北買了一包水泥砂, 周六晚上還趕工做小板模灌漿製作老師的教具, 嘿, 這我用得著, 等我要裝設大門口的風力發電機時就需要做方形混泥土磚了. 

這陣子愛奇藝似乎沒有值得看的韓劇, 沒追劇所以有更多時間讀書學習, 只有每周回鄉下看一集 "暗行御史", 此劇劇情張力普普 (因為是懸疑喜劇), 比較沒追劇的動力, 當成休閒打發時間倒是不錯. 我比較喜歡探討時空的推理劇, 像信號這部我就會熬夜繼續看下一集, 可惜聽說信號 2 要無限期延期了. 

2021年3月26日 星期五

Python 學習筆記 : 基本語法 (四) : 類別

本篇繼續複習整理 Python 的類別. 類別是物件的模子 (model), 藍圖 (blueprint), 或樣版 (template), 物件則是依據類別定義於記憶體中所建立的實例 (instance). Python 支援物件導向 (object-oriented programmin) 設計, Python 除了所有原生資料型態 (數值, 字串, 元組, 串列, 字典, 集合) 都是物件, 開發者也可以自訂類別, 也就是自己的資料型態. 

物件導向設計這種設計範式 (paradigm) 源自 Simula 語言, 雖然以傳統函數式或程序導向 (procedure-oriented) 設計也能完成軟體專案, 但大型軟體專案傾向採用物件導向方式開發, 因為物件導向具有模組化 (modularization) 與可重用性 (reusable) 等優點, 有助於團隊協力開發, 並能降低後續的維護成本. 

本系列之前的筆記參考 :


更多 Python 筆記參考 :


參考書籍 :
  1. 人工智慧 Python 基礎課 (碁峰, 陳會安)
  2. 精通 Python (碁峰, 賴屹民譯)
  3. Python 程式設計入門與運算思維 (新陸, 曹祥雲)
  4. 一步到位! Python 程式設計 (旗標, 陳惠貞)
  5. Python 程式設計入門指南 (碁峰, 蔡明志譯)
  6. Python 也可以這樣學 (博碩, 董付國)
線上教學文件參考 : 



十. 類別與物件 : 

類別是物件導向設計的基礎, 類別是物件的抽象定義, 物件是類別的具體實例, 說明如下 : 
  • 類別 (class) :
    類別是物件的抽象結構定義, 是包含資料與處理資料之程式碼的結構, 也是用來建立物件的模版, 程式設計者可用類別來自訂資料型態. 類別由屬性 (attribute/property) 與方法 (method) 兩種成員 (member) 組成, 屬性就是物件中保存的資料, 又稱為資料欄位 (data field), 它們描述了物件的靜態特性 (狀態); 而方法則是物件內函式的特稱, 方法描述了物件的動態特性 (行為), 也是物件之間互動的窗口. 
  • 物件 (object) : 
    類別的實例 (instance), 根據類別的定義在記憶體中所建立的實體, 事實上就是將資料 (變數) 與操作 (函式/方法) 合在一起的組合體. 建立物件具體而言就是透過呼叫類別的建構子 __init__() 來初始化物件, 亦即將變數與方法參考儲存在特定的一塊記憶體中. 
物件導向主要有下面三大特性 : 
  • 封裝 (encapsulation) : 
    封裝是將資料 (屬性) 與操作 (方法) 組合在一起, 程式只能透過方法存取屬性, 從而達到保護資料的作用. 屬性代表物件的狀態, 而方法則代表物件之行為. 封裝主要的目的是為了隱藏資料與保護程式實作細節, 對使用者來說物件如同一個黑盒子, 只能透過公開的方法呼叫才能存取物件內的資料. 在定義類別時於屬性或方法名稱前加上雙底線 (dunder) 即可將其設為私有 (private, 僅限本身存取), 否則預設為公開 (public). 
  • 繼承 (inheritance) :
    定義一個類別時可以指定繼承另一個已定義過的類別, 被繼承的類別稱為父類別 (parent class), 繼承者稱為子類別 (child), 在子類別中呼叫 super() 即可參考到父類別. 只要在建構子方法 __init__() 中用 super().__init__() 呼叫父類別的建構子方法, 子類別就能完全複製父類別的成員 (屬性+方法), 視需要也可再增添子類別專屬的屬性與方法, 也可以重新定義與父類別同名之方法來覆寫 (override) 或擴充繼承自父類別的方法, 達到元件可重用 (reusable) 目標, 避免重新打造輪子. 注意, Python 支援多重繼承, 只要將父類別以逗號隔開傳入子類別當參數即可依序逐一繼承. 
  • 多型 (polymorphism) :
    多型為不同之類別但具有同名之方法, 可以先建立一個具有這些同名方法之基礎類別讓這些不同類別繼承然後各自覆寫這些同名方法, 這種方法名稱相同但實作的程式碼功能不同的作法稱為多型, 也稱為同名異式. 

1. 定義類別 :    

Python 使用關鍵字 class 來定義類別, 語法如下 : 

class 類別名稱(父類別1, 父類別2, ...):
    '''
    類別的說明
    '''
    類別變數1=值1        # 定義類別變數 (又稱為靜態變數)
    類別變數2=值2        # 定義類別變數
    ....
    def __init__(self, 參數1, 參數2, ...):   # 覆寫根類別 object 之建構子方法
        #初始化設定
        self.屬性1=參數1                             # 定義物件變數 (屬性)
        self.屬性2=參數2                             # 定義物件變數 (屬性)
        .....
    def 方法1(self, 參數1, 參數2, ...):       # 宣告物件方法 (self 為物件本身, 自動傳入)
        #操作1程式碼
    @staticmethod                                     # 宣告靜態方法
    def 方法2(參數1, 參數2, ...):
        #操作2程式碼 
    @classmethod                                      # 宣告類別方法
    def 方法3(cls, 參數1, 參數2, ...):         # (cls 為類別本身, 自動傳入)
        #操作3程式碼
    .....

用法說明如下 :
  • 類別的標頭以關鍵字 class 開頭, 後面是類別名稱以及小括弧內的父類別列, 標頭以冒號結束後即跳行縮排開始定義類別內容.
  • 類別名稱與變數識別字的命名規則相同, 即只能用英數字與底線, 但首字元不能是數字. 慣例上類別名稱首字母用大寫, 多詞名稱可用駝峰字或底線分隔, 例如 MyClass 或 My_class 等.  
  • 定義在所有方法外的變數稱為類別變數 (class variable), 又稱為靜態變數 (static variable), 它會被此類別的所有物件共用, 存取類別變數時前面須冠類別名稱, 例如 MyClass.name. 
  • 類別名稱後面的小括弧內為所繼承的父類別名稱, Python 支援多重繼承, 多個父類別以逗號隔開, 子類別將由左至右依序繼承父類別的所有成員, 遇到同名衝突時以左方的類別之成員優先. 若未指定父類別則預設會繼承 object 類別, 這時可省略小括號. 
  • 類別的方法有四種 :
    (1). 初始化方法 : 即名稱為 __init__() 的方法
    (2). 物件方法 (一般方法) : 存取對象為物件變數, 故第一參數必須傳入物件
    (3). 靜態方法 : 存取對象為靜態變數, 故不須傳入物件
    (4). 類別變數 : 
  • __init__() 是類別的特殊方法, 稱為類別的建構子 (constructor), 每次用類別名稱建立新物件時都會呼叫此方法, 利用傳入參數對物件進行初始化 (設定屬性值). 其第一參數 self 代表要建立之物件, 後面跟著以逗號隔開的參數列, 初始化時要參考 self 來進行屬性值之設定, 例如傳入參數 name 之屬性設定為 self.name=name. 這些屬性又稱為實體變數 (instance variable), 因為它們屬於物件實體. 注意, self 不是 Python 的關鍵字, 使用 self 只是慣例, 可以改用任何合法之識別字. 注意, init 前後為雙底線 (double underscore, 又稱 dunder), 前後為雙底線的識別字為 Python 內部使用. 
  • 類別內可定義三種方法 :
    (1). 物件方法 :
    此種方法無任何修飾器, 且第一參數為傳入物件本身 (通常以 self 代表物件), 用來讓方法可存取物件之屬性 (即實體變數), 後面跟著以逗號隔開的參數列. 由於封裝與資料隱藏的需要, 類別基本上會有設定 (setter) 與取得 (getter) 這兩類方法作為外部程式存取物件的窗口, 慣例上前者會用 set 開頭; 後者會用 get 開頭, 為了能存取物件的成員, 所有的物件方法第一參數都應該傳入物件參考 self, 除非該方法不需要存取物件的屬性或呼叫其它方法. 因為 self 的作用範圍 (scope) 遍及類別內的任何地方, 傳入 self 可讓方法之作用範圍也能遍及類別內任何地方. 不過外部程式在呼叫方法時卻不需要理會 slef, 只要傳入 self 後面的其他參數即可 (self 會隱性地自動傳入). 若要在物件方法中存取類別變數須用 self.__class__.類別變數. 
    (2). 類別方法 : 
    此方法前面須加上 @classmethod 修飾器, 其第一參數必須傳入類別本身 (通常用 cls 表示), 只能用來存取類別變數, 無法存取物件變數 (因為不能傳入物件). 外部程式碼可用類別名稱或物件名稱呼叫類別方法, 不需建立物件也能用類別名稱直接呼叫. 
    (3). 靜態方法 :
    此方法前面須加上 @staticmethod 修飾器, 與類別方法的差別是不會傳入類別本身, 在類別內只能用類別名稱來存取類別變數, 無法存取物件變數 (因為不能傳入物件). 外部程式碼可用類別名稱或物件名稱呼叫類別方法, 不需建立物件也能用類別名稱直接呼叫.
例如下面是一個最簡單的類別範例 : 

class MyClass():
    pass

此處類別的內容用關鍵字 pass 佔位, 表示這是一個尚未實作的程式碼區塊, 目前跳過不做任何事情, 其用途只是為了滿足語法結構的完整性而已. 

這個 MyClass 類別是一個沒有繼承特定父類別的空類別, 類別名稱後面的小括弧也可以省略 :

class MyClass:
    pass

看似沒有父類別, 其實是隱性地自動繼承了 Python 的根類別 object, 也可以明確寫出來, 這在 Python 2 是必要的 :

class MyClass(object):
    pass

根類別 object 是 Python 所有物件的父類別 (基礎類別, base/parent class), 可用類別的 __bases__ 屬性檢視, 它會傳回其所有父類別組成之 tuple :

>>> class MyClass():
    pass

>>> MyClass.__bases__    
(<class 'object'>,)

參考 :


根類別 object 內以特殊名稱定義的屬性與方法, 可用 dir() 檢視其內容 :

>>> dir(object)
['__class__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__']

這些特殊成員都會被每一個物件繼承. 

為程式碼加上註解是良好的開發習慣, 關於類別說明的註解文字是在類別標頭底下, 類別內容開始之前用單引號或雙引號括起來, 這樣就可以用 "類別名稱.__doc__" 屬性取得這段說明, 例如 :

>>> class MyClass():
    '''這是 MyClass 類別的說明'''       # 單行的說明文字
    pass

>>> MyClass.__doc__
'這是 MyClass 類別的說明'

說明文字也會包含跳行字元, 例如 : 

>>> class MyClass():
    '''
    這是 MyClass 類別的說明           # 多行的說明文字
    '''
    pass

>>> MyClass.__doc__
'\n    這是 MyClass 類別的說明\n    '


2. 建立物件 : 

定義好類別後即可呼叫與類別同名的函數建立物件, 以上面的空類別為例 :

myobj=MyClass()

此指令會執行下列動作 :
  • 檢查類別定義, 在記憶體中建立新物件資料結構, 傳回物件參考 (self)
  • 呼叫物件的 __init__() 方法並傳入物件參考與參數執行初始化設定
  • 傳回新物件的參考給呼叫者
與物件相關的 Python 常用內建函式如下 :
  • dir(obj) : 用來檢視物件 obj 的類別
  • dir(obj) : 用來檢視物件的內容
  • isinstance(obj, class) : 用來檢查 obj 是否為 class 類別之實例
例如 :

>>> class MyClass():        # 定義一個空類別
    pass
>>> myobj=MyClass()      # 建立一個 MyClass 類別的物件 (實體)
>>> myobj                         # 顯示物件參考位址 print(myobj)
<__main__.MyClass object at 0x000001E5C5C9B048>
>>> myobj.__str__()         # 與 print(nyobj) 一樣
'<__main__.MyClass object at 0x000001E5C5C9B048>'
>>> myobj.__class__
<class '__main__.MyClass'>
>>> type(myobj)
<class '__main__.MyClass'>       # 檢視物件類型為 MyClass
>>> dir(myobj)                            # 檢視物件內容 
['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__']
>>> isinstance(myobj, MyClass)    # 檢查 myobj 是否為 MyClass 類別之實例
True
>>> isinstance(myobj, str)     # 檢查 myobj 是否為 str 類別之實例
False

此例的 MyClass 是一個空類別, 故所建立的物件也是空的, dir() 所顯示的雙底線開頭結尾物件屬性與方法, 這些都是每個物件內部的都有的特殊成員, 其中比較常用的是 : 
  • __new__() : 建立物件前會被呼叫, 完畢後再呼叫 __init__() 進行初始化
  • __init__() : 物件的建構子, 用來初始化物件
  • __str__() : 將物件資訊轉成字串輸出, 等於呼叫 print(obj)
  • __class__ : 此物件屬性會參考到此物件之類別本身
  • __doc__ : 此物件屬性會參考到類別之說明文字
其中 __init__() 被稱為建構子 (constructor), 當執行建立物件指令時, 解譯器首先會先呼叫 __new__() 方法 (可用來處理物件初始化前之前置作業), 然後呼叫 __init__() 進行初始化, 此兩方法只有在建立物件時被先後自動呼叫一次, 但使用者通常只需要覆寫 __init__() 方法來為物件進行初始化設定, 主要是利用傳入參數來設定物件的屬性值或呼叫其他方法, 例如上面的 MyClass 空類別可以改寫為 :

class MyClass():
    def __init__(self, a, b):
        self.a=a
        self.b=b
    def showInfo(self):
        print("a=", self.a, "b=", self.b)      # 透過 self 物件存取其屬性

這個 MyClass 是具有 a, b 兩個公開 (public) 屬性以及一個公開方法 showInfo() 的新類別. 其中 self 是剛建立的新物件的參考, 類別內的每個方法 (包括 __init__() 也是) 都應該以 self 為第一參數, 因為 self 的作用範圍遍及類別內的任何地方, 有傳入 self 的方法才能存取到物件的屬性. 方法內也可以定義區域變數, 但其作用範圍只限於該方法. 例如 : 

>>> myobj=MyClass("Hello", "World")     # 傳入 a, b 參數建立 MyClass 物件
>>> myobj
<__main__.MyClass object at 0x000001E5C5C9B160>     # 新的物件 (參考位址不同)
>>> type(myobj)
<class '__main__.MyClass'>   
>>> dir(myobj)                          # 檢視物件內容
['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'a', 'b', 'showInfo']

與上面的空物件比較, 多了 a, b, showInfo 三個成員, 由於 Python 類別之成員預設都是公開的 (public, 沒有隱藏), 亦即類別外部程式碼可以存取到內部屬性與方法, 例如 : 

>>> myobj.a       # 取得屬性 a 之值
'Hello'
>>> myobj.b       # 取得屬性 a 之值
'World'
>>> myobj.showInfo()     # 呼叫物件方法
a= Hello b= World
>>> myobj.a=123       # 直接更改屬性值
>>> myobj.a
123
>>> myobj.b=456       # 直接更改屬性值
>>> myobj.b
456
>>> myobj.showInfo()     # 呼叫物件方法
a= 123 b= 456

注意, 建構子有兩個位置參數 a, b, 在呼叫 MyClass() 建立物件時必須傳入這兩個參數, 否則會出現錯誤, 例如 : 

>>> myobj=MyClass()                  # 建立物件時沒有傳入參數導致錯誤
Traceback (most recent call last):
  File "<pyshell>", line 1, in <module>
TypeError: __init__() missing 2 required positional arguments: 'a' and 'b'

為了避免此問題可以為 __init__() 方法的參數設定預設值, 例如 : 

class MyClass():
    def __init__(self, a="hello", b="world"):     # 設定參數之預設值
        self.a=a
        self.b=b
    def showInfo(self):
        print("a=", self.a, "b=", self.b)      # 透過 self 物件存取其屬性

上面範例是在 __init__() 建構子中設定類別的屬性, 也可以透過呼叫自訂的物件方法設定, 例如 : 

class MyClass():
    def __init__(self, a="hello", b="world"):     # 設定參數之預設值
        self.setA(a)
        self.setB(b)
    def showInfo(self):
        print("a=", self.a, "b=", self.b)      # 透過 self 物件存取其屬性
    def setA(self, a):
        if not isinstance(a, str):
            print("參數 a 必須為字串!")
            self.a=""
        else:
            self.a=a
    def setB(self, b):
        if not isinstance(b, str):
            print("參數 b 必須為字串!")
            self.b=""
        else:
            self.b=b

此處 MyClass 類別自訂了兩個物件方法 setA() 與 setB(), 並且在呼叫它們進行初始化設定前先檢查傳入參數的型別是否為字串, 否則就輸出提示訊息並以空字串填入, 例如 : 

>>> class MyClass():
    def __init__(self, a="hello", b="world"):     # 設定參數之預設值
        self.setA(a)
        self.setB(b)
    def showInfo(self):
        print("a=", self.a, "b=", self.b)      # 透過 self 物件存取其屬性
    def setA(self, a):
        if not isinstance(a, str):
            print("參數 a 必須為字串!")
            self.a=""
        else:
            self.a=a
    def setB(self, b):
        if not isinstance(b, str):
            print("參數 b 必須為字串!")
            self.b=""
        else:
            self.b=b
            
>>> myobj=MyClass()
>>> myobj.showInfo()
a= hello b= world
>>> myobj=MyClass('Hello', 'Tony')
>>> myobj.showInfo()
a= Hello b= Tony
>>> myobj=MyClass(123)
參數 a 必須為字串!
>>> myobj=MyClass('123', 456)
參數 b 必須為字串!

下面是一個比較具體用途的圓類別 Circle 範例 : 

>>> class Circle():                                # 定義圓類別
    def __init__(self, radius=1):             # 建構子
        self.radius=radius                          # 初始化成員
    def get_area(self):                              # 求面積的方法
        return math.pi * self.radius ** 2
    def get_perimeter(self):                      # 求周長的方法
        return a * math.pi * self.radius
    def set_radius(self, radius):                # 設定半徑的方法
        self.radius=radius
    def get_radius(self):                            # 取得半徑的方法
        return self.radius

此類別含有一個屬性 radius, 以及 get_area(), get_perimeter(), get_radius(), 以及 set_radius() 四個函式共五個成員. 建構子 __init__() 除了第一參數 self (物件參考) 外, 還有一個 radius 參數, 當呼叫 Circle() 建立物件時可傳入引數以便設定圓物件的半徑初始值. 此 radius 參數有設預設值 1, 若呼叫 Circle() 時未傳入半徑即以預設值 1 初始化. 注意, 雖然 __init__() 與 set_radius() 內容看似一樣, 但其用途不同, __init__() 僅在呼叫 Circle() 建立物件時會被呼叫一次, 物件建立後若要更改半徑就必須呼叫 set_radius().

呼叫與類別同名之函數 Circle() 可建立 Circle 類別的一個物件實體, 例如 :

>>> class Circle():  
    def __init__(self, radius=1):
        self.radius=radius
    def get_area(self):
        return math.pi * self.radius ** 2
    def get_perimeter(self):
        return a * math.pi * self.radius
    def set_radius(self, radius):
        self.radius=radius
    def get_radius(self):
        return self.radius
    
>>> c=Circle()      # 建立 Circle 物件 (預設半徑)
>>> type(c)
<class '__main__.Circle'>     # 資料類型為 Circle 類別
>>> dir(c)                              # 檢視物件內容 
['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'get_area', 'get_perimeter', 'get_radius', 'radius', 'set_radius']

此例在建立物件時未傳入半徑參數 radius, 故會建立預設半徑為 1 的 Circle 物件 c, 透過 dir() 指令檢視物件內容可知它有五個自訂的資料成員 (高亮度部分), 我們可以透過呼叫物件方法來存取預設半徑物件的成員, 例如 : 

>>> c.get_radius()          # 取得半徑
1
>>> c.get_perimeter()     # 取得周長
9.42477796076938
>>> c.get_area()              # 取得面積
3.141592653589793

建立物件後可以呼叫 set_radius() 方法設定半徑, 這樣呼叫其它方法時結果也會同時改變, 例如 :

>>> c.set_radius(5)       # 更改物件的 radius 屬性
>>> c.get_radius()         # 取得物件屬性
5
>>> c.get_perimeter()   # 取得周長
47.12388980384689
>>> c.get_area()            # 取得面積
78.53981633974483

可見面積與周長都改變了. 


3. 動態綁定 : 

由於 Python 是動態語言, 可在執行時期改變資料的結構, Python 的類別與物件也可以在執行時期動態綁定與修改其成員, 這是與靜態語言如 Java 與 C++ 的物件導向特性不同之處. 

例如上面所定義的空類別 MyClass, 可以先建立空物件後再動態綁定新增其屬性與方法 :

>>> class MyClass():       # 定義一個空類別
    pass

>>> myobj=MyClass()    # 建立空物件
>>> myobj.a="Hello"      # 動態設定屬性, 若不存在就建立屬性並賦值
>>> myobj
<__main__.MyClass object at 0x000001E5C5C9B860>
>>> dir(myobj)                # 檢視物件內容
['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'a']           # 已新增了 a 屬性
>>> myobj.a                     # 印出屬性 a 之值
'Hello'

除了用點運算子直接綁定一個新增屬性外, 也可以呼叫內建函式 setattr() 來設定物件之屬性值, 若該屬性不存在就建立新屬性並與以賦值, 其語法為 :

setattr(物件, 屬性, 值)   

例如 :

>>> setattr(myobj, "b", "World")         # 設定屬性 b 之值為 "World", 若屬性不存在就建立
>>> dir(myobj)
['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'a''b']       # 新增了 b 屬性
>>> myobj.b
'World'

動態綁定方法需要匯入標準函式庫內建的 types 模組, 利用其 MethodType() 函式將要作為方法的函式綁定到物件中, 其語法如下 :

types.MethodType(函式, 物件)  

參考 :


先定義要做為物件方法的函式 showInfo(), 然後匯入 types 模組再進行綁定 :

>>> def showInfo(self):                       # 定義物件方法 
    print("a=", self.a, "b=", self.b)
    
>>> import types                                # 匯入 types 模組
>>> myobj.showInfo=types.MethodType(showInfo, myobj)      # 將函式綁定到物件中
>>> dir(myobj)
['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'a', 'b', 'showInfo']     # 檢視物件多了 showInfo 成員
>>> myobj.showInfo()                    # 呼叫物件方法
a= Hello b= World

可見與上面用標準方式於類別中定義好成員再產生物件的結果相同, 雖然動態綁定非常具有彈性, 自由度很高, 但這種用法不符合物件導向的設計理念, 對程式開發與維護並沒有好處, 只適合用在原型測試, 不建議用在軟體專案開發上. 


4. 類別變數, 類別方法與靜態方法 :   

類別變數是定義在方法外面的變數, 是此類別所建立的物件共享之變數 (每一個物件的該變數值都一樣), 在類別內存取類別變數的方式須視方法之不同而定. Python 類別中的方法有三種 : 
  • 物件方法 
  • 類別方法
  • 靜態方法
沒有任何修飾器的是物件方法, 又稱為實體方法, 因為必須透過物件實體來呼叫, 其操作對象主要是物件變數, 因此會自動傳入物件本身 (self), 但也可以透過 "self.__class__.類別變數" 來操作類別變數. 

類別方法與靜態方法操作的對象都只能是類別變數, 前面有 @classmethod 修飾器的是類別方法, 它會自動傳入類別本身 (cls) 作為第一參數用來存取類別變數; 前面有 @staticmethod 修飾器的是靜態方法, 它與類別方法的差別是它不會自動傳入類別本身, 因此必須使用類別名稱來存取類別變數. 類別方法與靜態方法都屬於類別本身, 因此不須建立物件, 可以直接用類別名稱呼叫. 

在物件方法中可以使用兩種方式來存取類別變數 : 
  • 類別名稱.類別物件名稱
  • self.__class__.類別物件名稱
外部程式碼則可以使用類別名稱或物件名稱來存取類別變數 :
  • 類別名稱.類別變數名稱
  • 物件名稱.類別變數名稱
參考 :


例如 : 

>>> class MyClass():
    count=0                              # 累計物件個數用的類別變數
    def __init__(self, name):
        self.name=name
        MyClass.count += 1      # 建立新物件時就累計加 1 
    def showInfo1(self):
        print("物件名稱", self.name, "目前共有", MyClass.count, "個物件")    
    def showInfo2(self):
        print("物件名稱", self.name, "目前共有", self.__class__.count, "個物件")


此 MyClass 類別有三個物件方法, 建構子方法 __init__() 與 showInfo1() 都使用類別名稱來存取類別變數; showInfo2() 則使用 self.__class__ 屬性先取得類別本身再存取 count, 效果一樣. 
        
>>> myobj1=MyClass("tony")    # 建立第一個物件
>>> myobj1.showInfo1()
物件名稱 tony 目前共有 1 個物件
>>> myobj1.showInfo2()
物件名稱 tony 目前共有 1 個物件    # 結果與 showInfo1() 相同
>>> myobj2=MyClass("foo")      # 建立第二個物件
>>> myobj1.showInfo1()
物件名稱 tony 目前共有 2 個物件
>>> myobj2.showInfo2()
物件名稱 foo 目前共有 2 個物件
>>> myobj3=MyClass("bar")      # 建立第三個物件
>>> myobj1.showInfo1()
物件名稱 tony 目前共有 3 個物件
>>> myobj2.showInfo2()
物件名稱 foo 目前共有 3 個物件
>>> myobj3.showInfo2()
物件名稱 bar 目前共有 3 個物件
>>> MyClass.count              # 外部程式用類別名稱讀取類別變數
3
>>> myobj1.count                # 外部程式用物件名稱讀取類別變數
3
>>> myobj2.count                # 外部程式用物件名稱讀取類別變數
3
>>> myobj3.count                # 外部程式用物件名稱讀取類別變數
3

可見外部程式碼不論用類別名稱或物件變數名稱讀取類別變數 count, 其值都是相同的, 因為都是指向同一變數. 類別變數屬於該類別的全部物件共享; 而物件變數 (例如 name) 則屬於個別物件.

類別方法只能用來存取類別變數, 不能存取物件變數 (因為不能傳入 self), 其第一個參數必須是類別本身 (呼叫時自動傳入), 慣例上通常使用 cls (也可以用任何合法名稱), 例如 : 

>>> class MyClass():
    count=0                      # 類別變數
    def __init__(self, name):
        self.name=name
        MyClass.count += 1
    def showInfo(self):
        print("物件名稱", self.name, "目前共有", MyClass.count, "個物件")
    @classmethod               # getCount() 的修飾器, 宣告其為類別方法 (無參數)
    def getCount(cls):
        return cls.count
    @classmethod               # setCount() 的修飾器, 宣告其為類別方法 (有參數)
    def setCount(cls, count):
        cls.count=count

>>> MyClass.setCount(0)    # 類別方法不須建立物件可直接用類別名稱呼叫
>>> MyClass.getCount()      # 類別方法不須建立物件可直接用類別名稱呼叫
0    
>>> myobj1=MyClass('tony')
>>> myobj1.showInfo()
物件名稱 tony 目前共有 1 個物件
>>> MyClass.getCount()     # 用類別名稱存取
>>> myobj1.getCount()       # 用物件名稱存取
1
>>> myobj2=MyClass('foo')    # 建立第二個物件
>>> myobj2.getCount()
2
>>> myobj2.showInfo()
物件名稱 foo 目前共有 2 個物件
>>> MyClass.getCount()         # 以類別名稱呼叫類別方法
2
>>> MyClass.setCount(10)      # 呼叫類別方法 (有參數)
>>> myobj2.getCount()
10
>>> myobj1.getCount()           # 以物件名稱呼叫類別方法
10
>>> MyClass.getCount()         # 以類別名稱呼叫類別方法
10

靜態方法與類別方法同樣都只能用來存取類別變數, 不能存取物件變數, 兩者的差別是靜態方法不會像類別方法那樣自動傳入類別本身 (無 cls 可用), 因此在類別方法內部必須使用類別名稱來存取類別變數, 而外部程式碼可以使用類別名稱或物件名稱來呼叫靜態方法, 即使沒有建立物件, 也可以直接用類別名稱呼叫, 例如 : 

>>> class MyClass():
    count=0                      # 類別變數
    def __init__(self, name):
        self.name=name
        MyClass.count += 1
    def showInfo(self):
        print("物件名稱", self.name, "目前共有", MyClass.count, "個物件")
    @staticmethod               # getCount() 的修飾器, 宣告其為靜態方法 (無參數)
    def getCount():
        return MyClass.count
    @staticmethod               # setCount() 的修飾器, 宣告其為靜態方法 (有參數)
    def setCount(count):
        MyClass.count=count
        
>>> MyClass.setCount(0)    # 靜態方法不須建立物件可直接用類別名稱呼叫
>>> MyClass.getCount()      # 靜態方法不須建立物件可直接用類別名稱呼叫
0
>>> myobj1=MyClass('tony')               # 建立第一個物件
>>> myobj1.showInfo()                        # 呼叫物件方法
物件名稱 tony 目前共有 1 個物件
>>> myobj2=MyClass('foo')                 # 建立第二個物件
>>> myobj2.showInfo()
物件名稱 foo 目前共有 2 個物件   
>>> myobj1.showInfo()
物件名稱 tony 目前共有 2 個物件
>>> MyClass.getCount()                       # 以類別名稱呼叫靜態方法
2
>>> myobj1.getCount()                         # 以物件名稱呼叫靜態方法
2
>>> myobj2.getCount()                         # 以物件名稱呼叫靜態方法
2
>>> MyClass.setCount(10)                   # 以類別名稱呼叫靜態方法 (setter)
>>> myobj1.getCount()                         # 以物件名稱呼叫靜態方法
10
>>> myobj2.getCount()                         # 以物件名稱呼叫靜態方法
10
>>> MyClass.getCount()                       # 以類別名稱呼叫靜態方法
10
>>> myobj1.showInfo()                        
物件名稱 tony 目前共有 10 個物件
>>> myobj2.showInfo()
物件名稱 foo 目前共有 10 個物件
>>> 


5. 存取權限控制 : 

在物件導向設計中, 資料與程式碼被封裝在類別裡面, 目的是可限制外部程式碼對類別成員的存取權限, 以提升軟體之強固性 (robustness) 與隱蔽性 (privacy), 存取權限分成三類 :
  • 公開 (public) : 
    不限制存取, 外部程式碼也可自由存取, 也會被子類別繼承
  • 保護 (protected) : 
    只限類別內可存取, 也會被子類別繼承
  • 私有 (private) :
    只限類別內可存取, 不會被子類別繼承
因為 Python 管控存取權限是透過對成員的名稱做特定的規範, 所以 Python 類別的成員在預設情況下都是公開的, 非公開之成員是對名稱做如下之特別處理 : 
  • 保護 (protected) :
    成員以一個底線開頭 (protected), 例如 _name 或 _showInfo()
  • 私有 (private) :
    以兩個底線開頭 (private) : 例如 __name 或 __showInfo()
不過 Python 的保護其實只是掩耳盜鈴, 防君子不防小人, 其所謂的保護只是著眼於一般人較少會用底線開頭來為成員命名, 從而被直接存取的機會較少而已, 例如 :

>>> class MyClass():
    def __init__(self, a="hello", b="world"):     # 設定參數之預設值
        self._a=a                # 被保護的屬性
        self.__b=b             # 私有的屬性
    def showInfo(self):
        print("a=", self._a, "b=", self.__b)      # 透過 self 物件存取其屬性
        
>>> myobj=MyClass()
>>> myobj._a                # 外部程式碼仍可存取被保護的屬性
'hello'
>>> myobj._a=123        # 被保護的屬性可以被更改
>>> myobj._a
123
>>> myobj.showInfo()     # 屬性 _a 真的被改變了
a= 123 b= world

可見只要知道屬性名稱是單底線開頭就可以自由地存取它, 資料並沒有真正被隱藏. 反觀雙底線開頭的屬性則會被隱藏, 外部程式碼無法直接存取它, 只能透過公開的方法取得其值 : 

>>> myobj.__b                 # 外部程式碼無法存取私有的屬性
Traceback (most recent call last):
  File "<pyshell>", line 1, in <module>
AttributeError: 'MyClass' object has no attribute '__b'    
>>> myobj.showInfo()     # 私有屬性只能透過公開方法取得  
a= 123 b= world    

如果直接改變私有屬性之值, 雖然不會出現錯誤, 但其實並沒有真的被改變 :

>>> myobj.__b=456         # 企圖更改私有屬性之值
>>> myobj.__b                 # 檢視似乎值被改變了
456
>>> myobj.showInfo()     # 呼叫公開方法 showInfo() 顯示並沒有被改變
a= 123 b= world     

單底線開頭的屬性不僅外部程式碼可讀寫 _a 屬性, 也可以用 del 指令刪除它, 例如 : 

>>> del myobj._a              # 單底線開頭屬性可用 del 刪除
>>> myobj.showInfo()
Traceback (most recent call last):
  File "<pyshell>", line 1, in <module>
  File "<pyshell>", line 6, in showInfo
AttributeError: 'MyClass' object has no attribute '_a'      

但雙底線開頭的私有屬性無法用 del 刪除, 會出現 AttributeError 錯誤訊息, 例如 :

>>> del myobj.__b               # 無法刪除私有屬性
  Traceback (most recent call last):
  File "<pyshell>", line 1, in <module>
AttributeError: __b   

只有在類別內部才能更改私有屬性之值, 例如重新定義 MyClass 類別, 新增一個設定私有屬性值的 setter 公開方法 set__b() :

>>> class MyClass():    
    def __init__(self, a="hello", b="world"):     # 設定參數之預設值
        self._a=a                # 被保護的屬性
        self.__b=b             # 私有的屬性
    def showInfo(self):
        print("a=", self._a, "b=", self.__b)      # 透過 self 物件存取其屬性
    def set__b(self, b):     # 私有屬性 __b 的公開設定方法
        self.__b=b
        
>>> myobj=MyClass()        # 建立物件
>>> myobj.showInfo()         # 顯示屬性值
a= hello b= world
>>> myobj.set__b(456)       # 呼叫公開方法設定私有屬性之值
>>> myobj.showInfo()        # 顯示屬性值
a= hello b= 456    

可見私有屬性確實被封裝隱藏在類別內部, 只能透過公開的設定方法才能存取. 

不過 Python 的類別對資料的隱藏與保護並沒有像 Java/C++ 那樣嚴密, 因為私有屬性其實是以特定名稱儲存 (在私有屬性名稱前面冠上單底線與類別名稱 "_類別"), 可用 dir() 檢視物件內容看到此屬性, 例如 :

>>> class MyClass():    
    def __init__(self, a="hello", b="world"):     # 設定參數之預設值
        self._a=a                # 被保護的屬性
        self.__b=b             # 私有的屬性
    def showInfo(self):
        print("a=", self._a, "b=", self.__b)      # 透過 self 物件存取其屬性
    def set__b(self, b):     # 私有屬性 __b 的公開設定方法
        self.__b=b
        
>>> myobj=MyClass()        # 建立物件
>>> dir(myobj)
['_MyClass__b', '__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', '_a', 'set__b', 'showInfo'

用 dir() 可發現, 與 _a 屬性不同的是, 私有屬性 __b 其實是以 _MyClass__b 之名存放, 而非類別定義中的 __b, 所以外部程式碼直接存取 __b 會失敗, 但若改用 _MyClass__b 即可順利存取, 也可以用 del 將私有屬性刪除, 例如 :

>>> myobj.showInfo()         # 顯示屬性值
a= hello b= world
>>> myobj._MyClass__b=789    # 用特殊方法修改私有屬性值
>>> myobj._MyClass__b
789
>>> myobj.showInfo()                 # 確認私有屬性值已被修改
a= hello b= 789   
>>> del myobj._MyClass__b       # 用 del 可順利刪除冠上 "_類別" 之私有變數
>>> myobj.showInfo()
Traceback (most recent call last):      # 呼叫 showInfo() 確認 __b 已經被刪除
  File "<pyshell>", line 1, in <module>
  File "<pyshell>", line 6, in showInfo
AttributeError: 'MyClass' object has no attribute '_MyClass__b' 

同樣地, 私有方法基本上只限類別內部呼叫, 但外部程式碼還是可以用 "_類別__私有方法()" 的方式呼叫, 例如 : 

>>> class MyClass():           
    def __init__(self, a="Hello"):
        self.__a=a
    def foo(self, b):             # 公開方法
        self.__bar(b)             # 類別內才可以呼叫私有方法
    def __bar(self, b):         # 私有方法
        print(self.__a + " " + b)
        
>>> myobj=MyClass()      # 建立物件
>>> dir(myobj)
['_MyClass__a', '_MyClass__bar', '__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'foo']

用 dir() 檢視物件可知, 私有方法 __bar 其實在物件內部是以 _MyClass__bar 之名作為參考, 而非類別定義中的 __bar, 所以直接呼叫 __bar() 會失敗, 但呼叫 _MyClass__bar() 卻可以 : 

>>> myobj.foo('Tony')      # 透過呼叫公開方法才能呼叫私有方法
Hello Tony
>>> myobj.__bar('Tony')    # 外部程式碼不可直接呼叫私有方法 
  Traceback (most recent call last):
  File "<pyshell>", line 1, in <module>
AttributeError: 'MyClass' object has no attribute '__bar'       #外部程式碼看不到私有方法  
>>> myobj._MyClass__bar('Tony')      # 但可以利用特殊管道呼叫私有方法
Hello Tony

由上面測試可知, 外部程式碼無法直接呼叫私有方法, 必須透過類別內的公開方法間接呼叫, 可見 Python 的私有成員命名機制雖然可基本上達到隱藏成員之目的, 但並沒有完全被封裝起來. 

雖然私有變數無法被外部存取, 必須透過公開的物件方法, 但可否像存取屬性那樣而非呼叫函式? 可以的, 只要利用 @property 修飾器將此物件方法轉成屬性模式即可. 

具體作法是定義一個傳回私有變數值的公開物件方法, 其名稱為私有變數去掉前面的雙底線, 然後在此物件方法前面添加 @property 修飾器將此方法變身為屬性 :

class MyClass():           
    def __init__(self, a="Hello"):
        self.__a=a
    @property  
    def a(self):             
        return self.__a

此類別定義了一個與私有變數 __a 的詞幹 a 同名的物件方法 a(), 其內容只是單純地傳回私有屬性之值 (唯讀), 然後在 a() 方法前面加上 @property 修飾器, 這樣外部程式碼就可以用 .a 屬性讀取私有屬性 __a 之值, 而非呼叫 a() 方法了, 但只能唯讀, 不可更改, 也不可刪除, 例如 : 

>>> class MyClass():                
    def __init__(self, a="Hello"):
        self.__a=a
    @property                # 修飾器, 將方法 a() 修飾為物件屬性
    def a(self):             
        return self.__a      # 傳回私有屬性之 值
    
>>> myobj=MyClass()
>>> dir(myobj)            # 檢視物件內容
['_MyClass__a', '__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'a']

可見除了真實名稱為 _MyClass__a 的私有變數 __a 外, 還多了一個 a 方法, 但它已被綁定
到屬性, 故要以屬性的方式存取, 而不是呼叫方法, 例如 :

>>> myobj.a                    # 讀取私有屬性值
'Hello'
>>> myobj.a()                 # 被修飾器綁定為屬性後無法被呼叫 (not callable)
Traceback (most recent call last):
  File "<pyshell>", line 1, in <module>
TypeError: 'str' object is not callable    
>>> del myobj.a              # 無法刪除私有屬性
Traceback (most recent call last):
  File "<pyshell>", line 1, in <module>
AttributeError: can't delete attribute      
>>> myobj.a="World"     # 無法設定私有屬性之值
Traceback (most recent call last):
  File "<pyshell>", line 1, in <module>
AttributeError: can't set attribute

可見此 a 屬性只能讀取, 不能設定, 也不能刪除. @property 修飾器其實是內建函式 property() 的 語法糖 (只能用在 getter ), 此唯讀功能也可以用 property() 函式這本尊來實現, 其參數如下 :

attr=property(fget [, fset [, fdel [, fdoc]]])     

常用的是前三個參數, fget 為讀取方法 (getter); fset 為設定方法 (setter), fdel 為刪除方法, 若只傳入 fget 表示傳回之屬性為唯讀; 若傳入 fget 與 fset 表示傳回之屬性可讀寫; 若傳入三個參數表示傳回之屬性可讀寫可刪除, 參考 : 


property() 函式會傳回一個代表屬性的參考, 須指配一個代表私有變數的公開屬性名稱作為外部程式碼的存取對象, 例如 : 

>>> class MyClass():           
    def __init__(self, a="Hello"):
        self.__a=a
    def __getA(self):            # 定義讀取私有屬性之物件方法
        return self.__a
    a=property(__getA)        # 只傳入第一參數 (唯讀)
    
>>> myobj=MyClass()
>>> dir(myobj)
['_MyClass__a', '_MyClass__getA', '__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'a']
>>> myobj.a                      # 透過公開屬性讀取私有屬性 __a
'Hello'
>>> myobj.a='World'         # 唯讀, 不能設定
Traceback (most recent call last):
  File "<pyshell>", line 1, in <module>
AttributeError: can't set attribute
>>> del myobj.a                 # 不可刪除屬性
Traceback (most recent call last):
  File "<pyshell>", line 1, in <module>
AttributeError: can't delete attribute

可見功能與使用 @property 修飾器完全一樣. 注意, property() 的傳回值也可以指配給其他名稱變數例如 v, 因為它是代表私有變數 __a 的公開屬性, 這樣外部程式碼就要用 myobj.v 來存取私有屬性 __a 了. 

若要可讀寫私有屬性, 則必須再定義一個設定方法, 並將其傳給 property() 的第二參數 fset, 例如 :

class MyClass():           
    def __init__(self, a="Hello"):
        self.__a=a
    def __getA(self):             
        return self.__a
    def __setA(self, a):
        self.__a=a
    a=property(__getA, __setA)

此類別定義了兩個私有方法 __getA() 與 __setA() 用來讀寫私有變數 __a, 然後將此兩方法傳入內建函式 property(fget, fset), 將此函式的傳回值指派給類別變數 a, 這樣就可用它來代表私有變數的公開屬性名稱, 例如 : 

>>> class MyClass():           
    def __init__(self, a="Hello"):
        self.__a=a
    def __getA(self):             # 私有取得方法
        return self.__a
    def __setA(self, a):         # 私有設定方法
        self.__a=a
    a=property(__getA, __setA)      # 傳回可讀寫之屬性
    
>>> myobj=MyClass()
>>> dir(myobj)
['_MyClass__a', '_MyClass__getA', '_MyClass__setA', '__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'a']
>>> myobj.a                    # 讀取屬性 a
'Hello'
>>> myobj.a='World'       # 設定屬性 a
>>> myobj.a
'World'
>>> del myobj.a              # 未傳入刪除方法故不能刪除屬性 a
Traceback (most recent call last):
  File "<pyshell>", line 1, in <module>
AttributeError: can't delete attribute

如果要讓私有變數可讀寫可刪除, 則必須在 property() 函數中傳入 fdel 方法 :

class MyClass():           
    def __init__(self, a="Hello"):
        self.__a=a
    def __getA(self):             
        return self.__a
    def __setA(self, a):
        self.__a=a
    def __delA(self):
        del self.__a   
    a=property(__getA, __setA, __delA)

此類別定義了一個物件方法 __delA() 用來刪除 __a 屬性, 將此方法傳入 property() 作為第三參數即可使外部程式碼透過其傳回的公開屬性 a 刪除私有 __a, 例如 : 

>>> class MyClass():           
    def __init__(self, a="Hello"):
        self.__a=a
    def __getA(self):             
        return self.__a
    def __setA(self, a):
        self.__a=a
    def __delA(self):          # 定義刪除私有屬性 __a 之方法
        del self.__a
    a=property(__getA, __setA, __delA)      # 將刪除方法作為第三參數
    
>>> myobj=MyClass()
>>> dir(myobj)
['_MyClass__a', '_MyClass__delA', '_MyClass__getA', '_MyClass__setA', '__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'a']
>>> myobj.a
'Hello'
>>> myobj.a='World'      # 透過屬性 a 讀寫私有屬性 __a
>>> myobj.a
'World'
>>> del myobj.a             # 刪除屬性 a 即刪除私有屬性 __a 
>>> myobj.a                   # 屬性 a 已被刪除無法存取
Traceback (most recent call last):
  File "<pyshell>", line 1, in <module>
  File "<pyshell>", line 5, in __getA
AttributeError: 'MyClass' object has no attribute '_MyClass__a'
>>> dir(myobj)               # 檢視物件內容已無私有屬性 _a 之真實名稱 _MyClass__a
['_MyClass__delA', '_MyClass__getA', '_MyClass__setA', '__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'a']

可見經由傳入可刪除私有屬性 __a 的方法給 property() 的第三參數, 確實可透過刪除屬性 a 來刪除私有屬性 __a. 

注意, property() 的傳回值雖然指配給一個類別變數, 但因為它是綁定到物件變數, 所以並不會像一般類別變數一樣成為物件的共同變數, 例如 : 

>>> class MyClass():           
    def __init__(self, a="Hello"):
        self.__a=a
    def __getA(self):             
        return self.__a
    def __setA(self, a):
        self.__a=a
    def __delA(self):
        del self.__a
    a=property(__getA, __setA, __delA)
    
>>> myobj1=MyClass()
>>> myobj1.a
'Hello'
>>> myobj2=MyClass('World')
>>> myobj2.a
'World'
>>> myobj1.a
'Hello'

此例用 MyClass 類別建立的兩個物件 myobj1 與 myobj2 的屬性 a 值並不同, 顯示雖然 a 在類別定義中的位置屬於類別變數, 但因為它被綁定到私有的物件變數 __a (其實就是 _MyClass__a), 因此實際上它是物件變數, 不同物件的 a 屬性值不會一樣. 


6. 繼承 (inheretance) : 

程式碼可重用是軟體開發的不變目標, 在程序導向設計中, 程式碼可重用體現在函式的設計上; 而在物件導向設計中則擴展至類別的繼承機制, 這是比單純的函式呼叫更精緻的可重用結構, 從繼承也衍生了多型 (polymorphism) 的概念. 

Python 的類別繼承關係體現於類別定義標頭的小括號中 :

class 子類別名稱(父類別名稱):
    #定義類別成員

如果沒有要繼承之父類別, 則整個括號也可以省略, 或留一個空括號亦可, 這時預設會繼承 Python 名為 object 的根類別, 故也可以寫明繼承 object. 

子類別將繼承父類別的全部非私有方法 (即名稱不是以雙底線開頭的方法), 但卻不會自動繼承其非私有屬性 (即名稱不是以雙底線開頭的屬性), 例如 :

>>> class Parent():    # 定義父類別
    def __init__(self, a='foo'):
        self.a=a
    def set_a(self, a):
        self.a=a
    def get_a(self):
        return self.a
    
>>> class Child(Parent):    # 定義繼承 Parent 之子類別
    def __init__(self, b='bar'):
        self.b=b
    def set_b(self, b):
        self.b=b
    def get_b(self):
        return self.b

上面定義了一個具有公開屬性 a 的父類別 Parent, 以及一個繼承 Parent 且具有公開屬性 b 的子類別 Child, 用 dir() 函式檢視類別內容可知子類別 Child 繼承了父類別的兩個公開方法 get_a() 與 get_b() : 
    
>>> dir(Parent) 
['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'get_a', 'set_a']
>>> dir(Child)
['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'get_a', 'get_b', 'set_a', 'set_b']

可見因為子類別 Child 從父類別繼承了 get_a() 與 set_a(), 所以它共有四個方法. 但 Child 並沒有從 Parent 繼承其公開屬性 a, 例如 : 

>>> parent_obj=Parent()      # 建立父類別物件
 >>> parent_obj.get_a()        # 呼叫父類別的物件方法 get_a()
'foo'
>>> child_obj=Child()         # 建立子類別物件
>>> dir(child_obj)                # 檢視子類別物件內容 (沒有繼承到父類別的屬性 a)  
['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'b', 'get_a', 'get_b', 'set_a', 'set_b']     # 只有屬性 b
>>> child_obj.get_a()          # 呼叫繼承自父類別的物件方法 get_a() 失敗
Traceback (most recent call last):
  File "<pyshell>", line 1, in <module>
  File "<pyshell>", line 7, in get_a
AttributeError: 'Child' object has no attribute 'a'      # 子類別無屬性 a 
 >>> child_obj.get_b()        # 呼叫父子類別的物件方法 get_b() 成功
'bar'
>>> child_obj.set_a()          # 呼叫繼承自父類別的物件方法 set_a() 失敗
Traceback (most recent call last):
  File "<pyshell>", line 1, in <module>
TypeError: set_a() missing 1 required positional argument: 'a'

由上面測試可知, 父類別的方法會被自動繼承, 但屬性並不會 (檢視物件 child_obj 內容可知它沒有屬性 a), 必須先呼叫 super() 取得父類別參考, 然後呼叫父類別的建構子即可, 修改上面的子類別, 加上呼叫父類別建構子的 super().__init__() 如下 : 

>>> class Child(Parent):
    def __init__(self, b='bar'):
        super().__init__()             # 呼叫父類別的建構子 
        self.b=b
    def set_b(self, b):
        self.b=b
    def get_b(self):
        return self.b

重新建立子類別之物件, 用 dir() 檢視物件內容發現已經繼承了父類別的屬性 a, 呼叫繼承自父類別的 get_a() 方法也不會出現錯誤了 : 

>>> child_obj=Child()
>>> dir(child_obj)           
['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'a', 'b', 'get_a', 'get_b', 'set_a', 'set_b']
>>> child_obj.get_a()      # 呼叫繼承自父類別的 get_a() 方法      
'foo'    
 
可見子類別的建構子務必要用 super().__init__() 呼叫父類別的建構子, 這樣才能繼承父類別的非私有屬性. 

在繼承父類別時, 私有成員不會被繼承, 例如定義一個具有公開, 保護, 與私有成員之父類別 : 

>>> class Parent():
    def __init__(self):
        self.public_var="父類別的公開屬性"
        self._protected_var="父類別的保護屬性"
        self.__private_var="父類別的私有屬性"
    def public_method(self):
        return "父類別的公開方法"
    def _protected_method(self):
        return "父類別的保護方法"
    def __private_method(self):
        return "父類別的私有方法"

然後定義一個繼承 Parent 類別的子類別 Child, 並於建構子中呼叫父類別的建構子 :

>>> class Child(Parent):     # 繼承父類別 Parent
    def __init__(self):
        super().__init__()        # 呼叫父類別的建構子

接著建立一個子類別物件, 用 dir() 檢視其內容可知父類別的公開與被保護的成員都會被子類別繼承, 可以直接存取; 私有成員則不會被繼承 : 

>>> child_obj=Child()
>>> dir(child_obj)
['_Parent__private_method', '_Parent__private_var', '__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', '_protected_method', '_protected_var', 'public_method', 'public_var']
>>> child_obj.public_var
'父類別的公開屬性'
>>> child_obj._protected_var
'父類別的保護屬性'
>>> child_obj.public_method()
'父類別的公開方法'
>>> child_obj._protected_method()
'父類別的保護方法'

觀察上面子類別物件的 dir() 結果可知, 其實子類別暗中還是有繼承父類別的私有成員, 只是前面冠上了底線與類別名稱 _Parent 改名了, 就名稱來說也算是沒被繼承 : 

>>> child_obj._Parent__private_var            # 偷偷存取父類別的私有屬性
'父類別的私有屬性'
>>> child_obj._Parent__private_method()   # 偷偷呼叫父類別的私有方法
'父類別的私有方法'

子類別可以覆寫這些繼承來的公開與被保護成員與定義自己的私有成員, 例如 : 

>>> class Child(Parent):
    def __init__(self):
        super().__init__()
        self.public_var="子類別的公開屬性"
        self._protected_var="子類別的保護屬性"
        self.__private_var="子類別的私有屬性"
    def public_method(self):
        return "子類別的公開方法"
    def _protected_method(self):
        return "子類別的保護方法"
    def __private_method(self):
        return "子類別的私有方法"
    
>>> child_obj=Child()
>>> dir(child_obj)
['_Child__private_method', '_Child__private_var', '_Parent__private_method', '_Parent__private_var', '__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', '_protected_method', '_protected_var', 'public_method', 'public_var']
>>> child_obj.public_var
'子類別的公開屬性'
>>> child_obj._protected_var
'子類別的保護屬性'
>>> child_obj._Child__private_var     # 偷偷存取私有屬性
'子類別的私有屬性'
>>> child_obj.public_method()
'子類別的公開方法'
>>> child_obj._protected_method()
'子類別的保護方法'
>>> child_obj._Child__private_method()    # 偷偷呼叫私有方法
'子類別的私有方法'

可見同名的成員已經被子類別覆寫了, 不過子類別中的同名私有成員並非被覆寫, 而是定義了自己的私有成員. 注意, 一般來說外部程式碼無法直接存取私有成員, 但在 Python 中可利用在名稱前面冠上 "_類別名稱" 的方式存取私有成員. 

Python 支援多重繼承, 一個類別可以同時繼承多個類別, 只要在類別定義時將各父類別作為形式參數以逗號隔開傳入即可, 子類別會依照順序自左向右逐一繼承 :

class 子類別名稱(父類別1, 父類別2, ...):    
   #定義類別成員  

從上面範例已知, 即使沒有明確繼承任何父類別, 仍會隱性地繼承名為 object 的根類別. 

例如 : 

>>> class Parent_Class_1():       # 定義父類別 1
    def parent_method_1(self):
        print('呼叫父類別 1 的 parent_method_1() 方法')
        
>>> class Parent_Class_2():       # 定義父類別 2
    def parent_method_2(self):
        print('呼叫父類別 2 的 parent_method_2() 方法')
        
>>> class Child_Class(Parent_Class_1, Parent_Class_2):     # 定義子類別繼承父類別 1, 2
    def child_method(self):
        print('呼叫子類別的 child_method() 方法')
        
>>> child=Child_Class()                                 # 建立子類別物件
>>> child.parent_method_1()                         # 呼叫父類別之方法 1
呼叫父類別 1 的 parent_method_1() 方法
>>> child.parent_method_2()                         # 呼叫父類別之方法 2
呼叫父類別 2 的 parent_method_2() 方法
>>> child.child_method()                                # 呼叫子類別之方法
呼叫子類別的 child_method() 方法

此例先定義兩個父類別 Parent_Class_1 與 Parent_Class_2, 再繼承此二類別定義其子類別 Child_Class, 然後建立子類別物件, 再逐一呼叫從兩個父類別所繼承來的方法以及子類別自己的方法. 

要注意的是, Python 的多重繼承是有順序的, 位置越左越優先, 如果各父類別的成員都沒有同名則順序就不是個問題, 因為所有父類別的成員都會被子類別繼承; 但若有同名現象時, 較左的父類別成員會被繼承, 而較右的父類別同名成員則被捨棄.

修改上面範例, 將兩個父類別的方法改為同名的 parent_method(), 子類別繼承時先繼承父類別 1, 再繼承父類別 2, 重新建立物件後呼叫 parent_method() 結果顯示子類別是繼承了父類別 1 的方法, 捨棄了父類別 2 的方法  :

>>> class Parent_Class_1():
    def parent_method(self):
        print('呼叫父類別 1 的 parent_method() 方法')
        
>>> class Parent_Class_2():
    def parent_method(self):
        print('呼叫父類別 2 的 parent_method() 方法')

>>> class Child_Class(Parent_Class_1, Parent_Class_2):
    def child_method(self):
        print('呼叫子類別的 child_method() 方法')

>>> child=Child_class()
>>> child.parent_method()
呼叫父類別 1 的 parent_method() 方法

如果修改子類別定義, 調換父類別順序, 改為先繼承父類別 2, 則將繼承其 parent_method() 方法 :

>>> class Child_class(Parent_class_2, Parent_class_1):
    def child_method(self):
        print('呼叫子類別的 child_method() 方法')
        
>>> child=Child_class()
>>> child.parent_method()
呼叫父類別 2 的 parent_method() 方法

上面範例為方法的繼承, 屬性的繼承也是如此. 可將上面的範例修改如下 :

>>> class Parent_Class_1():          # 父類別 1
    def __init__(self):
        self._name='Parent Class 1'   # 同名屬性
    def show_name(self):
        print(self._name)
        
>>> class Parent_Class_2():          # 父類別 2
    def __init__(self):
        self._name='Parent Class 2'   # 同名屬性
    def show_name(self):
        print(self._name)
        
>>> class Child_Class(Parent_Class_1, Parent_Class_2):     # 子類別優先繼承父類別 1 的屬性
    def __init__(self):
        super().__init__()          # 呼叫父類別建構子以繼承其成員
        
>>> child=Child_Class()
>>> child.show_name()        
Parent Class 1                        # 顯示子類別繼承的是父類別 1 的屬性

可見因為在繼承時將父類別 1 擺前面, 所以遇到同名屬性時會優先繼承 Parent_Class_1 的 _name 屬性, 如果把父類別 2 放前面, 則子類別就會優先繼承父類別 2 的 _name 屬性了, 例如 :

>>> class Child_Class(Parent_Class_2, Parent_Class_1):     # 優先繼承父類別 2
    def __init__(self):
        super().__init__()
        
>>> child=Child_Class()
>>> child.show_name()
Parent Class 2                   # 顯示子類別繼承的是父類別 2 的屬性

如果這優先順序因為其他原因不能改, 但 _name 屬性又想繼承父類別 2 的, 則可以在定義子類別時覆寫此屬性, 例如子類別可以改成下面這樣 :

>>> class Child_Class(Parent_Class_1, Parent_Class_2):      # 優先繼承父類別 1 的成員
    def __init__(self):
        super().__init__()
        self._name='Parent Class 2'      # 覆寫繼承自父類別 1 的 _name 屬性
        
>>> child=Child_Class()
>>> child.show_name()
Parent Class 2                   # 覆寫為父類別 2 之 _name 屬性


7. 多型 (polymorphism) :  

多型的概念來自於類別的繼承, 子類別物件透過繼承與覆寫而擁有同名方法, 多型的意思是子類別物件可以被用在代表父類別物件的地方 (意即將子類別物件傳遞給一個代表父類別物件之變數), 當呼叫這個代表父類別物件的同名方法時, Python 解譯器會沿著繼承鏈決定要呼叫哪一個子類別物件的方法. 參考 :


具體來說, 多型通常展現於傳遞給函式的參數或迴圈的迭代變數中, 這參數或變數在形式上雖然代表著父類別物件, 但意義上卻是各個子類別物件, 因此呼叫同名方法時是參考到各子類別所覆寫實作的方法, 故多型又稱為同名異式 (方法名稱相同, 但執行的是不同的程式碼).

在繼承關係中, 達成多型之要件是子類別必須覆寫父類別的方法, 例如 :  

>>> class Book():                        # 定義父類別
    def __init__(self, price):
        self.price=price
    def getPrice(self):                        # 物件方法
        return self.price
    
>>> class ComicBook(Book):         # ComicBook 類別繼承父類別 Book
    def __init__(self, price):
        super().__init__(price)
    def getPrice(self):                        # 覆寫父類別之同名方法
        return self.price * 0.8
    
>>> class ComputerBook(Book):    # ComputerBook 類別繼承父類別 Book
    def __init__(self, price):
        super().__init__(price)
    def getPrice(self):                         # 覆寫父類別之同名方法
        return self.price * 0.9    

>>> books=[ComicBook(100), ComputerBook(100)]   # 建立兩個物件串列
>>> books[0].getPrice()     # 呼叫同名方法
80.0
>>> books[1].getPrice()     # 呼叫同名方法
90.0
>>> total=0                         # 總價初始值
>>> for book in books:       # 用迴圈計算總價
    total += book.getPrice()        # 多型: 呼叫各物件之同名方法
    
>>> total
170.0

此例建立了兩個 Book 的子類別物件, 它們覆寫了繼承自父類別 Book 的同名方法 getPrice() 來計算書打折後的價格 (漫畫書打 8 折, 電腦書打 9 折), 在計算總價的迴圈中呼叫了相同的 book.getPrice() 方法, 看似相同的方法, 但實際上卻會呼叫各物件對應之不同程式碼方法, 故多型又名同形異式. 

另外一個可以說明多型概念的範例如下 :

>>> class Shape():                  # 父類別 
    def getArea(self):
        pass

>>> class Square(Shape):       # 子類別
    def __init__(self, width):
        self.width=width
    def getArea(self):
        print("正方形面積=",self.width ** 2)
        
>>> class Rect(Shape):           # 子類別
    def __init__(self, width, length):
        self.width=width
        self.length=length
    def getArea(self):
        print("矩形面積=",self.width * self.length)

>>> def showArea(shape):       # 函式 (子類別物件傳給像是父類別物件之參數)
    shape.getArea()

>>> shape=Shape()            # 父類別物件
>>> square=Square(10)     # 子類別物件 (正方形)
>>> rect=Rect(10, 20)       # 子類別物件 (矩形)
>>> showArea(square)      # 實際呼叫的是 Square 類別的 getArea()
正方形面積= 100
>>> showArea(rect)          # 實際呼叫的是 Rect 類別的 getArea()
矩形面積= 200

此例中子類別 Square 與 Rect 繼承了父類別 Shape 並實作了其方法 getArea(), 我們另外定義了一個顯示面積的函式 showArea(), 其內容只是呼叫傳入物件的同名方法 getArea() 而已, 此處子類別物件被當成參數傳遞給看起來是父類別物件的 shape 參數, 但實際上並不是去呼叫父類別未實作的 getArea() 方法, 而是在執行當下根據傳入物件決定呼叫哪一個子類別物件的 getArea() 方法. 

OK, 經過近兩周的學習, 終於把 Python 的物件導向測試整理完畢, 我覺得跟以前學的 Java 物件導向有點不一樣啊! Java 的類別比較嚴謹, 至少在權限控制上一板一眼, Python 就沒這麼麻煩. 對於還沒有機會開發大型軟體專案的我來說還感受不到 Java 嚴謹的好處, 倒是覺得 Python 的類別與物件用法簡單易學.