# Python Programming - Lecture 02 運算式 ###### tags: `python-programming` 在上一章,我們學習了關於資料基本概念。有了資料之後,我們就會想要對資料進行運算,以獲得我們所需的資訊。所謂的「運算」,說穿了其實就是加減乘除這種基本的動作。透過組合各式各樣的基本運算,程式得以表現出複雜的行為。在本章當中,我們就要介紹運算的基本知識。 ## 2.1 運算元與運算子 **運算 (Operation)** 這個動作,牽扯到兩個很重要的角色,分別是**運算元 (Operand)** 跟**運算子 (Operator)**。這兩個要素缺一不可,少了一個就沒有辦法表達出完整的運算行為。 ``` a + 3 ↑ ↑ ↑ 運算元 運算子 運算元 ``` > Figure 2.1 以加法為例,運算元和運算子。 運算元指的是參與運算的東西。舉例來說,在加法運算當中,被加數與加數就是參與這個運算的兩個運算元;又比如在取負號的運算當中,被取負號的那一個數字就是這個運算的運算元。運算元可以是單純的資料(正式的說法應該是**字面值 (literal)**,也就是還沒被放進變數裡的資料)或是**變數**。 運算子指的則是這個運算要做什麼樣的動作。比如說,在加法運算當中, `+` 這個符號就是加法運算子;又例如在比較運算當中, `>` 這個符號就是大於運算子。 Figure 2.1 當中展示了以加法為例,哪些東西是運算元,哪些東西是運算子。 在上述的例子中,我們可以看到有些運算子需要兩個運算元,有些運算子卻只需要一個運算元。我們稱只需要一個運算元的運算子叫做**單元運算子 (Unary operator)**,像是取正號、取負號等;需要兩個運算元的則叫做**二元運算子 (Binary operator)**,像是加減乘除這些。以前我們在學除法時,有學過除法沒有交換律。基於類似的概念,有些二元運算子左右兩邊的運算元是不能交換的,如果交換了可能會算出不一樣的結果。 Python 的運算子可以簡單分成三類:**算術運算子 (Arithmetic operator)**、**比較運算子 (Comparison operator)** 和**位元運算子 (Bitwise operator)**。以下我們分別簡介這三類運算子的功能和特性。 ### 2.1.1 算術運算子 算數運算子進行的是數值計算類的運算,其得出的結果會是另外一個數值。 Python 中原生的算數運算子有以下這些: * 加法: `a + b` * 減法: `a - b` * 乘法: `a * b` * 實數除法: `a / b` (結果為浮點數) * 整數除法: `a // b` (只取商數) * 模除法: `a % b` (只取餘數) * 取次方: `a ** b` * 取正號: `+a` * 取負號: `-a` ### 2.1.2 比較運算子 比較運算子進行的是大小比較的運算,其得出的結果會是一個布林值,表示這個大小關係是否成立。 Python 中原生的關係運算子有以下這些: * 小於: `a < b` * 小於等於: `a <= b` * 等於: `a == b` * 不等於: `a != b` * 大於等於: `a >= b` * 大於: `a > b` <!-- [`==` 和 `is` 的差別](https://clay-atlas.com/blog/2020/08/04/python-cn-equal-is-difference/) --> ### 2.1.3 位元運算子 所有資料在電腦當中都是以二進制的形式儲存的,在某些時候,我們會需要直接操作到這些組成資料的位元,以位元為單位進行運算。在這種時候,我們就會需要用到位元運算子。 Python 中原生的位元運算子有以下這些: * 位元且: `a & b` * 位元或: `a | b` * 位元互斥或: `a ^ b` * 位元否: `~a` * 左位移: `a << b` * 右位移: `a >> b` 位元運算子通常牽涉到較底層的資料表示法,因此往後的課程中我們基本上不太用到位元運算子。若對位元運算有興趣的話,可以參考[附錄](https://hackmd.io/@kaeteyaruyo/python-programming-appendix)。 <!-- TODO: ### 2.1.4 賦值運算子 --> 除了上述這些,Python 還有實作了其他的運算子,詳細的列表可以到[官方文件](https://docs.python.org/3/library/operator.html?highlight=operator#mapping-operators-to-functions)查看。 上述的運算子們,雖然乍聽之下似乎都只能作用在數字上,但事實上, Python 多載 (overload) 了這些運算子的功能,使得他們在不同類型的資料上會有不同的行為。這樣的設計讓 Python 寫起來非常簡潔,許多過往在其他程式語言中必須以函式呼叫來完成的動作,現在都能直觀的用運算子就表達完成。以下我們就來介紹上述這些運算子在數值資料和在文字資料上有什麼樣的行為。 ## 2.2 數值的運算 算術運算子和比較運算子用在數值資料的計算上,其結果是相當直觀的。以下我們分別介紹算術運算子和比較運算子在數值運算上的行為。 ### 2.2.1 數值的算術運算 算術運算的結果會是另一個數值。這些運算子的行為大多都符合我們學過的計算規則。 ```python >>> 3 + 5 8 >>> 3 - 5 -2 >>> 3 * 5 15 >>> 3 / 5 0.6 >>> 3 // 5 0 >>> 3 % 5 3 >>> 3 ** 5 243 >>> +3 3 >>> -3 -3 ``` 上述這幾種運算中,就屬除法比較複雜。 Python 設計了三種不同的除法,來因應各式常見的數值運算需求。 * 實數除法的結果必為浮點數,即便是兩個可以整除的整數相除也一樣 ```python >>> 6 / 3 2.0 >>> 22 / 7 3.142857142857143 >>> 12.5 / 0.25 50.0 >>> 1.27 / 3 0.42333333333333334 ``` * 整數除法的結果只取商數。當被除數與除數都是整數時,結果為整數,否則結果為浮點數 ```python >>> 5 // 3 1 >>> 5.0 // 3 1.0 >>> 5 // 0.3 16.0 ``` * 模除法的結果只取餘數。當被除數與除數都是整數時,結果為整數,否則結果為浮點數。在 Python 中,[模除法所得餘數,其正負號會與除數的正負號相同](https://stackoverflow.com/questions/3883004/the-modulo-operation-on-negative-numbers-in-python) ```python >>> 10 % 3 1 >>> 10 % 3.0 1.0 >>> 10.0 % 3 1.0 >>> 10 % -3 -2 >>> -10 % 3 2 ``` 值得注意的是,如果參與計算的兩個數字,一個是整數,一個是浮點數的話, Python 會先進行**隱式轉型 (Implicit type casting)**,也就是偷偷幫你把兩個資料都轉換成相同型態之後,才進行運算。由於浮點數包含整數,所以 Python 在進行隱式轉型時,會統一把整數轉換成浮點數。 ```python >>> 3 + 0.4 3.4 >>> 5e2 - 240 260.0 >>> 2.0 * 9 18.0 >>> 4 ** 0.5 2.0 ``` 大抵來說,用整數進行運算時的結果都會跟我們的預期相符。但是在進行浮點數的運算時,就很常會出現奇怪的結果。舉例來說: ```python >>> 0.1 + 0.2 0.30000000000000004 ``` 怎麼多了一個[小小的誤差](https://0.30000000000000004.com/)?!這其實是因為**浮點數精確度**的問題導致的(請參考[附錄 — 資料表示法](https://hackmd.io/@kaeteyaruyo/python-programming-appendix))。電腦的儲存空間有其上限,沒有辦法無窮無盡的表達小數點後無限多位數的數字。有些時候,電腦無法非常準確的表達某些浮點數,其表示的值跟實際上想要表達的值之間會存在落差。這些誤差在經過計算後會被累加、放大,導致計算結果失準。因此,在一些非常講求精確度的應用當中,例如銀行的金融系統,或是火箭的軌道計算等等,我們會避免直接用內建的浮點數表示法去表達數字,而轉用其他方式來進行計算。 此外,當浮點數的計算結果過大時,會變成以無限大來表示;而使用無限大進行計算有時會得到未定義的結果,會以未定義數來表示。 ```python >>> 1e20000 * 1e10000 inf >>> a = 1e20000 * 1e10000 >>> a - a nan >>> a / a nan >>> a * 0 nan ``` 一些不合法的運算則會以 Error 的形式出現。 ```python >>> 1 / 0 Traceback (most recent call last): File "<stdin>", line 1, in <module> ZeroDivisionError: division by zero ``` 在進行數值運算時,這些細節都必須注意,必要時需要進行錯誤處理。 ### 2.2.2 數值的比較運算 比較運算的用意,是用來判斷數值大小關係是否成立。因此其計算結果只會是 `True` 或 `False` 而已,代表大小關係的成立與否。 ```python >>> 1 < 1 False >>> 1 <= 1 True >>> 1 == 1 True >>> 1 != 1 False >>> 1 >= 1 True >>> 1 > 1 False ``` 一般來說,整數的比較運算是沒有問題的,但是浮點數因為精確度問題的關係,在某些時候會給出錯誤的比較結果。這個問題通常發生在等於判斷的時候,例如: ```python >>> 0.1 + 0.2 == 0.3 False ``` 如上所示,因為數值計算 `0.1 + 0.2` 的結果實際上比 `0.3` 大了一點點點點,所以在進行等於判斷時,會被判斷為 `False` 。這就是為什麼我們通常不推薦對於浮點數直接進行兩者等於的判斷。若真的有需要對浮點數進行等於判斷時,我們通常會自行定義一個可容忍的誤差值 $\epsilon$ 。若欲比較相等的兩個數字差值在可容忍的誤差範圍內,我們就說這兩個數字相等。 ## 2.3 字串的運算 上述的運算子作用在數字上的時候,其結果是相當直觀的。但這些運算子如果用在文字上面會有什麼什麼效果呢?或者說,這些運算子居然可以用在文字上嗎?其實在其他許多語言中,運算子都是只能作用在數值類資料上的,但 Python 多載了某些運算子的功能,讓這些運算子作用在字串上時也有意義。以下我們就來介紹有哪些運算子可以作用在字串上。 ### 2.3.1 字串的算術運算 在 Python 中,加法和乘法這兩個運算子被多載成可以作用在字串上。 對兩個字串進行相加,會將兩個字串串接 (concatenate) 起來。有些時候我們會想要將計算結果加上一些說明文字再輸出,字串相加的功能就會相當實用。 ```python >>> 'a' + 'b' 'ab' >>> '123' + '456' '123456' >>> a = 10 >>> print('The result is ' + str(a)) The result is 10 ``` 而將字串乘上一個整數會得到字串重複數次。字串作為被乘數或乘數都可以,但要注意的是只能乘上整數,不可以乘浮點數。當要輸出指定數量的某字元時,這個運算就會相當方便。 ```python >>> '123' * 3 '123123123' >>> 10 * 'a' 'aaaaaaaaaa' >>> 'abc' * 5.0 Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: can't multiply sequence by non-int of type 'float' ``` ### 2.3.2 字串的比較運算 在 Python 中,我們可以直接使用比較運算子來比較兩個字串的大小。但這時你就要問了:字串要怎麼比大小?我們是怎麼定義哪個字比較大、哪個字比較小的呢? 其實,在電腦中,所有的資料都是以二進制數字的形式儲存的,不論這個資料本來就是數字,還是是文字、影像、聲音等等東西。但是原本不是數字的東西,卻要使用數字下去表示,那就必須要講好一個大家都認同的對應關係,如此一來所有的電腦才能正確地溝通。這個大家都認同的對應關係,就叫作**編碼 (Encoding)**。簡單來說,如果我們大家都講好在電腦裏面如果要儲存文字時, A 就記做 1、 B 就記做 2 、 C 就記做 3...,以此類推,那麼當大家在電腦裏面看到 1 的時候,就會知道這個數字作為文字時是 A 的意思。而既然這些文字都已經用數字儲存了,我們當然就可以用這些代表文字的數字來比大小。以上述例子來說,C = 3 、 A = 1,所以 C 是比 A 還要來得大的。 早期電腦科學還只在歐美地區發展時,美國使用的 [ASCII 編碼](https://zh.wikipedia.org/zh-tw/ASCII)就是當時電腦的標準編碼方式,可以表示所有的英文字母以及大部分的標點符號。後來電腦開始在世界各地普及,各個國家的語言都開始出現自己的編碼,造成多語系的文件要同時顯示不同語言時會有問題(例如:如果英文裏面 1 是 A ,但日文裏面 1 是 あ,那麼在同一個文件裏面既要顯示英文也要顯示日文時,看到 1 到底要顯示什麼呢?)。因此,後來發展出了[萬國碼 (Unicode)](https://zh.wikipedia.org/zh-tw/Unicode) 這樣的編碼方式,利用很大量的數字來為世界上所有常見語言的所有文字編碼。如此一來,要同時顯示多國語系的文件就不會有問題了。 [Python 預設的文字編碼是 UTF-8](https://docs.python.org/3/tutorial/interpreter.html?highlight=encoding#source-code-encoding),這是萬國碼的一種編碼方式。當我們要對字串比大小時, Python 就會用該字串裡的文字的 UTF-8 編號來比大小。當字串裡有很多個字時, Python 會一個字一個字比。首先比第一個字,如果第一個字不一樣就直接得到結果,如果一樣就比較第二個字...直到比到最後一個字都還一樣的話, Python 就會判斷這兩個字串大小一模一樣。 ```python >>> 'A' < 'a' # Upper case is smaller than lower case True >>> 'AbC' > 'aBC' # Compare from left to rigiht False >>> 'ABC' == 'ABC' # Two strings are equal if and only if every character is identical True >>> '一' > '壹' # Unicode encoding False ``` 在實際應用當中,字串的比較運算是相當常用的。例如:當你要登入某個網站的時候,伺服器會比較你輸入的密碼和資料庫裡儲存的密碼是否相等,由於密碼通常是用字串儲存,因此就會使用到字串相等的比較。又例如:當我們要針對某一個名單進行排序時,排序演算法也會利用到字元之間的大小關係,來對字串進行排序。 ## 2.4 運算優先度 <!-- https://aprogrammerlife.com/top-rated/what-stresses-me-out-more-the-fact-that-26-somehow-got-13-or-the-fact-that-the-correct-answer-isnt-even-an-option-1723?fbclid=IwAR1mnZZnHcWV6g6v-ljOvM7IURCLWqR-y-a5wqvLu7ZzjrNlNhTopDDmuPs --> 現在我們知道這些運算子分別的功能是什麼了,但實際應用上來說,單獨的一個運算子可以做到的事情有限。很多時候,我們會需要連續使用很多個運算子,來完成一個較為複雜的運算。 舉例來說,在統計學上,我們有時會需要對資料進行[標準化](https://zh.wikipedia.org/zh-tw/%E6%A8%99%E6%BA%96%E5%88%86%E6%95%B8)。 Z 分數標準化的計算公式如下: $$ Z = \frac{X - \mu}{\sigma} $$ 其中 $\mu$ 為資料的平均數、 $\sigma$ 是資料的標準差。當資料比平均數多一個標準差時,其 Z 分數會是 1。假設 $\mu = 10$ 、 $\sigma = 2$ ,那麼當資料為 $X = 12$ 時,這筆資料的 Z 分數就會是 $Z = 1$。 $$ Z = \frac{X - \mu}{\sigma} = \frac{12 - 10}{2} = 1 $$ 這個運算同時牽涉到減法和除法的計算,如果我們直接這樣寫 ```python Z = X - mu / sigma ``` 這樣是正確的算式嗎?讓我們來計算看看 ```python >>> mu = 10 >>> sigma = 2 >>> X = 12 >>> Z = X - mu / sigma >>> Z 7.0 ``` 怎麼算出來好像不太對?這是因為**運算子的結合優先度 (Operator precedence)** 造成的。我們小學在學習四則運算時,有學過**先乘除、後加減**這樣的口訣,這句話其實就是最基本的運算優先度的概念。當一條數學算式當中同時有加法和乘法,必須要先計算乘法,才可以再計算加法,即便加法寫在乘法前面也一樣。同樣的道理,當我們寫下 `Z = X - mu / sigma` 這樣的算式時,實際上電腦認為的計算優先順序是 ```python Z = (X - (mu / sigma)) ``` 也就是說,電腦會先計算 `mu / sigma` ,得到 `5.0` 之後,再算 `X - 5.0` ,得到 `7.0` 。如果我們想要正確表達 Z 分數當中「分子要一起計算」的意義的話,我們就需要用小括號來括住需要優先計算的部份,也就是 ```python Z = (X - mu) / sigma ``` 如此一來就可以得到正確的結果 ```python >>> mu = 10 >>> sigma = 2 >>> X = 12 >>> Z = (X - mu) / sigma >>> Z 1.0 ``` 當然, Python 當中的運算子並不是只有加減乘除,所有的運算子之間都必須要規定一個優先順序,電腦才能夠正確運算。 Python 中各種運算子的優先順序可以在[官方文件](https://docs.python.org/3/reference/expressions.html?highlight=operator#operator-precedence)中找到。表中有一些我們未來才會學到的運算子,此處我們只列出目前已學過的運算子來比較他們之間的運算優先度: | 運算子 | 功能 | |-|-| | `(expressions...)` | 小括號,愈裏面的小括號要愈先算 | | `**` | 取次方 | | `+x`, `-x`, `~x` | 取正號、取負號、位元否 | | `*`, `/`, `//`, `%` | 乘法、實數除法、整數除法、模除法 | | `+`, `-` | 加法、減法 | | `<<`, `>>` | 右位移、左位移 | | `&` | 位元且 | | `^` | 位元互斥或 | | `\|` | 位元或 | | `<`, `<=`, `>`, `>=`, `!=`, `==` | 比較運算子們 | | `=` | 賦值 | 在這個表當中,愈上面的東西優先度愈高。意思就是說**如果同一條運算式裏面同時出現了這些符號的話,愈上面的東西要愈先計算**。如果是在同一層內的話,**運算優先度相同,那麼就由左而右計算**。因此,下列這條計算式: ```python 1 + 2 * 3 / 4 ``` 會算出 `2.5` ,因為電腦在看待這句話時,他認為運算的優先順序應該是這樣的: ``` (1 + ((2 * 3) / 4)) = (1 + (6 / 4)) = (1 + 1.5) = 2.5 ``` 當我們在寫算式時,務必要注意這些不同運算子之間的優先度順序,否則可能會算出預期外的答案。如果懶得記憶哪些運算子優先度較高的話,可以一律使用小括號來明白的寫出希望優先計算的部份,如此一來程式碼的可讀性也會比較高。 現在,我們已經學會了基本的計算了。我們的程式已經可以做到一些簡單的算術任務。但現實中,有許多的問題都會牽扯到條件判斷的行為,我們會根據計算的結果來決定接下來要做什麼事情,不同的值會導致我們做出不同的決策。因此,在下一章當中,我們將要來學習如何讓我們的電腦可以根據計算結果做決定。 ## Excersices ### 2.1 日本購物 好久沒有去日本了,好想去日本大買特買啊。 2019 年 2 月 11 日時,日幣對台幣的匯率是 0.2801 ,當時日本國內的消費稅稅率還是 8% 。現在 2022 年 7 月 31 日,日幣對台幣的匯率是 0.2274,但日本國內的消費稅已經變成了 10% 。請問原價不含稅要價 130 日元的一罐綠茶,現在買比起在 2019 年的時候買還要便宜了多少新台幣呢? <!-- 參考解答 ``` >>> price_no_tax = 130 >>> price_2019 = int(price_no_tax * 0.2801 * 1.08) >>> price_2019 39 >>> price_2022 = int(price_no_tax * 0.2274 * 1.1) >>> price_2022 32 >>> price_2019 - price_2022 7 ``` --> ### 2.2 BMI BMI 的計算公式如下: $$ \text{BMI} = \frac{W}{H^2} $$ 其中,$W$ 是體重,單位為公斤、$H$ 是身高,單位是公尺。 某甲高三時的身高 159 公分、體重 55 公斤,請問他當時的 BMI 是多少呢? (:有種就放現在的身高體重 R!) (對...我就沒種...:cry:) <!-- 參考解答 ``` >>> W = 55 >>> H = 1.59 >>> BMI = W / H ** 2 >>> BMI 21.75546853368142 ``` --> ### 2.3 今天星期幾? [蔡勒公式 (Zellers Kongruenz)](https://zh.wikipedia.org/zh-tw/%E8%94%A1%E5%8B%92%E5%85%AC%E5%BC%8F),是一種計算任何一日屬一星期中哪一日的演算法,由德國數學家克里斯提安·蔡勒推算出來。其公式為 $$ w = \Bigg( y + \Big[\frac{y}{4}\Big] + \Big[\frac{c}{4}\Big] - 2c + \Bigg[\frac{26(m+1)}{10}\Bigg] + d - 1 \Bigg) \text{ mod } 7 $$ 其中 * $w$ 為星期數,0 = 星期日、 1 = 星期一、...,以此類推。 * $c$ 為年份前兩位數,例如: 2022 年, $c = 20$ * $y$ 為年份後兩位數,例如: 2022 年, $y = 22$ * $m$ 為月份數,但是在蔡勒公式中,某年的 1、2 月要看作上一年的 13、14 月來計算,因此若是 2022 年 1 月,必須要視為 2021 年 13 月來計算。 * $d$ 為日期,例如: 7 月 31 日, $d = 31$。 * $[ ]$ 稱作高斯符號,代表無條件捨去而得的整數。 * mod 為同餘,也就是取餘數的意思。 請問根據蔡勒公式,你出生的那一天是星期幾呢? <!-- 參考解答 ``` >>> # My birthday is 1997/09/30 >>> c = 19 >>> y = 97 >>> m = 9 >>> d = 30 >>> w = (y + y // 4 + c // 4 - 2 * c + (26 * (m + 1)) // 10 + d - 1) % 7 >>> w 2 >>> # My birthday is Tuesday ``` --> :::info :memo: **查看參考解答** 點選頁面右上角的 ![](https://i.imgur.com/dsGJToB.png) 進入編輯模式,就可以在編輯器當中看到參考答案~ ::: ---- [<< Lec 01 - 變數宣告與資料型態](https://hackmd.io/@kaeteyaruyo/python-programming-01) | [目錄](https://hackmd.io/@kaeteyaruyo/python-programming-index) | [Lec 03 - 流程控制 >>](https://hackmd.io/@kaeteyaruyo/python-programming-03)