算術運算

April 11, 2022

學習程式語言,加減乘除應該是很基本,不就是使用 +-*/ 運算子嗎?不過,實際上並不是那麼簡單,畢竟你還是在跟電腦打交道,目前還沒有一個程式語言,可以高階到完全忽略電腦的物理性質,因此,有些細節還是要注意一下。

應用於數值型態

1 + 11 - 0.1 不成問題,結果分別是 2、0.9,那麼 0.1 + 0.1 + 0.11.0 - 0.8 會是多少?前者不是 0.3,後者也不是 0.2。

>>> 0.1 + 0.1 + 0.1
0.30000000000000004
>>> 1.0 - 0.8
0.19999999999999996
>>>

開發人員基本上都要知道 IEEE 754 浮點數算術標準,Python 也遵守此標準,這個算術標準不使用小數點,而是使用分數及指數來表示小數,例如 0.5 會以 1/2 來表示,0.75 會以 1/2+1/4 來表示,0.875 會以 1/2+1/4+1/8 來表示,然而,有些小數無法使用有限的分數來表示,像是 0.1,會是 1/16+1/32+1/256+1/512 +1/4096+1/8192+… 沒有止境,因此造成了浮點數誤差。

那如果對小數點的精度要求很高的話,就要小心這個問題,像是最基本的 0.1 + 0.1 + 0.1 == 0.3,結果會 False,如果程式碼中有這類的判斷,那麼就會因為誤差,而使得程式行為不會是你想像的方式進行。

如果需要處理小數,而且需要精確的結果,可以使用 decimal.Decimal 類別。例如:

import sys
import decimal

n1 = float(sys.argv[1])
n2 = float(sys.argv[2])
d1 = decimal.Decimal(sys.argv[1])
d2 = decimal.Decimal(sys.argv[2])

print('# 不使用 decimal')
print(f'{n1} + {n2} = {n1 + n2}')
print(f'{n1} - {n2} = {n1 - n2}')
print(f'{n1} * {n2} = {n1 * n2}')
print(f'{n1} / {n2} = {n1 / n2}')

print()  # 換行

print(f'{d1} + {d2} = {d1 + d2}')
print(f'{d1} - {d2} = {d1 - d2}')
print(f'{d1} * {d2} = {d1 * d2}')
print(f'{d1} / {d2} = {d1 / d2}')

這個程式可以使用命令列引數指定兩個數字,可以觀察到是否使用的差別,一個執行範例是:

>python decimal_demo.py 1.0 0.8
# 不使用 decimal
1.0 + 0.8 = 1.8
1.0 - 0.8 = 0.19999999999999996
1.0 * 0.8 = 0.8
1.0 / 0.8 = 1.25

# 使用 decimal
1.0 + 0.8 = 1.8
1.0 - 0.8 = 0.2
1.0 * 0.8 = 0.80
1.0 / 0.8 = 1.25

為避免撰寫浮點數實字時的誤差,指定數字時必須使用字串;decimal.Decimal 定義了運算子相關的特殊方法,可以直接使用 +-*/ 等運算子,這樣的方便性,也是 Python 在數值運算上受歡迎的原因之一,運算結果也是以 decimal.Decimal 型態傳回。

在乘法運算上,除了可以使用 * 進行兩個數字的相乘之外,還可以使用 ** 進行指數運算。例如:

>>> 2 ** 3
8
>>> 2 ** 5
32
>>> 2 ** 10
1024
>>> 9 ** 0.5
3.0
>>>

在除法運算上,有 /// 兩個運算子,前者是 True Division,也就是除法結果是浮點數的話,/ 就是得到浮點數結果;// 是 Floor Division,也就是除法後實行 math.floor,往負方向的捨入,只留下整數部份。

>>> 10 / 3
3.3333333333333335
>>> 10 // 3
3
>>> 10 / 3.0
3.3333333333333335
>>> 10 // 3.0
3.0
>>>

10 除 3 的結果是 3.333333333… 往負方向捨入的最大整數是 3。

那麼 10 除以 -3 呢?10 除 -3 的結果是 -3.333333333… 往負方向捨入的最大整數是 -4,可以使用底下的範例來印證:

>>> 10 // -3
-4
>>> 10 // -3.0
-4.0
>>>

還有個 % 沒談到,a % b 時會進行除法運算,並取餘數作為結果。至於布林值需要進行 +-*/ 等運算時,True 會被當成是 1,False 會被當成是 0,接著再進行運算。

應用於字串型態

使用 + 運算子可以串接字串,使用 * 可以重複字串:

>>> text1 = 'Just'
>>> text2 = 'in'
>>> text1 + text2
'Justin'
>>> text1 * 10
'JustJustJustJustJustJustJustJustJustJust'
>>>

字串不可變動,因此 + 串接字串時產生新字串。在強型別(Strong type)與弱型別(Weak type)的光譜中,Python 偏向強型別,也就是型態間在運算時,比較不會自行發生轉換,在 Python 中,字串與數字不能進行 + 運算,若要進行字串串接,得將數字轉為字串,若要進行數字運算,得將字串剖析為數字。例如:

>>> '10' + 1
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: Can't convert 'int' object to str implicitly
>>> '10' + str(1)
'101'
>>> int('10') + 1
11
>>>

應用於 list 與 tuple

list 有許多方面與字串類似,使用 + 運算子可以串接 list,使用 * 可以重複 list

>>> nums1 = ['one', 'two']
>>> nums2 = ['three', 'four']
>>> nums1 + nums2
['one', 'two', 'three', 'four']
>>> nums1 * 2
['one', 'two', 'one', 'two']
>>>

+串接兩 list 會產生新 list,然後將來源兩個 list 的元素參考,複製至新產生的 list,同樣道理也應用在使用 *重複 list 時:

>>> nums1 = ['one', 'two']
>>> nums2 = ['three', 'four']
>>> nums_lt = [nums1, nums2]
>>> nums_lt
[['one', 'two'], ['three', 'four']]
>>> nums1[0], nums1[1] = '1', '2'
>>> nums_lt
[['1', '2'], ['three', 'four']]
>>>

在上例中,nums_lt[0] 只是參考至 nums1 參考的 list,因此,透過 nums1 來修改索引位置的元素,nums_lt 取得的也會是修改過的結果。

tuplelist 有許多類似之處,+* 的操作在 tuple 也有同樣效果,雖然說,tuple 本身的結構不可變動,不過,這並不是指當中的元素本身也不可變動。例如:

>>> nums1 = ['one', 'two']
>>> nums2 = ['three', 'four']
>>> nums_tp = (nums1, nums2)
>>> nums_tp
(['one', 'two'], ['three', 'four'])
>>> nums1[0], nums1[1] = '1', '2'
>>> nums_tp
(['1', '2'], ['three', 'four'])
>>>

nums_tp 本身是個 tuple,然而放了兩個 list 作為元素,list 是可變動的,因此才會有這樣的結果,tuple 本身不能變動,是指不能做 nums_tp[0] = ['five', 'six'] 這類的事。

分享到 LinkedIn 分享到 Facebook 分享到 Twitter