2021年10月21日 星期四

Python 學習筆記 : SciPy 的線性代數子套件 linalg 測試

SciPy 的線性代數子套件 linalg 是以 Numpy 的 ndarry 陣列為基礎建構的, 它提供了比 Numpy 更多更完整的線性代數函式庫, 事實上 SciPy 可視為 Numpy 的擴充集 (superset), 但 Numpy 基於歷史緣故仍保有核心的線性代數函式. 


匯入 SciPy 線性代數子套件 linalg 的方式例如 : 

from scipy.linalg import * 

這樣就可以直接呼叫此子套件的函數了, 常用函數如下表 : 


 scipy.linalg 常用函數 說明
 inv(A) 傳回方陣 A 的反矩陣
 det(A) 傳回方陣 A 的行列式值
 solve(A, b) 傳回線性方程組 Ax=b 的變數向量 x 之解 (串列)
 eig(A) 傳回方陣 A 的特徵值 (eigen value) 與特徵向量 (eigen vector)
 qr(A) 將方陣 A 進行 QR 分解, 傳回正交矩陣 Q 與上三角矩陣 (元組)
 lu(A) 將正規矩陣分解為置換矩陣 P, 上三角矩陣 U, 下三角矩陣 L (元組)
 
 


參考 scipy.linalg 子套件的說明文件 : 



1. 用 inv() 求方陣的反矩陣 (inverse matrix) : 

在線性代數中, 一個 n 階方陣 A 的反矩陣 B 的定義是, A 與 B 的內積為 n 階單元方陣 :  




A 的反矩陣 B 記做 :


一個方陣的反矩陣若存在, 此矩陣稱為可逆 (invertible), 可逆的矩陣又稱為非奇異矩陣 (non-singular matrix); 反之, 若方陣的反矩陣不存在即為不可逆, 不可逆的方陣被稱為奇異矩陣 (singular matrix). 反矩陣若存在, 則它是唯一的, 即一個方陣不可能有兩個反矩陣. 

子套件 linalg 的 inv() 函數可用來求方陣的反矩陣, 例如 : 

>>> import numpy as np    
>>> from scipy import linalg    
>>> A=np.array([[1, -1],[1, 2]])          # 定義一個方陣 A
>>> A   
array([[ 1, -1],
       [ 1,  2]])
>>> B=linalg.inv(A)           # 求 A 的反矩陣 B
>>> B   
array([[ 0.66666667,  0.33333333],
       [-0.33333333,  0.33333333]])
>>> np.dot(A, B)                                              # 計算 A, B 的內積
array([[1.00000000e+00, 0.00000000e+00],
       [1.11022302e-16, 1.00000000e+00]])
>>> np.dot(B, A)                                              # 計算 B, A 的內積
array([[ 1.00000000e+00, -1.11022302e-16],
       [ 0.00000000e+00,  1.00000000e+00]])    

A, B 交換做內積結果都是單元矩陣, 可見 A 的反矩陣為 B. 

反矩陣的用途之一是用來解線性聯立方程組 AX=b, 其中 A 為係數矩陣 (coefficient matrix), X 是變數向量, b 則是右手係數 (right-hand coefficients) 向量, 則方程組的解如下 : 


例如下面這個三元線性聯立方程組 : 


寫成矩陣方程式為 :


此方程組的解為係數矩陣之反矩陣與右手係數向量之內積 :


下面用 linalg 的 inv() 函數來計算其解 : 

>>> A=np.array([[3, 2, 3], [2, 5, -7], [1, 2, -2]])       # 係數矩陣
>>> A   
array([[ 3,  2,  3],
       [ 2,  5, -7],
       [ 1,  2, -2]])
>>> b=np.array([9, -12, -3])         # 右手係數向量
>>> b   
array([  9, -12,  -3])
>>> B=linalg.inv(A)       # 係數矩陣的反矩陣
>>> B 
array([[ 1.33333333,  3.33333333, -9.66666667],
       [-1.        , -3.        ,  9.        ],
       [-0.33333333, -1.33333333,  3.66666667]])
>>> np.dot(B, b)             # 聯立方程組的解
array([ 1.00000000e+00, -7.10542736e-15,  2.00000000e+00])

第二個變數之解近乎 0, 故其解為 [x, y, z]=[1, 0, 2]. 


2. 用 det() 函數求方陣的行列式 (determinant) 值 : 
 
一個二階方陣 A 的行列式的算法為 :


即左上右下對角線元素相乘後相加, 再減所有右上左下對角線元素之乘積. 同樣的算法在三階方陣 A 的行列式為 :




下面用 linalg 的 det() 函數來計算上面兩個方陣的行列式 :

>>> A=np.array([[1, -1],[1, 2]])      # 二階方陣
>>> linalg.det(A)        # 計算方陣 A 的行列式值
3.0
>>> A=np.array([[3, 2, 3], [2, 5, -7], [1, 2, -2]])     # 三階方陣
>>> linalg.det(A)        # 計算方陣 A 的行列式值
3.0000000000000013   


3. 用 solve() 函數求解線性聯立方程組 : 

上面我們使用反矩陣求解線性聯立方程組之解, 事實上呼叫 solve(A, b) 函數並將係數矩陣 A 與右手係數向量 b 傳入分別做為第一與第二參數亦可求解, 它會傳回以串列表示的變數向量, 例如 :

>>> A=np.array([[3, 2, 3], [2, 5, -7], [1, 2, -2]])       # 係數矩陣
>>> A   
array([[ 3,  2,  3],
       [ 2,  5, -7],
       [ 1,  2, -2]])
>>> b=np.array([9, -12, -3])         # 右手係數向量
>>> b   
array([  9, -12,  -3])   
>>> x=linalg.solve(A, b)      # 求解線性聯立方程組
>>> x    
array([1., 0., 2.])        # 變數向量 [x, y, z] 之解

可見答案與上面用反矩陣計算的相同. 


4. 用 eig() 函數求特徵值與特徵向量 :

一個 n 階方陣 A 的特徵值與特徵向量定義如下 : 存在一個非 0 向量 X 與純量 λ 使得 AX=λX, 則 λ 稱為方陣 A 的特徵值, 而向量 X 為其特徵向量. AX=λX 可看成向量 X 經過 A 的線性轉換後得到一個新向量 Y=AX, 而此新向量與原向量 X 呈平行關係, 特徵值即兩平行向量之比例係數. 

AX=λX 經移項後為 (A-λI)X=0, 故 λ 為 A 的特徵值之充要條件為 |A-λI|=0, 將此式展開即可得到 λ 的特徵方程式, 解此方程式之根即可得到特徵值 λ. 呼叫 linalg 子套件的 eig(A) 函數會傳回兩個元素的 tuple, 第一個傳回值為特徵值 λ, 第二個傳回值為特徵向量 : 

l, v=linalg.eig(A)  

例如 :

>>> A=np.array([[1, 2],[3, 2]])     # 二階方陣
>>> A   
array([[1, 2],
       [3, 2]])
>>> l, v=linalg.eig(A)     # 求特徵值與特徵向量
>>> l                                # 有兩個特徵值 : -1 與 4
array([-1.+0.j,  4.+0.j])
>>> v                               # 有兩個特徵向量 : 前者對應特徵值 -1, 後者對應 4
array([[-0.70710678, -0.5547002 ],
       [ 0.70710678, -0.83205029]])

此例有兩組特徵向量, 分別對應兩個特徵值.

特徵方程式可能會有複數根, 例如 : 

>>> A=np.array([[3, 2, 3], [2, 5, -7], [1, 2, -2]])    
>>> A   
array([[ 3,  2,  3],   
       [ 2,  5, -7],
       [ 1,  2, -2]])
>>> l, v=linalg.eig(A)    
>>> l                            # 有三個特徵值
array([4.90057187+0.j        , 0.54971406+0.55676557j,
       0.54971406-0.55676557j])
>>> v                           # 有三組特徵向量 
array([[-0.86169137+0.j        , -0.74295072+0.j        ,
        -0.74295072-0.j        ],
       [-0.44017844+0.j        ,  0.63299867-0.06720996j,
         0.63299867+0.06720996j],
       [-0.25244984+0.j        ,  0.18481478-0.09307649j,
         0.18481478+0.09307649j]])

此例有三個特徵向量, 分別對應三個特徵值. 


5. 用 qr() 函數對方陣進行 QR 分解 :

一個實數方陣 A 的 QR 分解是指它可拆解為一個正交矩陣 Q 與一個上三角矩陣 R 的內積, 所謂正交矩陣意指轉置矩陣等於反矩陣, 亦即正交矩陣與其轉置矩陣之內積為單元矩陣 : 


參考 :


linalg 子套件的 qr(A) 函數會傳回方陣 A 的正交矩陣 Q 與一個上三角矩陣 R 組成之 tuple, 使得 A=QR, 例如 : 

>>> A=np.array([[1, 2],[3, 2]])      # 二階方陣
>>> A   
array([[1, 2],
       [3, 2]])
>>> Q, R=linalg.qr(A)         # 對方陣 A 進行 QR 分解
>>> Q                                    # 正交矩陣 Q
array([[-0.31622777, -0.9486833 ],
       [-0.9486833 ,  0.31622777]])
>>> R                                    # 上三角矩陣 R
array([[-3.16227766, -2.52982213],
       [ 0.        , -1.26491106]])
>>> np.dot(Q, Q.T)              # 正交矩陣與其轉置矩陣之內積必為單元矩陣 I
array([[ 1.0000000e+00, -5.8339229e-18],
       [-5.8339229e-18,  1.0000000e+00]])
>>> B=linalg.inv(Q)             # 求正交矩陣 Q 的反矩陣 B
>>> B                                     # 正交矩陣的反矩陣即為本身之轉置
array([[-0.31622777, -0.9486833 ],
       [-0.9486833 ,  0.31622777]])
>>> np.allclose(A, np.dot(Q, R))      # 檢查 A 與 QR 是否相等
True  

由上面驗算可知, 經 QR 分解後得到的正交矩陣與其轉置矩陣之內積為單元矩陣 I. 此外也使用了 Numpy 的 allclose() 函數檢驗 A 確實等於 QR. 函數 np.allclose() 常用來檢驗兩個陣列 A, B 的每個對應元素是否都相等 (element-wise equal), 若全部相等傳回 True, 否則傳回 False :

np.allclose(A, B) 

其預設判別條件如下 : 


其中 a, b 分別是矩陣 A, B 中的相對應元素, atol 為絕對容忍參數, rtol 為相對容忍參數 (相對於 b 的絕對值). atol 預設值為 1e-8 (即 0.00000001), rtol 預設值為 1e-5 (即 0.00001). 當對應元素 a, b 差之絕對值小於等於右方條件值時傳回 True, 否則傳回 False, 參考 : 


例如 :

>>> np.allclose([1e10,1e-7], [1.00001e10,1e-8])    
False
>>> np.allclose([1e10,1e-8], [1.00001e10,1e-9])   
True

下面是三階方陣的範例 : 

>>> A=np.array([[3, 2, 3], [2, 5, -7], [1, 2, -2]])   # 三階方陣
>>> A   
array([[ 3,  2,  3],
       [ 2,  5, -7],
       [ 1,  2, -2]])
>>> Q, R=linalg.qr(A)        # 對方陣 A 進行 QR 分解
>>> Q                                    # 正交矩陣 Q
array([[-0.80178373,  0.59152048, -0.08512565],
       [-0.53452248, -0.77352678, -0.34050261],
       [-0.26726124, -0.22750788,  0.93638218]])
>>> R                                    # 上三角矩陣 R
array([[-3.74165739, -4.81070235,  1.87082869],
       [ 0.        , -3.13960871,  7.64426469],
       [ 0.        ,  0.        ,  0.25537696]])
>>> np.dot(Q, Q.T)              # 正交矩陣與其轉置矩陣之內積必為單元矩陣 I
array([[ 1.00000000e+00, -4.66827300e-17,  8.68933848e-17],
       [-4.66827300e-17,  1.00000000e+00,  7.00177831e-17],
       [ 8.68933848e-17,  7.00177831e-17,  1.00000000e+00]])
>>> B=linalg.inv(Q)             # 求正交矩陣 Q 的反矩陣 B
>>> B                                     # 正交矩陣的反矩陣即為本身之轉置
array([[-0.80178373, -0.53452248, -0.26726124],
       [ 0.59152048, -0.77352678, -0.22750788],
       [-0.08512565, -0.34050261,  0.93638218]])
>>> np.allclose(A, np.dot(Q, R))      # 檢查 A 與 QR 是否相等
True  

從三階方陣更能清楚看出正交矩陣的特性. 

參考 :

# 基礎線性代數 (黃學亮, 五南, 2013)

2021年10月20日 星期三

Python 學習筆記 : SciPy 的子套件

一直想透過學習 Scipy 把自己虛弱的數學底子好好地補一補, 但時間總是非常有限. 今天得空先將 SciPy 的常用子套件整理成一張表, 常回來看這張表就能提醒自己還有哪些是還沒學的. 

SciPy 系列之前的筆記參考 :


SciPy 是以 Numpy 為基礎建構的開放原始碼科學運算套件, 它依賴底層以 Fortran 實作的演算法可以高效地操作 Numpy 陣列以進行各種數學運算, 其功能足以與商用的 Matlab 或開放原始碼的 Scilab, GSL, 以及 Octave 等軟體相匹敵. 

Scipy 的功能分門別類放在下列子套件中 : 


 SciPy 常用子套件 功能與用途
 scipy.cluster 分群 (clustering) 運算
 scipy.constants 數學與物理常數
 scipy.fftpack 離散傅立葉 (Fourier) 轉換
 scipy.integrate 數值積分與求解常微分方程式
 scipy.interpolate 內插運算 (一維 & 多維內插, 徑向基函數內插, 平滑樣條 spline 等)
 scipy.io 輸入與輸出
 scipy.linalg 線性代數 (求解線性方程組, 特徵值, 特徵向量, 矩陣分解等)
 scipy.signal 訊號處理 (FIR 與 IIR 數位濾波器)
 scipy.stats 統計函數 (連續 & 離散隨機變數的各種機率分布)
 scipy.stats.mstats 遮罩陣列統計函數
 scipy.sparse 稀疏矩陣
 scipy.sparse.linalg 稀疏線性代數
 scipy.sparse.csgraph 壓縮的稀疏圖像函數
 scipy.special 特殊函數
 scipy.spatial 空間演算法與資料結構
 scipy.optimize 最佳化與求根 (求非線性方程組解, 資料擬合, 函數最小值等)
 scipy.odr 正交距離回歸
 scipy.ndimage 多維影像處理 (影像濾波器, 傅立葉轉換, 影像內插, 旋轉等)
 scipy.misc 雜項函數 (求導數, 載入心電圖等)


參考 SciPy 官網教學文件 : 


由於 SciPy 功能收納於各個子套件, 因此通常會用 from scipy 方式匯入會使用到的子套件 (亦可加上 as 取一個簡名), 例如 : 

from scipy import linalg
from scipy import linalg as la

然後用子套件名稱或簡名呼叫旗下函數來運算, 例如求解線性聯立方程組 :

a_ans=linalg.solve(a_coeff, a_const)     
a_ans=la.solve(a_coeff, a_const)           

也可以像 Numpy 那樣直接匯入 SciPy 並取個慣用簡名 sp :

import scipy as sp    

但這樣存取函數的路徑就較長了, 例如 : 

a_ans=sp.linalg.solve(a_coeff, a_const)    

另外一種方式是用 from 從子套件直接匯入會用到的函數, 例如 : 

from scipy.linalg import solve

然後直接呼叫該函數做運算 :

a_ans=solve(a_coeff, a_const)

但這種方式要注意命名空間汙染問題 (勿自訂同名函數). 

如果要匯入整個子套件且想要直接呼叫旗下函數, 可在 import 後面使用 *, 例如 : 

from scipy.linalg import *  

科學與工程科系的學生若能好好地掌握 Numpy 與 SciPy 的用法, 在學習研究上必然如虎添翼. 下面是幾本跟 SciPy 相關的好書 :

Python資料運算與分析實戰 (中久喜健司, 旗標, 2018)

2021年10月19日 星期二

線上語料庫分析服務 Sketch Engine

語料庫是計算語言學的基礎, 除了可在本機使用 NLTK 或 SpaCy 等 Python 套件做語料分析外 (這需要基本的 Python 程式基礎), 現在也可以線上用瀏覽器來做語料庫文本分析, 例如 Sketch Engine, 其操作介面簡單, 完全不需要寫程式 : 

按官網首頁左上角的 "Sign up" 鈕註冊 30 天免費試用帳號 : 




按 "Free 30 Days Trial" 鈕 (右邊的 Individual user account 雖然有教育用途永久免費優惠, 但那只限歐盟區域內的教育機構) : 




填寫帳戶名稱與 e-mail 等資訊, 最底下三項需勾選 : 





按 "Sign Up" 鈕即完成註冊, 然後去收信, 裡面有登入密碼, 要記下來, 因為好像沒有提供更改的功能, 我的 30 天使用密碼是 xxtxXDPB3CWE : 




Sketch Engine 的收費方案如下 :




然後到官網首頁按 Login 鈕登入, 成功後會顯示 Dashboard (儀錶板) : 




可見 Sketch Engine 就是把類似 NLTK 的功能放在雲端, 讓使用者能透過瀏覽器很方便地進行基於語料庫的線上文本分析. 

按右上角的帳戶頭像, 點選 "My Account" 會顯示帳戶資訊 :





此處 Corpus storage used 是指已用掉的語料庫大小比率, Sketch Engine 可讓使用者上傳最多 100 萬字的語料庫.  

網站使用方法參考官網簡介影片 : 





另外 bilibili 也有一個中國人製作的 Sketch Engine 中文使用教學 : 


首先按左邊框第二個按鈕, 這樣右邊就會顯示語言選項, 可以點選語料庫之語言例如 English :




這樣右邊的 "Recently Used Corpus" 欄就會列出所選擇之語料庫 (此處為 EnTenTen 2020) : 




按 "Corpus Info" 鈕會顯示此語料庫的統計資訊, 例如總字數等 :




接下來示範如何做最常用的共詞句分析 (concordance, 即以指定詞為中心排列的例句). 先按左邊框上面第一個按鈕回到 Dashboard, 然後按 "Concordance" 鈕, 然後輸入欲搜尋的字詞, 按 Search 鈕即可 :





可見結果會以所搜尋的 make 為中心列出例句, 這就是共詞句 (concordance). 

除了內建的語料庫外, Sketch Engine 也支援上傳自建的語料庫. 以下示範如何將 NLTK 內建的台灣中央研究院中文語料庫 sinica_treebank 上傳至 Sketch Engine. 

首先要從 NLTK 中把 sinica_treebank 語料庫複製出來, NLTK 的語料庫位置可透過 nltk.data.path 指令查詢, 它會傳回一組可能路徑之串列, 例如 : 

>>> import nltk    
>>> nltk.data.path   
['C:\\Users\\User/nltk_data', 'C:\\Python37\\nltk_data', 'C:\\Python37\\share\\nltk_data', 'C:\\Python37\\lib\\nltk_data', 'C:\\Users\\User\\AppData\\Roaming\\nltk_data', 'C:\\nltk_data', 'D:\\nltk_data', 'E:\\nltk_data']

nltk_data 資料夾的實際位置要一個個去檢視 (因 Python 安裝方式與 Windows 版本而異), 例如我的電腦是在 User 的 AppData 下, 這是預設隱藏路徑, 必須於檢視中開啟顯示隱藏檔案才找得到 : 




找到 nltk_data 資料夾後, 將 parsed 這個語料庫檔案 (約 2.4MB) 複製到文件資料夾下並幫它加上副檔名 .txt, 因為 Sketch Engine 不接受無副檔名的語料庫上傳. 然後到 Sketch Engine 的 Dashboard 頁面按右上角的 "New Corpus" 鈕 : 




填寫名稱, 類型, 語言等欄位後按底下的 "Next" 鍵 : 




按右邊的 "I have my own texts" 鈕後點選複製的 sinica_treebank 語料庫檔 parsed.txt 上傳 : 






上傳完畢後顯示 -397777 表示已占用接近 40% 儲存空間, 按 "Next" 鈕 : 




按 "Compile" 鈕進行編譯 (需要一點時間, 看檔案大小) : 




編譯完按 "Corpus Dashboard" 鈕就可看到右邊多出了一個 sinica_treebank 語料庫了 :




按 "Corpus Info" 顯示此語料庫的統計資訊, 例如有 92 萬多個 token 等訊息 :  




按 "Manage Corpus" 鈕會出現語料庫的管理頁面, 因為是自建的, 故管理功能全部都打開了 : 




回 Dashboard 按 Concordance 來查詢共詞句, 輸入 "我" 按 Search 鈕 : 





因為上傳的是剖析過的語料庫, 所以查詢結果中含有許多語法標注 (例如 POS 等), 如果用 NLTK 查詢會列出原始文本 :

>>> import nltk   
>>> from nltk.corpus import sinica_treebank     
>>> sinica_text=nltk.Text(sinica_treebank.words())     
>>> sinica_text.concordance('我')      

Displaying 25 of 724 matches:
一 友情 嘉珍 和 我 住在 同一條 巷子 我們 是 鄰居 也 是 同班 同學 我們 常常 一起 上
居 也 是 同班 同學 我們 常常 一起 上學 一起 回家 有一天 上學 時 我 到 她 家 等候 按 了 門鈴 卻 沒有 任何 動靜 正當 我 想 離開 時
天 上學 時 我 到 她 家 等候 按 了 門鈴 卻 沒有 任何 動靜 正當 我 想 離開 時 門 內 突然 傳來 急促 的 腳步聲 嘉珍 打開 了 門 大聲
 突然 傳來 急促 的 腳步聲 嘉珍 打開 了 門 大聲 的 叫 著 快 點 我 媽媽 暈倒 了 嘉珍 抓起 我 的 手 急忙 往 屋 裡 跑 進入 房間 看
嘉珍 打開 了 門 大聲 的 叫 著 快 點 我 媽媽 暈倒 了 嘉珍 抓起 我 的 手 急忙 往 屋 裡 跑 進入 房間 看到 她 的 媽媽 趴 在 地 上
 她 的 媽媽 趴 在 地 上 臉色 蒼白 得 像 紙 一樣 這種 情景 把 我 嚇壞 了 怎麼辦 嘉珍 不停 的 哭泣 聲音 有些 顫抖 我 的 腦海 中 
這種 情景 把 我 嚇壞 了 怎麼辦 嘉珍 不停 的 哭泣 聲音 有些 顫抖 我 的 腦海 中 頓時 一片 空白 不 曉得 怎麼辦 才 好 過 了 一會兒 我
我 的 腦海 中 頓時 一片 空白 不 曉得 怎麼辦 才 好 過 了 一會兒 我 才 問 她 你 爸爸 呢 他 出差 了 嘉珍 擦 著 眼淚 我 握住 她 的
 了 一會兒 我 才 問 她 你 爸爸 呢 他 出差 了 嘉珍 擦 著 眼淚 我 握住 她 的 雙手 她 的 手 又 冰 又 冷 這時 有個 念頭 突然 閃過
握住 她 的 雙手 她 的 手 又 冰 又 冷 這時 有個 念頭 突然 閃過 我 的 眼前 我 幫 她 撥 了 一一九 請 救護車 來 而且 拍拍 她 的 背
雙手 她 的 手 又 冰 又 冷 這時 有個 念頭 突然 閃過 我 的 眼前 我 幫 她 撥 了 一一九 請 救護車 來 而且 拍拍 她 的 背 安慰 她 不
撥 了 一一九 請 救護車 來 而且 拍拍 她 的 背 安慰 她 不要 著急 我 會 陪 你 的 不久 救護車 停 在 她 家 門口 醫護 人員 很 快 的 
 醫護 人員 很 快 的 將 她 的 媽媽 抬上 救護車 嘉珍 上車 前 對 我 說 謝謝 你 的 幫忙 我 握 著 她 的 手 說 不用 謝 我 因為 我們
 她 的 媽媽 抬上 救護車 嘉珍 上車 前 對 我 說 謝謝 你 的 幫忙 我 握 著 她 的 手 說 不用 謝 我 因為 我們 是 好朋友 二 無私 的 
車 前 對 我 說 謝謝 你 的 幫忙 我 握 著 她 的 手 說 不用 謝 我 因為 我們 是 好朋友 二 無私 的 愛 愛 像 火紅 的 太陽 散發 光 
正 為了 缺乏 旅費 而 發愁 的 時候 員外 來 找 他 並且 對 他 說 我 看 你 悶悶不樂 是不是 進京 趕考 的 旅費 不 夠 這裡 有 一些 銀子
物 和 銀子 向 員外 道謝 時 員外 握 著 他 的 手 說 你 不用 還 我 了 我 只是 盡 一份 心力 而已 以後 有 機會 希望 你 也 能夠 幫助
銀子 向 員外 道謝 時 員外 握 著 他 的 手 說 你 不用 還 我 了 我 只是 盡 一份 心力 而已 以後 有 機會 希望 你 也 能夠 幫助 別人 
身 趕考 吧 書生 用 顫抖 的 手 接過 銀子 含 著 眼淚 說 謝謝 您 我 不 知道 要 怎麼 來 報答 您 江 巡撫 說 你 不必 謝 我 也 不必 
 謝謝 您 我 不 知道 要 怎麼 來 報答 您 江 巡撫 說 你 不必 謝 我 也 不必 回報 我 四 快樂 的 閱讀課 上 國語課 的 時候 老師 叫 我
知道 要 怎麼 來 報答 您 江 巡撫 說 你 不必 謝 我 也 不必 回報 我 四 快樂 的 閱讀課 上 國語課 的 時候 老師 叫 我們 這一組 做 讀書
 閱讀課 上 國語課 的 時候 老師 叫 我們 這一組 做 讀書 心得 報告 我 第一個 站起來 發言 我們 閱讀 的 好 書 是 小 王子 這 是 法國人 
 小 王子 告訴 他 在 星球 旅行 的 經過 兩 人 成為 知心 的 朋友 我 說完 之後 組長 站起來 補充 說 小 王子 是 一個 可愛 的 人 他 在
星 上 每天 照顧 會 說話 的 玫瑰花 、 清理 火山灰 而且 欣賞 落日 我 喜歡 這種 自由自在 的 生活 明珠 說 這本 書 最 精采 的 部分 遇到
 的 商人 等 小 王子 不 喜歡 自大 的 人 也 不 喜歡 虛偽 的 人 我 覺得 小 王子 很 正直 最後 阿平 說 這本 書 中 有 許多 值得 思考
>>> 

我將 sinica_treebank 語料庫放在 GitHub 方便下載進行測試 :


也可以上傳 sinica_treebank 的原始語料庫, 即 nltk_data 資料夾下的 raw 檔, 將其複製到文件夾後加上 .txt 副檔名, 然後新增一個語料庫取名為 sinica_treebank2, 再將 raw.txt上傳 : 





可見未剖析的原始檔所佔儲存空間少很多, 只有 10MB 左右. 同樣搜尋 "我" 做共詞句分析 (concordance) 結果就不會出現剖析用後的語法標記了 :




參考 : 


2021年10月18日 星期一

2021 年第 42 周記事

時序已來到 10 月下旬, 再過 10 周即將告別 2021. 本周氣候不穩定, 週六回鄉下時高雄仍炎熱無雲, 但一下高速公路就迎來暴雨, 回家前將花圃約 30 公分高的自生木瓜苗挖起來帶回鄉下, 原本打算回到家立刻種在菜園, 但雨勢這麼大只好等到晚上雨停後戴上頭燈挑夜種下去. 週日去市場時又在種子行買了一株 60 元的台農二號 (非嫁接), 同樣種在菜園南側棚架旁. 下周打算清理香蕉樹附近的甘蔗拿去榨汁, 騰出來的空地要堆高來種木瓜, 菜園留兩畦少量種青菜, 其餘種木瓜. 

本周學習進度回頭整理 Pandas, 這真的太重要了, 不把它弄清楚弄熟, 機器學習那邊就是不太順. NLP 也停好久了, 雖然很想推進 SpaCy 的測試學習, 但時間就是那麼有限, 還要 relax 一下追韓劇, 一直高速運行也不好. 只要 Pandas 弄熟了, 不僅機器學習會很順, 做爬蟲專案也很好用. 要學的東西實在太多, 定下心來想想甚麼才是最基礎最重要的, 打好基礎再行動才不會左支右絀. 

最近在追的韓劇 "海岸村恰恰恰" 昨天 Netflix 終於播完了, 第 16 集當然是 Happy Ending, 第 15 集才是重點, 因為洪班長消失 5 年的謎團在這集解開 (不是國情院特務), 原來是因為一連串的意外. 雖然是愛情喜劇, 但看這集卻有點催淚. 坎離小姐在睡夢中帶著幸福感離世, 三個老人在睡覺前聊到小時候郊遊前一天的心情, 哈哈, 真的, 長大後都忘記這樣的心情是甚麼了. 中午吃飯時間在 Line TV 看的則是 "請回答 1988", 那個成德善好像我們家菁菁. 看這部才想起 1988 年 9 月 17 開幕的漢城奧運, 那時我剛從部隊退伍, 天啊! 這居然已是 33 年前的事了. 

2021年10月17日 星期日

料理實驗 : 金針炒大白菜

一顆大白菜放在冰箱已兩周, 除了中午炒冬粉用掉一半, 另外一半就晚餐煮掉吧! 原想簡單用蒜頭清煮大白菜, 但想說有沒有其他料理方式, 就上網找到下面這個食譜 :

 
感覺這好像是喜宴常見配菜, 於是把原本要來煎蛋的金針菇拿來煮這道菜了. 

材料 : 
  • 大白菜 半顆
  • 金針菇 1 包
  • 胡蘿蔔 1/3 顆
  • 蒜頭 兩三片
  • 鹽 適量
作法 : 
  1. 大白菜葉片取下洗淨切段, 金針菇頭切掉泡水洗淨, 胡蘿蔔 1/3 切片, 蒜片切碎備用.
  2. 起油鍋下蒜片拌炒一下後大白菜, 胡蘿蔔, 金針菇下鍋炒至大白菜軟化即可. 亦可於起鍋前淋少許太白粉水勾芡. 



加了胡蘿蔔整體顏色看起來就讓人蠻有食慾的, 還有人稱窮人人參的美味金針菇, 嗯, 比以前清煮蒜頭好吃多了! 以後就這麼煮大白菜唄! 

好書 : Intro to Python-for Computer Science and Data Science

今天發現下面這本從基礎到 AI 進階的自學 Python 好書 : 



Source : 天瓏


此書共 18 章, 前 10 章是 Python 基礎篇, 後面 8 章則是進階篇, 包含演算法 (搜尋, 排序), 資料科學 (Numpy, Pandas), 以及機器學習 (scikit-learn, TensorFlow) 等等, 最重要的是每的章節都有 self-check 與習題, 非常適合自學之用. 

2021年10月14日 星期四

Python 內建 GUI 模組 tkinter 測試 (十六) : Menu 元件

前陣子為了還書而放下 tkinter 轉而去看機器學習的書, 停頓了一陣子感覺有點生疏了, 剩下一些元件趕快測試完, 這樣才能全心攻略機器學習. 今天就來繼續測試 Menu (功能選單) 元件唄. 
 
本系列之前的文章參考 :   


Menu 元件 (功能選單) 是 GUI 應用程式上視窗標題列下方的選單列 (Menu bar) 選單, 用來整合應用程式所有功能, 讓使用者下拉點選要執行的選項功能, 例如通常在檔案選單底下會有開新檔案, 開啟舊檔, 另存檔案等功能選項. 




建立 Menu 元件之語法如下 (注意, 此元件僅在 tk 中提供, ttk 中無此元件) : 

menu=Menu(父容器, **參數列)

注意, 此處父容器若為 Tk 視窗物件, 則此 Menu 物件為最上層選單列 (Menu bar); 父容器若為其他 Menu 物件, 則此 Menu 物件為子選單. 選單列的 Menu 物件是頂層選單, 建立後還必須呼叫 Tk 視窗物件的 config() 方法將 menu 參數設為選單列物件, 這樣才會在視窗標題底下才會出現此選單列, 例如 :

win=tk.Tk()  
menubar=tk.Menu(win)                  
win.config(menu=menubar)

注意, 只有頂層的選單列 Menu 物件才需要與視窗物件掛勾. 

參考 :


Menu 元件常用參數如下表 : 


 Menu 元件常用參數 說明
 image 功能選單圖示 (PhotoImage 圖片物件名稱)
 tearoff 功能選單上方分隔線=True (預設有, 可分離)/False (無, 不可分離)
 bd 功能選單邊框寬度 (預設 1px)
 font 字型與尺寸 (px), 粗體 (bold) 或斜體 (italic), 底線或刪除線等


Menu 元件常用方法如下表 : 


 Menu 元件常用方法 說明
 add_command(**options) 加入選項, 參數 label=選項文字, command=呼叫的函式
 add_cascade(**options) 串接父子選單, 參數 label=子選單標籤, menu=子選單物件
 add_separator() 加入分隔線


其中 add_command() 可傳入如下參數 :
  • label : 文字選項
  • command : 所呼叫之函式名稱 (後面不要加括弧)
  • image : 圖片選項
  • bitmap : 小圖示選項)  
最常用的是 label 與 command, 前者顯示選項文字, 後者指定點選此選項時要執行之函式. 而 add_cascade() 則是用來建立一個子選單, 其常用參數如下 :
  • label : 文字選項
  • menu : 父選單名稱
  • image : 圖片選項
其中最重要的是用來指向父選單的 menu 參數. 

下面範例是最簡單的選單做法, 直接把功能選項放在頂層選單列中 : 


測試 1 : 直接在選單列 (Menu bar) 上新增選項 (1) [看原始碼]

import tkinter as tk
from tkinter import messagebox as msgbox

win=tk.Tk()                                                      
win.title("tkinter GUI 測試")
win.geometry("300x200")

def say_hello():
    msgbox.showinfo("Info", "Hello World!")

menubar=tk.Menu(win)       # 建立頂層功能列
menubar.add_command(label="Hello", command=say_hello)     # 新增選項
menubar.add_command(label="Quit", command=win.destroy)   # 新增選項
win.config(menu=menubar)
win.mainloop()

此例在呼叫 tk.Menu() 時傳入 Tk 物件當父容器以建立一個 Menu 物件, 然後呼叫視窗物件 win 的 config() 方法將其 menu 參數指定為此 Menu 物件, 這樣這個 Menu 物件就成為視窗的選單列了. 呼叫此 Menu 物件的 add_command() 方法會將選項依序以水平排列方式加入選單列中, 結果如下 : 




可見若選項不多這樣做最簡單, 但若有很多功能選項, 則功能列很快就會被填滿. 當排列到視窗右邊界時就會折回選單列最左邊並新增一列來放置新的選項, 例如 : 


測試 2 : 直接在選單列 (Menu bar) 上新增選項 (2) [看原始碼]

import tkinter as tk
from tkinter import messagebox as msgbox

win=tk.Tk()                                                      
win.title("tkinter GUI 測試")
win.geometry("300x200")

def say_hello():
    msgbox.showinfo("Info", "Hello World!")

menubar=tk.Menu(win)
menubar.add_command(label="Hello-1", command=say_hello)
menubar.add_command(label="Hello-2", command=say_hello)
menubar.add_command(label="Hello-3", command=say_hello)
menubar.add_command(label="Hello-4", command=say_hello)
menubar.add_command(label="Hello-5", command=say_hello)
menubar.add_command(label="Quit", command=win.destroy)
win.config(menu=menubar)
win.mainloop()

此例在功能列上添加了 5 個 Hello 功能選項, 超出邊界時會折返左邊新增一列來放置, 但將視窗拉寬到能放得下全部選項時又會自動變成一列, 結果如下 :





一般應用程式做法會將選項根據功能放入子選單中收納, 這樣的階層式選單結構可讓使用者快速找到選項. 呼叫父選單物件的 add_cascade() 方法時將子選單物件傳給 menu 參數就可以將子選單與父選單串接在一起了 :

父選單.add_cascade(label="子選單標籤", menu=子選單物件)  

例如可以用子選單概念改寫上面範例, 將其中原本直接放在選單列上的 5 個 Hello 選項收納到 hello_menu 子選單中, 例如 :


測試 3 : 使用子選單收納功能項 [看原始碼]

import tkinter as tk
from tkinter import messagebox as msgbox

win=tk.Tk()                                                      
win.title("tkinter GUI 測試")
win.geometry("300x200")

def say_hello():
    msgbox.showinfo("Info", "Hello World!")

menubar=tk.Menu(win)                  # 建立頂層父選單 (選單列)
hello_menu=tk.Menu(menubar)     # 在選單列下建立一個子選單
hello_menu.add_command(label="Hello-1", command=say_hello)    # 子選單新增選項
hello_menu.add_command(label="Hello-2", command=say_hello)
hello_menu.add_command(label="Hello-3", command=say_hello)
hello_menu.add_command(label="Hello-4", command=say_hello)
hello_menu.add_command(label="Hello-5", command=say_hello)
menubar.add_cascade(label="Hello", menu=hello_menu)          # 將子選單串接到父選單
menubar.add_command(label="Quit", command=win.destroy)    # 選單列新增選項
win.config(menu=menubar)    # 設定視窗的選單列
win.mainloop()

此例建立一個含有 5 個選項的子選單物件 file_menu, 然後呼叫父選單 (menubar) 的 add_cascade() 方法時將子選單物件傳給 menu 參數即可串接父子選單, 結果如下 : 




接層式選單結構可以有很多層, 只要由上而下依序呼叫父選單的 add_cascade() 就可一層一層往下串接, 例如上面的 5 個 Hello 可以分成 a, b 兩群分別放入第二層子選單 :

menubar=tk.Menu(win)
hello_menu=tk.Menu(menubar)
hello_menu_a=tk.Menu(hello_menu)
hello_menu_b=tk.Menu(hello_menu)
hello_menu.add_cascade(label="Hello-a", menu=hello_menu_a)
hello_menu.add_cascade(label="Hello-b", menu=hello_menu_b)
menubar.add_cascade(label="Hello", menu=hello_menu)

完整程式碼如下面範例所示 : 


測試 4 : 串接多層子選單 [看原始碼]

import tkinter as tk
from tkinter import messagebox as msgbox

win=tk.Tk()                                                      
win.title("tkinter GUI 測試")
win.geometry("300x200")

def say_hello():
    msgbox.showinfo("Info", "Hello World!")

menubar=tk.Menu(win)                             # 建立頂層父選單 (選單列)
hello_menu=tk.Menu(menubar)                # 在選單列下建立一個子選單
hello_menu_a=tk.Menu(hello_menu)       # 在子選單 hello_menu 下建立一個孫選單 a
hello_menu_a.add_command(label="Hello-1", command=say_hello)   # 孫選單 a 新增選項
hello_menu_a.add_command(label="Hello-2", command=say_hello)
hello_menu_a.add_command(label="Hello-3", command=say_hello)
hello_menu.add_cascade(label="Hello-a", menu=hello_menu_a)    # 串接子選單與孫選單 a
hello_menu_b=tk.Menu(hello_menu)       # 在子選單 hello_menu 下建立一個孫選單 b
hello_menu_b.add_command(label="Hello-4", command=say_hello)    # 孫選單 b 新增選項
hello_menu_b.add_command(label="Hello-5", command=say_hello)
hello_menu.add_cascade(label="Hello-b", menu=hello_menu_b)    # 串接子選單與孫選單 b
menubar.add_cascade(label="Hello", menu=hello_menu)    # 串接子選單與選單列
menubar.add_command(label="Quit", command=win.destroy)
win.config(menu=menubar)   
win.mainloop()

此例在子選單 hello_menu 下分別建立 hello_menu_a 與 hello_menu_b 兩個孫選單, 將上例中的 5 個 Hello 選項中的 Hello-1 到 Hello-3 放在孫選單 a 內, 將 Hello-4 與 Hello-5 放在孫選單 b 內, 再呼叫 hello_menu 的 add_cascade() 方法先後將孫選單 a 與 b 串接到子選單, 最後呼叫 menubar 的 add_cascade() 將子選單 hello_menu 串接到選單列, 結果如下 :





依此方式分層呼叫父選單的 add_cascade() 方法串接子選單即可建立多層選單. 

上面的所有範例中的選單最上方都有一條虛線, 如果點擊此虛線會將該選單從選單列或其父選單中拆出來, 變成獨立的浮動選單 :





這是因為在呼叫 tk.Menu() 建立選單時 tearoff 參數的預設值為 True (1) 的關係. 如果要禁止選單被扯下來, 可以在建立 Menu 物件時將 tearoff 參數數為 0 或 False (選單列的 Menu 不用設), 例如上面範例的不可分離版如下 : 


測試 5 : 將 tearoff 參數設為 False 使選單不可分離 [看原始碼]

import tkinter as tk
from tkinter import messagebox as msgbox

win=tk.Tk()                                                      
win.title("tkinter GUI 測試")
win.geometry("300x200")

def say_hello():
    msgbox.showinfo("Info", "Hello World!")

menubar=tk.Menu(win)      # 選單列不用設 tearoff
hello_menu=tk.Menu(menubar, tearoff=0)                    # 選單不可分離
hello_menu_a=tk.Menu(hello_menu, tearoff=False)     # 選單不可分離
hello_menu_a.add_command(label="Hello-1", command=say_hello)
hello_menu_a.add_command(label="Hello-2", command=say_hello)
hello_menu_a.add_command(label="Hello-3", command=say_hello)
hello_menu.add_cascade(label="Hello-a", menu=hello_menu_a)
hello_menu_b=tk.Menu(hello_menu, tearoff=0)           # 選單不可分離
hello_menu_b.add_command(label="Hello-4", command=say_hello)
hello_menu_b.add_command(label="Hello-5", command=say_hello)
hello_menu.add_cascade(label="Hello-b", menu=hello_menu_b)
menubar.add_cascade(label="Hello", menu=hello_menu)
menubar.add_command(label="Quit", command=win.destroy)
win.config(menu=menubar)
win.mainloop()

此例除選單列外, 其餘的 Menu 元件以參數 tearoff=0 設定為不可分離, 結果如下 : 





與上例結果相比, 可知選單最上面那條虛線不見了, 因此無法選單分離了. 

選單中選項可以加入分隔線做為區隔, 例如上面範例 3 的 hello_menu 選單有五個 Hello 選項, 可以呼叫選單物件的 add_separator() 方法來加入分隔線, 例如 :


測試 6 : 呼叫 add_separator() 在選單中加入分隔線 [看原始碼]

import tkinter as tk
from tkinter import messagebox as msgbox

win=tk.Tk()                                                      
win.title("tkinter GUI 測試")
win.geometry("300x200")

def say_hello():
    msgbox.showinfo("Info", "Hello World!")

menubar=tk.Menu(win)
hello_menu=tk.Menu(menubar, tearoff=False)
hello_menu.add_command(label="Hello-1", command=say_hello)
hello_menu.add_command(label="Hello-2", command=say_hello)
hello_menu.add_separator()     # 加入分隔線
hello_menu.add_command(label="Hello-3", command=say_hello)
hello_menu.add_command(label="Hello-4", command=say_hello)
hello_menu.add_separator()     # 加入分隔線
hello_menu.add_command(label="Hello-5", command=say_hello)
menubar.add_cascade(label="Hello", menu=hello_menu)
menubar.add_command(label="Quit", command=win.destroy)
win.config(menu=menubar)
win.mainloop()

此例在加入選項過程中利用 add_separator() 插入分隔線, 結果如下 : 




可見這兩條分隔線將選項分成了三群. 

上面為了便於了解 Menu 元件用法使用的都是玩具範例, 下面要結合之前已學過的 MeesageBox,  Text 與 Scrollbar 等元件, 以及 Python 的檔案存取函式來寫一個簡單的文字編輯器, 首先依照上面的範例來建立編輯器的基本架構, 然後再實作細部功能 :


測試 7 : 簡單的 Tkinter 文字編輯器 (1) [看原始碼]

import tkinter as tk
from tkinter import messagebox as msgbox

win=tk.Tk()                                                      
win.title("tkinter GUI 測試")
win.geometry("300x200")

def new_file():
    msgbox.showinfo("Info", "開新檔案")
    
def old_file():
    msgbox.showinfo("Info", "開啟舊檔")    

def save_file():
    msgbox.showinfo("Info", "儲存檔案")

def about():
    msgbox.showinfo("Info", "這是 tkinter 測試")

def version():
    msgbox.showinfo("Info", "v 1.0.0")

menubar=tk.Menu(win)
file_menu=tk.Menu(menubar, tearoff=False)
file_menu.add_command(label="開新檔案", command=new_file)
file_menu.add_command(label="開啟舊檔", command=old_file)
file_menu.add_command(label="儲存檔案", command=save_file)
file_menu.add_separator()
file_menu.add_command(label="關閉", command=win.destroy)
menubar.add_cascade(label="檔案", menu=file_menu)
help_menu=tk.Menu(menubar, tearoff=False)
help_menu.add_command(label="版本", command=version)
help_menu.add_command(label="關於", command=about)
menubar.add_cascade(label="幫助", menu=help_menu)
win.config(menu=menubar)
win.mainloop()

結果如下 :





OK, 架構弄好了就可以進行細部功能的實作了, 下面範例參考之前的文章 :



測試 8 : 簡單的 Tkinter 文字編輯器 (2) [看原始碼]

import tkinter as tk
from tkinter import messagebox as msgbox
from tkinter.filedialog import askopenfilename, asksaveasfile

win=tk.Tk()                                                      
win.title("Simple Text Editor")
win.geometry("500x400")

def new_file():
    yes=msgbox.askyesno("確認", "要儲存目前的內容嗎?")  # 確認是否要儲存舊內容
    if yes:                              # 按是傳回 True
        save_file()                  # 呼叫 save_file() 先儲存編輯器目前的內容
    text.delete(1.0, "end")    # 按否直接清除 Text 元件內容
        
def old_file():
    file=askopenfilename(filetypes=(('text files', 'txt'),))    # 只能開啟 .txt 檔
    with open(file, "r", encoding="utf-8") as f:                  # 必須指定編碼為 utf-8
        text.insert("insert", f.read())                                      # 讀取檔案內容插入 Text

def save_file():
    f=asksaveasfile(mode='w', defaultextension=".txt")    # 開啟檔案儲存視窗
    if f is None:                               # 按取消傳回 None
        return
    f.write(text.get(1.0, "end"))      # 將 Text 全部內容存入檔案
    f.close() 

def quit():
    yes=msgbox.askyesno("確認", "要儲存目前的內容嗎?")
    if yes:                   # 按是傳回 True
        save_file()        # 呼叫 save_file() 先儲存編輯器目前的內容
    win.destroy()        # 關閉視窗

def about():
    msgbox.showinfo("關於此軟體", "Simple Text Editor")

def version():
    msgbox.showinfo("版本", "v 1.0.0")

# add menu 
menubar=tk.Menu(win)
win.config(menu=menubar)
file_menu=tk.Menu(menubar, tearoff=False)
file_menu.add_command(label="開新檔案", command=new_file)
file_menu.add_command(label="開啟舊檔", command=old_file)
file_menu.add_command(label="儲存檔案", command=save_file)
file_menu.add_separator()
file_menu.add_command(label="關閉", command=quit)
menubar.add_cascade(label="檔案", menu=file_menu)
help_menu=tk.Menu(menubar, tearoff=False)
help_menu.add_command(label="版本", command=version)
help_menu.add_command(label="關於", command=about)
menubar.add_cascade(label="幫助", menu=help_menu)
# add editor
scrollbar=tk.Scrollbar(win)
scrollbar.pack(side=tk.RIGHT, fill=tk.Y)    
text=tk.Text(win, height=10, bg="aqua")
text.pack(side=tk.LEFT, fill=tk.Y)
text.config(padx=5, pady=5, font="Helvetic 12")  
text.config(yscrollcommand=scrollbar.set)
scrollbar.config(command=text.yview)          
win.mainloop()

由於是文字編輯器, 所以在呼叫 askopenfilename() 選取檔案時利用 filetypes 參數過濾只能挑選 .txt 檔, 呼叫 asksaveasfile() 開啟檔案儲存視窗時也是用 defaultextension 參數預設存為 .txt 檔. 另外要注意的是用 open() 開啟舊檔時必須用 encoding 參數指定為 utf-8 編碼, 否則會出現 "nicodeDecodeError: 'cp950' codec can't decode" 錯誤訊息, 參考 :


此例因為要處理目前編輯器內容要不要存檔問題, 故在點選 "關閉" 選項時不直接呼叫 win.destroy(), 而是呼叫自訂函式 quit(), 結果如下 :











嗯, 雖然很陽春, 但能 Work. 

接下來測試 Menu 物件的 post() 方法, 它會在指定的是窗位置顯示選單, 可以用來製作浮動選單, 與上面範例不同的是, 這種選單不是固定掛在選單列上, 而是在視窗中按下滑鼠右鍵才顯示在游標所在的位置, 所以不可以呼叫  win.config(menu=menubar), 而是要呼叫視窗物件的 bind() 函式去綁定按下滑鼠右鍵時的動作, 也就是呼叫 Menu 物件的 post() 函式以顯示此選單 : 

def popup(e):
    menubar.post(e.x_root, e.y_root)

win.bind("<Button-3>", popup)

此處 bind() 的第一參數  "<Button-3>" 為按下滑鼠右鍵之事件, 而 popup() 為事件處理函式, 在此函式中需呼叫頂層 Menu 物件的 post() 方法來顯示整個選單. 注意, bind() 函式的第二參數只要填入事件處理函式名稱即可, 不可加小括號, 因為加小括號是此列程式執行時立刻呼叫, 但我們要的是等使用者操作時才呼叫. 例如 :


測試 9 : 呼叫 post() 顯示浮動選單 [看原始碼]

import tkinter as tk
from tkinter import messagebox as msgbox

win=tk.Tk()                                                      
win.title("tkinter GUI 測試")
win.geometry("400x300")

def say_hello():
    msgbox.showinfo("Info", "Hello World!")

def popup(e):
    menubar.post(e.x_root, e.y_root)    # 在按鈕事件發生處顯示選單
    
menubar=tk.Menu(win, tearoff=0)
hello_menu=tk.Menu(menubar, tearoff=0)
hello_menu.add_command(label="Hello-1", command=say_hello)
hello_menu.add_command(label="Hello-2", command=say_hello)
hello_menu.add_command(label="Hello-3", command=say_hello)
hello_menu.add_command(label="Hello-4", command=say_hello)
hello_menu.add_command(label="Hello-5", command=say_hello)
menubar.add_cascade(label="Hello", menu=hello_menu)
menubar.add_command(label="Quit", command=win.destroy)
win.bind("<Button-3>", popup)     # 將按下滑鼠右鍵的事件與 popup 函式綁定
win.mainloop()

此例基本架構與上面測試 3 一樣, 差別在於不將頂層 Menu 物件指定給視窗物件的 menu 參數, 取而代之的是呼叫視窗物件的 bind() 方法, 將按下滑鼠右鍵之事件與 popup() 函式綁定, 在此 popup() 函式中呼叫頂層 Menu 物件的 post() 浮動顯示選單. 注意, 這個自訂函式會被傳入一個事件物件 e, 我們必須將代表滑鼠座標的 e.x_root 與 e.y_root 傳入 post() 以便函式能在滑鼠所在位置顯示此選單. 結果如下 : 




可見在視窗的任何地方按下滑鼠右鍵, 就會在那個座標點顯示選單, 在空白處按滑鼠左鍵選單就會自動消失. 

參考 :