python 浮點數相加相相除_來聊聊Python裡的“數”
技術標籤:python 浮點數相加相相除
Python中,數,用各種形式表示,不同形式的數有各自的用途。
整數
整數,令人驚歎於它的簡單。兩個整數相除,例如4/3,得到一個浮點數,並且(4/3)*3的結果也是浮點數4.0。即便你沒有定義浮點數,在進行除法運算的時候,它會自動出現。
浮點數
浮點數不是一般意義的數。按照數學上的規定,數應該遵循如下原則:減法是加法的逆運算,加法結合律,等等。
例如:
>>> 1 + 2 - 2 - 10>>> 0.1 + 0.2 - 0.2 - 0.12.7755575615628914e-17
兩個數相加,再分別減去它們,上述居然出現了不同的結果。
它們也不會遵循結合律:a + (b + c) = (a + b) + c
>>> a = 2**-53>>> (a + a) + 1 == a + (a + 1)False
以上僅僅是浮點數運算中存在的兩個“小問題”,還不令你驚訝嗎?此處不便將浮點數各種出乎意料的運算一一展現。
分數
很多看似簡單的程式,遇到分數,就會出問題,比如運算時間暴增,演算法的複雜度加倍。遇到分數的時候,演算法時間不是跟輸入成正比,而是指數增長。
如果時間足夠長,記憶體爆掉也是常見的。
加法就是其中一個典型例子
>>> print(set(type(p) for p in primes))>>> one = fractions.Fraction(1)>>> before = datetime.now()>>> res = sum(one/p for p in primes[:10000])>>> after = datetime.now()>>> print("It took", after-before)>>> print("Size of output", len(str(res)))>>> print("Approximate value", float(res)){}It took 0:01:16.033260Size of output 90676Approximate value 2.7092582487972945
這段程式,計算了一些素數的倒數的和。在膝上型電腦上,10000個這樣的數相加,要1分鐘,最終輸出結果的大小超過了90K。
對比著,執行浮點數運算,效能更好。
>>> print(set(type(p) for p in primes))>>> before = datetime.now()>>> res = sum(1/p for p in primes[:10000])>>> after = datetime.now()>>> print("It took", after-before)>>> print("Size of output", len(str(res)))>>> print("Approximate value", float(res)){}It took 0:00:00.000480Size of output 17Approximate value 2.709258248797317
這次執行時間小於1毫秒,並且,者還可能是因為用datetime測量產生的誤差,快了10000倍。而且輸出結果的大小僅有17位元,下降了1000多倍。然而,計算結果有誤差。
Approximate value 2.7092582487972945Approximate value 2.709258248797317 1234567891234
誤差低於 10的-14次方,這就如同將火箭發射月球上偏差了1毫米,用浮點數計算得到的結果足夠精確,並且效率更高。
對此,一般的觀點是:Python進行分數運算很慢。對此,Python可以承擔10倍的責任,但不是10000倍。有一個第三方模組,quicktions,用Cython執行分數的運算。
用quicktions,真的“很快”。在我的膝上型電腦上,上面那個程式的時間,從1分16秒,縮短到1分15秒。
問題在於程式本身,在程式中,我精心選擇了一種輸入方案,以素數作為分母進行分數相加,這本來就是一種很壞的情況。
小數
小數在財務中用途最廣,最無聊的是居然以法律的方式規定了小數的形式。然而,Python中所有的小數點運算,都有上下文精確度問題,對此,可以用專門的模組解決。
>>> getcontext().prec = 6>>> Decimal(1) / Decimal(7)Decimal('0.142857')>>> getcontext().prec = 28>>> Decimal(1) / Decimal(7)Decimal('0.1428571428571428571428571429')
在實際專案中,程式碼中設定精度的位置和進行計算的位置可能間隔幾百行,計算可以在一個函式中,也可以在另外一個檔案。
最安全的方法是使用localcontext:
>>> getcontext().prec = 6>>> # 6853 lines elided... with localcontext() as ctx:... ctx.prec = 10... Decimal(1) / Decimal(7)...Decimal('0.1428571429')
只要你認真地用localcontext,小數運算不會出問題。
總結
你在程式中用到數字的時候,是否想過:應該用什麼型別?會發生什麼?誤差重要嗎?
什麼也不想,會意味著暗藏bug。