鍍金池/ 教程/ Python/ 數(shù)字日期和時間
類與對象
模塊與包
數(shù)據(jù)編碼和處理
元編程
網(wǎng)絡(luò)與 Web 編程
數(shù)字日期和時間
測試、調(diào)試和異常
字符串和文本
文件與 IO
腳本編程與系統(tǒng)管理
迭代器與生成器
函數(shù)
C 語言擴(kuò)展
并發(fā)編程
數(shù)據(jù)結(jié)構(gòu)和算法

數(shù)字日期和時間

在 Python 中執(zhí)行整數(shù)和浮點(diǎn)數(shù)的數(shù)學(xué)運(yùn)算時很簡單的。 盡管如此,如果你需要執(zhí)行分?jǐn)?shù)、數(shù)組或者是日期和時間的運(yùn)算的話,就得做更多的工作了。 本章集中討論的就是這些主題。

數(shù)字的四舍五入

問題

你想對浮點(diǎn)數(shù)執(zhí)行指定精度的舍入運(yùn)算。

解決方案

對于簡單的舍入運(yùn)算,使用內(nèi)置的 round(value, ndigits) 函數(shù)即可。比如:

>>> round(1.23, 1)
1.2
>>> round(1.27, 1)
1.3
>>> round(-1.27, 1)
-1.3
>>> round(1.25361,3)
1.254
>>>

當(dāng)一個值剛好在兩個邊界的中間的時候, round函數(shù)返回離它最近的偶數(shù)。 也就是說,對1.5或者2.5的舍入運(yùn)算都會得到2。

傳給 round() 函數(shù)的 ndigits 參數(shù)可以是負(fù)數(shù),這種情況下, 舍入運(yùn)算會作用在十位、百位、千位等上面。比如:

>>> a = 1627731
>>> round(a, -1)
1627730
>>> round(a, -2)
1627700
>>> round(a, -3)
1628000
>>>

討論

不要將舍入和格式化輸出搞混淆了。 如果你的目的只是簡單的輸出一定寬度的數(shù),你不需要使用 round() 函數(shù)。 而僅僅只需要在格式化的時候指定精度即可。比如:

>>> x = 1.23456
>>> format(x, '0.2f')
'1.23'
>>> format(x, '0.3f')
'1.235'
>>> 'value is {:0.3f}'.format(x)
'value is 1.235'
>>>

同樣,不要試著去舍入浮點(diǎn)值來”修正”表面上看起來正確的問題。比如,你可能傾向于這樣做:

>>> a = 2.1
>>> b = 4.2
>>> c = a + b
>>> c
6.300000000000001
>>> c = round(c, 2) # "Fix" result (???)
>>> c
6.3
>>>

對于大多數(shù)使用到浮點(diǎn)的程序,沒有必要也不推薦這樣做。 盡管在計算的時候會有一點(diǎn)點(diǎn)小的誤差,但是這些小的誤差是能被理解與容忍的。 如果不能允許這樣的小誤差(比如涉及到金融領(lǐng)域),那么就得考慮使用 decimal 模塊了,下一節(jié)我們會詳細(xì)討論。

執(zhí)行精確的浮點(diǎn)數(shù)運(yùn)算

問題

你需要對浮點(diǎn)數(shù)執(zhí)行精確的計算操作,并且不希望有任何小誤差的出現(xiàn)。

解決方案

浮點(diǎn)數(shù)的一個普遍問題是它們并不能精確的表示十進(jìn)制數(shù)。 并且,即使是最簡單的數(shù)學(xué)運(yùn)算也會產(chǎn)生小的誤差,比如:

>>> a = 4.2
>>> b = 2.1
>>> a + b
6.300000000000001
>>> (a + b) == 6.3
False
>>>

這些錯誤是由底層 CPU 和 IEEE 754 標(biāo)準(zhǔn)通過自己的浮點(diǎn)單位去執(zhí)行算術(shù)時的特征。 由于 Python 的浮點(diǎn)數(shù)據(jù)類型使用底層表示存儲數(shù)據(jù),因此你沒辦法去避免這樣的誤差。

如果你想更加精確(并能容忍一定的性能損耗),你可以使用 decimal 模塊:

>>> from decimal import Decimal
>>> a = Decimal('4.2')
>>> b = Decimal('2.1')
>>> a + b
Decimal('6.3')
>>> print(a + b)
6.3
>>> (a + b) == Decimal('6.3')
True

初看起來,上面的代碼好像有點(diǎn)奇怪,比如我們用字符串來表示數(shù)字。 然而, Decimal 對象會像普通浮點(diǎn)數(shù)一樣的工作(支持所有的常用數(shù)學(xué)運(yùn)算)。 如果你打印它們或者在字符串格式化函數(shù)中使用它們,看起來跟普通數(shù)字沒什么兩樣。

decimal 模塊的一個主要特征是允許你控制計算的每一方面,包括數(shù)字位數(shù)和四舍五入運(yùn)算。 為了這樣做,你先得創(chuàng)建一個本地上下文并更改它的設(shè)置,比如:

>>> from decimal import localcontext
>>> a = Decimal('1.3')
>>> b = Decimal('1.7')
>>> print(a / b)
0.7647058823529411764705882353
>>> with localcontext() as ctx:
... ctx.prec = 3
... print(a / b)
...
0.765
>>> with localcontext() as ctx:
... ctx.prec = 50
... print(a / b)
...
0.76470588235294117647058823529411764705882352941176
>>>

討論 decimal 模塊實(shí)現(xiàn)了 IBM 的”通用小數(shù)運(yùn)算規(guī)范”。不用說,有很多的配置選項這本書沒有提到。

Python 新手會傾向于使用 decimal 模塊來處理浮點(diǎn)數(shù)的精確運(yùn)算。 然而,先理解你的應(yīng)用程序目的是非常重要的。 如果你是在做科學(xué)計算或工程領(lǐng)域的計算、電腦繪圖,或者是科學(xué)領(lǐng)域的大多數(shù)運(yùn)算, 那么使用普通的浮點(diǎn)類型是比較普遍的做法。 其中一個原因是,在真實(shí)世界中很少會要求精確到普通浮點(diǎn)數(shù)能提供的17位精度。 因此,計算過程中的那么一點(diǎn)點(diǎn)的誤差是被允許的。 第二點(diǎn)就是,原生的浮點(diǎn)數(shù)計算要快的多-有時候你在執(zhí)行大量運(yùn)算的時候速度也是非常重要的。

即便如此,你卻不能完全忽略誤差。數(shù)學(xué)家花了大量時間去研究各類算法,有些處理誤差會比其他方法更好。 你也得注意下減法刪除已經(jīng)大數(shù)和小數(shù)的加分運(yùn)算所帶來的影響。比如:

>>> nums = [1.23e+18, 1, -1.23e+18]
>>> sum(nums) # Notice how 1 disappears
0.0
>>>

上面的錯誤可以利用 math.fsum() 所提供的更精確計算能力來解決:

>>> import math
>>> math.fsum(nums)
1.0
>>>

然而,對于其他的算法,你應(yīng)該仔細(xì)研究它并理解它的誤差產(chǎn)生來源。

總的來說, decimal 模塊主要用在涉及到金融的領(lǐng)域。 在這類程序中,哪怕是一點(diǎn)小小的誤差在計算過程中蔓延都是不允許的。 因此, decimal 模塊為解決這類問題提供了方法。 當(dāng) Python 和數(shù)據(jù)庫打交道的時候也通常會遇到 Decimal 對象,并且,通常也是在處理金融數(shù)據(jù)的時候。

數(shù)字的格式化輸出

問題

你需要將數(shù)字格式化后輸出,并控制數(shù)字的位數(shù)、對齊、千位分隔符和其他的細(xì)節(jié)。

解決方案

格式化輸出單個數(shù)字的時候,可以使用內(nèi)置的 format() 函數(shù),比如:

>>> x = 1234.56789

>>> # Two decimal places of accuracy
>>> format(x, '0.2f')
'1234.57'

>>> # Right justified in 10 chars, one-digit accuracy
>>> format(x, '>10.1f')
'    1234.6'

>>> # Left justified
>>> format(x, '<10.1f')
'1234.6    '

>>> # Centered
>>> format(x, '^10.1f')
'  1234.6  '

>>> # Inclusion of thousands separator
>>> format(x, ',')
'1,234.56789'
>>> format(x, '0,.1f')
'1,234.6'
>>>

如果你想使用指數(shù)記法,將 f 改成 e 或者 E(取決于指數(shù)輸出的大小寫形式)。比如:

>>> format(x, 'e')
'1.234568e+03'
>>> format(x, '0.2E')
'1.23E+03'
>>>

同時指定寬度和精度的一般形式是 '[<>^]?width[,]?(.digits)?' , 其中 widthdigits為整數(shù),?代表可選部分。 同樣的格式也被用在字符串的 format() 方法中。比如:

>>> 'The value is {:0,.2f}'.format(x)
'The value is 1,234.57'
>>>

討論

數(shù)字格式化輸出通常是比較簡單的。上面演示的技術(shù)同時適用于浮點(diǎn)數(shù)和 decimal 模塊中的 Decimal 數(shù)字對象。

當(dāng)指定數(shù)字的位數(shù)后,結(jié)果值會根據(jù) round() 函數(shù)同樣的規(guī)則進(jìn)行四舍五入后返回。比如:

>>> x
1234.56789
>>> format(x, '0.1f')
'1234.6'
>>> format(-x, '0.1f')
'-1234.6'
>>>

包含千位符的格式化跟本地化沒有關(guān)系。 如果你需要根據(jù)地區(qū)來顯示千位符,你需要自己去調(diào)查下 locale 模塊中的函數(shù)了。 你同樣也可以使用字符串的 translate() 方法來交換千位符。比如:

>>> swap_separators = { ord('.'):',', ord(','):'.' }
>>> format(x, ',').translate(swap_separators)
'1.234,56789'
>>>

在很多 Python 代碼中會看到使用%來格式化數(shù)字的,比如:

>>> '%0.2f' % x
'1234.57'
>>> '%10.1f' % x
'    1234.6'
>>> '%-10.1f' % x
'1234.6    '
>>>

這種格式化方法也是可行的,不過比更加先進(jìn)的 format() 要差一點(diǎn)。 比如,在使用%操作符格式化數(shù)字的時候,一些特性(添加千位符)并不能被支持。

二八十六進(jìn)制整數(shù)

問題

你需要轉(zhuǎn)換或者輸出使用二進(jìn)制,八進(jìn)制或十六進(jìn)制表示的整數(shù)。

解決方案

為了將整數(shù)轉(zhuǎn)換為二進(jìn)制、八進(jìn)制或十六進(jìn)制的文本串, 可以分別使用 bin() , oct()hex()函數(shù):

>>> x = 1234
>>> bin(x)
'0b10011010010'
>>> oct(x)
'0o2322'
>>> hex(x)
'0x4d2'
>>>

另外,如果你不想輸出 0b , 0o 或者0x的前綴的話,可以使用 format()函數(shù)。比如:

>>> format(x, 'b')
'10011010010'
>>> format(x, 'o')
'2322'
>>> format(x, 'x')
'4d2'
>>>

整數(shù)是有符號的,所以如果你在處理負(fù)數(shù)的話,輸出結(jié)果會包含一個負(fù)號。比如:

>>> x = -1234
>>> format(x, 'b')
'-10011010010'
>>> format(x, 'x')
'-4d2'
>>>

如果你想產(chǎn)生一個無符號值,你需要增加一個指示最大位長度的值。比如為了顯示32位的值,可以像下面這樣寫:

>>> x = -1234
>>> format(2**32 + x, 'b')
'11111111111111111111101100101110'
>>> format(2**32 + x, 'x')
'fffffb2e'
>>>

為了以不同的進(jìn)制轉(zhuǎn)換整數(shù)字符串,簡單的使用帶有進(jìn)制的 int() 函數(shù)即可:

>>> int('4d2', 16)
1234
>>> int('10011010010', 2)
1234
>>>

討論

大多數(shù)情況下處理二進(jìn)制、八進(jìn)制和十六進(jìn)制整數(shù)是很簡單的。 只要記住這些轉(zhuǎn)換屬于整數(shù)和其對應(yīng)的文本表示之間的轉(zhuǎn)換即可。永遠(yuǎn)只有一種整數(shù)類型。

最后,使用八進(jìn)制的程序員有一點(diǎn)需要注意下。 Python 指定八進(jìn)制數(shù)的語法跟其他語言稍有不同。比如,如果你像下面這樣指定八進(jìn)制,會出現(xiàn)語法錯誤:

>>> import os
>>> os.chmod('script.py', 0755)
    File "<stdin>", line 1
        os.chmod('script.py', 0755)
                            ^
SyntaxError: invalid token
>>>

需確保八進(jìn)制數(shù)的前綴是0o ,就像下面這樣:

>>> os.chmod('script.py', 0o755)
>>>

字節(jié)到大整數(shù)的打包與解包

問題

你有一個字節(jié)字符串并想將它解壓成一個整數(shù)?;蛘撸阈枰獙⒁粋€大整數(shù)轉(zhuǎn)換為一個字節(jié)字符串。

解決方案

假設(shè)你的程序需要處理一個擁有128位長的16個元素的字節(jié)字符串。比如:

data = b'\x00\x124V\x00x\x90\xab\x00\xcd\xef\x01\x00#\x004'

為了將 bytes 解析為整數(shù),使用int.from_bytes() 方法,并像下面這樣指定字節(jié)順序:

>>> len(data)
16
>>> int.from_bytes(data, 'little')
69120565665751139577663547927094891008
>>> int.from_bytes(data, 'big')
94522842520747284487117727783387188
>>>

為了將一個大整數(shù)轉(zhuǎn)換為一個字節(jié)字符串,使用 int.to_bytes() 方法,并像下面這樣指定字節(jié)數(shù)和字節(jié)順序:

>>> x = 94522842520747284487117727783387188
>>> x.to_bytes(16, 'big')
b'\x00\x124V\x00x\x90\xab\x00\xcd\xef\x01\x00#\x004'
>>> x.to_bytes(16, 'little')
b'4\x00#\x00\x01\xef\xcd\x00\xab\x90x\x00V4\x12\x00'
>>>

討論

大整數(shù)和字節(jié)字符串之間的轉(zhuǎn)換操作并不常見。 然而,在一些應(yīng)用領(lǐng)域有時候也會出現(xiàn),比如密碼學(xué)或者網(wǎng)絡(luò)。 例如,IPv6網(wǎng)絡(luò)地址使用一個128位的整數(shù)表示。 如果你要從一個數(shù)據(jù)記錄中提取這樣的值的時候,你就會面對這樣的問題。

作為一種替代方案,你可能想使用6.11小節(jié)中所介紹的 struct 模塊來解壓字節(jié)。 這樣也行得通,不過利用 struct 模塊來解壓對于整數(shù)的大小是有限制的。 因此,你可能想解壓多個字節(jié)串并將結(jié)果合并為最終的結(jié)果,就像下面這樣:

>>> data
b'\x00\x124V\x00x\x90\xab\x00\xcd\xef\x01\x00#\x004'
>>> import struct
>>> hi, lo = struct.unpack('>QQ', data)
>>> (hi << 64) + lo
94522842520747284487117727783387188
>>>

字節(jié)順序規(guī)則(little 或 big)僅僅指定了構(gòu)建整數(shù)時的字節(jié)的低位高位排列方式。 我們從下面精心構(gòu)造的16進(jìn)制數(shù)的表示中可以很容易的看出來:

>>> x = 0x01020304
>>> x.to_bytes(4, 'big')
b'\x01\x02\x03\x04'
>>> x.to_bytes(4, 'little')
b'\x04\x03\x02\x01'
>>>

如果你試著將一個整數(shù)打包為字節(jié)字符串,那么它就不合適了,你會得到一個錯誤。 如果需要的話,你可以使用 int.bit_length() 方法來決定需要多少字節(jié)位來存儲這個值。

>>> x = 523 ** 23
>>> x
335381300113661875107536852714019056160355655333978849017944067
>>> x.to_bytes(16, 'little')
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
OverflowError: int too big to convert
>>> x.bit_length()
208
>>> nbytes, rem = divmod(x.bit_length(), 8)
>>> if rem:
... nbytes += 1
...
>>>
>>> x.to_bytes(nbytes, 'little')
b'\x03X\xf1\x82iT\x96\xac\xc7c\x16\xf3\xb9\xcf...\xd0'
>>>

復(fù)數(shù)的數(shù)學(xué)運(yùn)算

問題

你寫的最新的網(wǎng)絡(luò)認(rèn)證方案代碼遇到了一個難題,并且你唯一的解決辦法就是使用復(fù)數(shù)空間。 再或者是你僅僅需要使用復(fù)數(shù)來執(zhí)行一些計算操作。

解決方案

復(fù)數(shù)可以用使用函數(shù) complex(real, imag) 或者是帶有后綴j的浮點(diǎn)數(shù)來指定。比如:

>>> a = complex(2, 4)
>>> b = 3 - 5j
>>> a
(2+4j)
>>> b
(3-5j)
>>>

對應(yīng)的實(shí)部、虛部和共軛復(fù)數(shù)可以很容易的獲取。就像下面這樣:

>>> a.real
2.0
>>> a.imag
4.0
>>> a.conjugate()
(2-4j)
>>>

另外,所有常見的數(shù)學(xué)運(yùn)算都可以工作:

>>> a + b
(5-1j)
>>> a * b
(26+2j)
>>> a / b
(-0.4117647058823529+0.6470588235294118j)
>>> abs(a)
4.47213595499958
>>>

如果要執(zhí)行其他的復(fù)數(shù)函數(shù)比如正弦、余弦或平方根,使用 cmath模塊:

>>> import cmath
>>> cmath.sin(a)
(24.83130584894638-11.356612711218174j)
>>> cmath.cos(a)
(-11.36423470640106-24.814651485634187j)
>>> cmath.exp(a)
(-4.829809383269385-5.5920560936409816j)
>>>

討論

Python 中大部分與數(shù)學(xué)相關(guān)的模塊都能處理復(fù)數(shù)。 比如如果你使用 numpy ,可以很容易的構(gòu)造一個復(fù)數(shù)數(shù)組并在這個數(shù)組上執(zhí)行各種操作:

>>> import numpy as np
>>> a = np.array([2+3j, 4+5j, 6-7j, 8+9j])
>>> a
array([ 2.+3.j, 4.+5.j, 6.-7.j, 8.+9.j])
>>> a + 2
array([ 4.+3.j, 6.+5.j, 8.-7.j, 10.+9.j])
>>> np.sin(a)
array([ 9.15449915 -4.16890696j, -56.16227422 -48.50245524j,
        -153.20827755-526.47684926j, 4008.42651446-589.49948373j])
>>>

Python 的標(biāo)準(zhǔn)數(shù)學(xué)函數(shù)確實(shí)情況下并不能產(chǎn)生復(fù)數(shù)值,因此你的代碼中不可能會出現(xiàn)復(fù)數(shù)返回值。比如:

>>> import math
>>> math.sqrt(-1)
Traceback (most recent call last):
    File "<stdin>", line 1, in <module>
ValueError: math domain error
>>>

如果你想生成一個復(fù)數(shù)返回結(jié)果,你必須顯示的使用cmath模塊,或者在某個支持復(fù)數(shù)的庫中聲明復(fù)數(shù)類型的使用。比如:

>>> import cmath
>>> cmath.sqrt(-1)
1j
>>>

無窮大與 NaN

問題

你想創(chuàng)建或測試正無窮、負(fù)無窮或 NaN(非數(shù)字)的浮點(diǎn)數(shù)。

解決方案

Python 并沒有特殊的語法來表示這些特殊的浮點(diǎn)值,但是可以使用 float() 來創(chuàng)建它們。比如:

>>> a = float('inf')
>>> b = float('-inf')
>>> c = float('nan')
>>> a
inf
>>> b
-inf
>>> c
nan
>>>

為了測試這些值的存在,使用 math.isinf()math.isnan() 函數(shù)。比如:

>>> math.isinf(a)
True
>>> math.isnan(c)
True
>>>

討論

想了解更多這些特殊浮點(diǎn)值的信息,可以參考 IEEE 754 規(guī)范。 然而,也有一些地方需要你特別注意,特別是跟比較和操作符相關(guān)的時候。

無窮大數(shù)在執(zhí)行數(shù)學(xué)計算的時候會傳播,比如:

>>> a = float('inf')
>>> a + 45
inf
>>> a * 10
inf
>>> 10 / a
0.0
>>>

但是有些操作時未定義的并會返回一個 NaN 結(jié)果。比如:

>>> a = float('inf')
>>> a/a
nan
>>> b = float('-inf')
>>> a + b
nan
>>>

NaN 值會在所有操作中傳播,而不會產(chǎn)生異常。比如:

>>> c = float('nan')
>>> c + 23
nan
>>> c / 2
nan
>>> c * 2
nan
>>> math.sqrt(c)
nan
>>>

NaN 值的一個特別的地方時它們之間的比較操作總是返回 False。比如:

>>> c = float('nan')
>>> d = float('nan')
>>> c == d
False
>>> c is d
False
>>>

由于這個原因,測試一個 NaN 值得唯一安全的方法就是使用 math.isnan() ,也就是上面演示的那樣。

有時候程序員想改變 Python 默認(rèn)行為,在返回?zé)o窮大或 NaN 結(jié)果的操作中拋出異常。 fpectl 模塊可以用來改變這種行為,但是它在標(biāo)準(zhǔn)的 Python 構(gòu)建中并沒有被啟用,它是平臺相關(guān)的, 并且針對的是專家級程序員??梢詤⒖荚诰€的 Python 文檔獲取更多的細(xì)節(jié)。

分?jǐn)?shù)運(yùn)算

問題

你進(jìn)入時間機(jī)器,突然發(fā)現(xiàn)你正在做小學(xué)家庭作業(yè),并涉及到分?jǐn)?shù)計算問題。 或者你可能需要寫代碼去計算在你的木工工廠中的測量值。

解決方案

fractions 模塊可以被用來執(zhí)行包含分?jǐn)?shù)的數(shù)學(xué)運(yùn)算。比如:

>>> from fractions import Fraction
>>> a = Fraction(5, 4)
>>> b = Fraction(7, 16)
>>> print(a + b)
27/16
>>> print(a * b)
35/64

>>> # Getting numerator/denominator
>>> c = a * b
>>> c.numerator
35
>>> c.denominator
64

>>> # Converting to a float
>>> float(c)
0.546875

>>> # Limiting the denominator of a value
>>> print(c.limit_denominator(8))
4/7

>>> # Converting a float to a fraction
>>> x = 3.75
>>> y = Fraction(*x.as_integer_ratio())
>>> y
Fraction(15, 4)
>>>

討論

在大多數(shù)程序中一般不會出現(xiàn)分?jǐn)?shù)的計算問題,但是有時候還是需要用到的。 比如,在一個允許接受分?jǐn)?shù)形式的測試單位并以分?jǐn)?shù)形式執(zhí)行運(yùn)算的程序中, 直接使用分?jǐn)?shù)可以減少手動轉(zhuǎn)換為小數(shù)或浮點(diǎn)數(shù)的工作。

大型數(shù)組運(yùn)算

問題

你需要在大數(shù)據(jù)集(比如數(shù)組或網(wǎng)格)上面執(zhí)行計算。

解決方案

涉及到數(shù)組的重量級運(yùn)算操作,可以使用 NumPy 庫。 NumPy的一個主要特征是它會給 Python 提供一個數(shù)組對象,相比標(biāo)準(zhǔn)的 Python 列表而已更適合用來做數(shù)學(xué)運(yùn)算。 下面是一個簡單的小例子,向你展示標(biāo)準(zhǔn)列表對象和 NumPy 數(shù)組對象之間的差別:

>>> # Python lists
>>> x = [1, 2, 3, 4]
>>> y = [5, 6, 7, 8]
>>> x * 2
[1, 2, 3, 4, 1, 2, 3, 4]
>>> x + 10
Traceback (most recent call last):
    File "<stdin>", line 1, in <module>
TypeError: can only concatenate list (not "int") to list
>>> x + y
[1, 2, 3, 4, 5, 6, 7, 8]

>>> # Numpy arrays
>>> import numpy as np
>>> ax = np.array([1, 2, 3, 4])
>>> ay = np.array([5, 6, 7, 8])
>>> ax * 2
array([2, 4, 6, 8])
>>> ax + 10
array([11, 12, 13, 14])
>>> ax + ay
array([ 6, 8, 10, 12])
>>> ax * ay
array([ 5, 12, 21, 32])
>>>

正如所見,兩種方案中數(shù)組的基本數(shù)學(xué)運(yùn)算結(jié)果并不相同。 特別的, NumPy 中的標(biāo)量運(yùn)算(比如 ax * 2ax + 10 )會作用在每一個元素上。 另外,當(dāng)兩個操作數(shù)都是數(shù)組的時候執(zhí)行元素對等位置計算,并最終生成一個新的數(shù)組。

對整個數(shù)組中所有元素同時執(zhí)行數(shù)學(xué)運(yùn)算可以使得作用在整個數(shù)組上的函數(shù)運(yùn)算簡單而又快速。 比如,如果你想計算多項式的值,可以這樣做:

>>> def f(x):
... return 3*x**2 - 2*x + 7
...
>>> f(ax)
array([ 8, 15, 28, 47])
>>>

NumPy 還為數(shù)組操作提供了大量的通用函數(shù),這些函數(shù)可以作為 math模塊中類似函數(shù)的替代。比如:

>>> np.sqrt(ax)
array([ 1. , 1.41421356, 1.73205081, 2. ])
>>> np.cos(ax)
array([ 0.54030231, -0.41614684, -0.9899925 , -0.65364362])
>>>

使用這些通用函數(shù)要比循環(huán)數(shù)組并使用 math 模塊中的函數(shù)執(zhí)行計算要快的多。 因此,只要有可能的話盡量選擇 NumPy 的數(shù)組方案。

底層實(shí)現(xiàn)中,NumPy 數(shù)組使用了 C 或者 Fortran 語言的機(jī)制分配內(nèi)存。 也就是說,它們是一個非常大的連續(xù)的并由同類型數(shù)據(jù)組成的內(nèi)存區(qū)域。 所以,你可以構(gòu)造一個比普通 Python 列表大的多的數(shù)組。 比如,如果你想構(gòu)造一個10,000*10,000的浮點(diǎn)數(shù)二維網(wǎng)格,很輕松:

>>> grid = np.zeros(shape=(10000,10000), dtype=float)
>>> grid
    array([[ 0., 0., 0., ..., 0., 0., 0.],
    [ 0., 0., 0., ..., 0., 0., 0.],
    [ 0., 0., 0., ..., 0., 0., 0.],
    ...,
    [ 0., 0., 0., ..., 0., 0., 0.],
    [ 0., 0., 0., ..., 0., 0., 0.],
    [ 0., 0., 0., ..., 0., 0., 0.]])
>>>

所有的普通操作還是會同時作用在所有元素上:

>>> grid += 10
>>> grid
array([[ 10., 10., 10., ..., 10., 10., 10.],
    [ 10., 10., 10., ..., 10., 10., 10.],
    [ 10., 10., 10., ..., 10., 10., 10.],
    ...,
    [ 10., 10., 10., ..., 10., 10., 10.],
    [ 10., 10., 10., ..., 10., 10., 10.],
    [ 10., 10., 10., ..., 10., 10., 10.]])
>>> np.sin(grid)
array([[-0.54402111, -0.54402111, -0.54402111, ..., -0.54402111,
        -0.54402111, -0.54402111],
    [-0.54402111, -0.54402111, -0.54402111, ..., -0.54402111,
        -0.54402111, -0.54402111],
    [-0.54402111, -0.54402111, -0.54402111, ..., -0.54402111,
        -0.54402111, -0.54402111],
    ...,
    [-0.54402111, -0.54402111, -0.54402111, ..., -0.54402111,
        -0.54402111, -0.54402111],
    [-0.54402111, -0.54402111, -0.54402111, ..., -0.54402111,
        -0.54402111, -0.54402111],
    [-0.54402111, -0.54402111, -0.54402111, ..., -0.54402111,
        -0.54402111, -0.54402111]])
>>>

關(guān)于 NumPy 有一點(diǎn)需要特別的主意,那就是它擴(kuò)展 Python 列表的索引功能 - 特別是對于多維數(shù)組。 為了說明清楚,先構(gòu)造一個簡單的二維數(shù)組并試著做些試驗:

>>> a = np.array([[1, 2, 3, 4], [5, 6, 7, 8], [9, 10, 11, 12]])
>>> a
array([[ 1, 2, 3, 4],
[ 5, 6, 7, 8],
[ 9, 10, 11, 12]])

>>> # Select row 1
>>> a[1]
array([5, 6, 7, 8])

>>> # Select column 1
>>> a[:,1]
array([ 2, 6, 10])

>>> # Select a subregion and change it
>>> a[1:3, 1:3]
array([[ 6, 7],
        [10, 11]])
>>> a[1:3, 1:3] += 10
>>> a
array([[ 1, 2, 3, 4],
        [ 5, 16, 17, 8],
        [ 9, 20, 21, 12]])

>>> # Broadcast a row vector across an operation on all rows
>>> a + [100, 101, 102, 103]
array([[101, 103, 105, 107],
        [105, 117, 119, 111],
        [109, 121, 123, 115]])
>>> a
array([[ 1, 2, 3, 4],
        [ 5, 16, 17, 8],
        [ 9, 20, 21, 12]])

>>> # Conditional assignment on an array
>>> np.where(a < 10, a, 10)
array([[ 1, 2, 3, 4],
        [ 5, 10, 10, 8],
        [ 9, 10, 10, 10]])
>>>

討論

NumPy 是 Python 領(lǐng)域中很多科學(xué)與工程庫的基礎(chǔ),同時也是被廣泛使用的最大最復(fù)雜的模塊。 即便如此,在剛開始的時候通過一些簡單的例子和玩具程序也能幫我們完成一些有趣的事情。

通常我們導(dǎo)入 NumPy 模塊的時候會使用語句 import numpy as np 。 這樣的話你就不用再你的程序里面一遍遍的敲入numpy ,只需要輸入 np 就行了,節(jié)省了不少時間。

如果想獲取更多的信息,你當(dāng)然得去 NumPy 官網(wǎng)逛逛了,網(wǎng)址是: http://www.numpy.org

矩陣與線性代數(shù)運(yùn)算

問題

你需要執(zhí)行矩陣和線性代數(shù)運(yùn)算,比如矩陣乘法、尋找行列式、求解線性方程組等等。

解決方案 NumPy 庫有一個矩陣對象可以用來解決這個問題。 矩陣類似于3.9小節(jié)中數(shù)組對象,但是遵循線性代數(shù)的計算規(guī)則。下面的一個例子展示了矩陣的一些基本特性:

>>> import numpy as np
>>> m = np.matrix([[1,-2,3],[0,4,5],[7,8,-9]])
>>> m
matrix([[ 1, -2, 3],
        [ 0, 4, 5],
        [ 7, 8, -9]])

>>> # Return transpose
>>> m.T
matrix([[ 1, 0, 7],
        [-2, 4, 8],
        [ 3, 5, -9]])

>>> # Return inverse
>>> m.I
matrix([[ 0.33043478, -0.02608696, 0.09565217],
        [-0.15217391, 0.13043478, 0.02173913],
        [ 0.12173913, 0.09565217, -0.0173913 ]])

>>> # Create a vector and multiply
>>> v = np.matrix([[2],[3],[4]])
>>> v
matrix([[2],
        [3],
        [4]])
>>> m * v
matrix([[ 8],
        [32],
        [ 2]])
>>>

可以在 numpy.linalg 子包中找到更多的操作函數(shù),比如:

>>> import numpy.linalg

>>> # Determinant
>>> numpy.linalg.det(m)
-229.99999999999983

>>> # Eigenvalues
>>> numpy.linalg.eigvals(m)
array([-13.11474312, 2.75956154, 6.35518158])

>>> # Solve for x in mx = v
>>> x = numpy.linalg.solve(m, v)
>>> x
matrix([[ 0.96521739],
        [ 0.17391304],
        [ 0.46086957]])
>>> m * x
matrix([[ 2.],
        [ 3.],
        [ 4.]])
>>> v
matrix([[2],
        [3],
        [4]])
>>>

討論

很顯然線性代數(shù)是個非常大的主題,已經(jīng)超出了本書能討論的范圍。 但是,如果你需要操作數(shù)組和向量的話,NumPy是一個不錯的入口點(diǎn)。 可以訪問 NumPy 官網(wǎng) http://www.numpy.org 獲取更多信息。

隨機(jī)選擇

問題

你想從一個序列中隨機(jī)抽取若干元素,或者想生成幾個隨機(jī)數(shù)。

解決方案

random模塊有大量的函數(shù)用來產(chǎn)生隨機(jī)數(shù)和隨機(jī)選擇元素。 比如,要想從一個序列中隨機(jī)的抽取一個元素,可以使用 random.choice()

>>> import random
>>> values = [1, 2, 3, 4, 5, 6]
>>> random.choice(values)
2
>>> random.choice(values)
3
>>> random.choice(values)
1
>>> random.choice(values)
4
>>> random.choice(values)
6
>>>

為了提取出 N 個不同元素的樣本用來做進(jìn)一步的操作,可以使用random.sample()

>>> random.sample(values, 2)
[6, 2]
>>> random.sample(values, 2)
[4, 3]
>>> random.sample(values, 3)
[4, 3, 1]
>>> random.sample(values, 3)
[5, 4, 1]
>>>

如果你僅僅只是想打亂序列中元素的順序,可以使用random.shuffle()

>>> random.shuffle(values)
>>> values
[2, 4, 6, 5, 3, 1]
>>> random.shuffle(values)
>>> values
[3, 5, 2, 1, 6, 4]
>>>

生成隨機(jī)整數(shù),請使用 random.randint()

>>> random.randint(0,10)
2
>>> random.randint(0,10)
5
>>> random.randint(0,10)
0
>>> random.randint(0,10)
7
>>> random.randint(0,10)
10
>>> random.randint(0,10)
3
>>>

為了生成0到1范圍內(nèi)均勻分布的浮點(diǎn)數(shù),使用 random.random()

>>> random.random()
0.9406677561675867
>>> random.random()
0.133129581343897
>>> random.random()
0.4144991136919316
>>>

如果要獲取 N 位隨機(jī)位(二進(jìn)制)的整數(shù),使用 random.getrandbits()

>>> random.getrandbits(200)
335837000776573622800628485064121869519521710558559406913275
>>>

討論

random模塊使用 Mersenne Twister 算法來計算生成隨機(jī)數(shù)。這是一個確定性算法, 但是你可以通過 random.seed()函數(shù)修改初始化種子。比如:

random.seed() # Seed based on system time or os.urandom()
random.seed(12345) # Seed based on integer given
random.seed(b'bytedata') # Seed based on byte data

除了上述介紹的功能,random 模塊還包含基于均勻分布、高斯分布和其他分布的隨機(jī)數(shù)生成函數(shù)。 比如, random.uniform()計算均勻分布隨機(jī)數(shù), random.gauss() 計算正態(tài)分布隨機(jī)數(shù)。 對于其他的分布情況請參考在線文檔。

random 模塊中的函數(shù)不應(yīng)該用在和密碼學(xué)相關(guān)的程序中。 如果你確實(shí)需要類似的功能,可以使用 ssl 模塊中相應(yīng)的函數(shù)。 比如, ssl.RAND_bytes() 可以用來生成一個安全的隨機(jī)字節(jié)序列。

基本的日期與時間轉(zhuǎn)換

問題

你需要執(zhí)行簡單的時間轉(zhuǎn)換,比如天到秒,小時到分鐘等的轉(zhuǎn)換。

解決方案

為了執(zhí)行不同時間單位的轉(zhuǎn)換和計算,請使用 datetime 模塊。 比如,為了表示一個時間段,可以創(chuàng)建一個timedelta 實(shí)例,就像下面這樣:

>>> from datetime import timedelta
>>> a = timedelta(days=2, hours=6)
>>> b = timedelta(hours=4.5)
>>> c = a + b
>>> c.days
2
>>> c.seconds
37800
>>> c.seconds / 3600
10.5
>>> c.total_seconds() / 3600
58.5
>>>

如果你想表示指定的日期和時間,先創(chuàng)建一個datetime實(shí)例然后使用標(biāo)準(zhǔn)的數(shù)學(xué)運(yùn)算來操作它們。比如:

>>> from datetime import datetime
>>> a = datetime(2012, 9, 23)
>>> print(a + timedelta(days=10))
2012-10-03 00:00:00
>>>
>>> b = datetime(2012, 12, 21)
>>> d = b - a
>>> d.days
89
>>> now = datetime.today()
>>> print(now)
2012-12-21 14:54:43.094063
>>> print(now + timedelta(minutes=10))
2012-12-21 15:04:43.094063
>>>

在計算的時候,需要注意的是 datetime會自動處理閏年。比如:

>>> a = datetime(2012, 3, 1)
>>> b = datetime(2012, 2, 28)
>>> a - b
datetime.timedelta(2)
>>> (a - b).days
2
>>> c = datetime(2013, 3, 1)
>>> d = datetime(2013, 2, 28)
>>> (c - d).days
1
>>>

討論

對大多數(shù)基本的日期和時間處理問題, datetime 模塊以及足夠了。 如果你需要執(zhí)行更加復(fù)雜的日期操作,比如處理時區(qū),模糊時間范圍,節(jié)假日計算等等, 可以考慮使用 dateutil 模塊

許多類似的時間計算可以使用 dateutil.relativedelta()函數(shù)代替。 但是,有一點(diǎn)需要注意的就是,它會在處理月份(還有它們的天數(shù)差距)的時候填充間隙??蠢幼钋宄?/p>

>>> a = datetime(2012, 9, 23)
>>> a + timedelta(months=1)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: 'months' is an invalid keyword argument for this function
>>>
>>> from dateutil.relativedelta import relativedelta
>>> a + relativedelta(months=+1)
datetime.datetime(2012, 10, 23, 0, 0)
>>> a + relativedelta(months=+4)
datetime.datetime(2013, 1, 23, 0, 0)
>>>
>>> # Time between two dates
>>> b = datetime(2012, 12, 21)
>>> d = b - a
>>> d
datetime.timedelta(89)
>>> d = relativedelta(b, a)
>>> d
relativedelta(months=+2, days=+28)
>>> d.months
2
>>> d.days
28
>>>

計算最后一個周五的日期

問題

你需要查找星期中某一天最后出現(xiàn)的日期,比如星期五。

解決方案

Python 的 datetime 模塊中有工具函數(shù)和類可以幫助你執(zhí)行這樣的計算。 下面是對類似這樣的問題的一個通用解決方案:

#!/usr/bin/env python
# -*- encoding: utf-8 -*-
"""
Topic: 最后的周五
Desc :
"""
from datetime import datetime, timedelta

weekdays = ['Monday', 'Tuesday', 'Wednesday', 'Thursday',
            'Friday', 'Saturday', 'Sunday']

def get_previous_byday(dayname, start_date=None):
    if start_date is None:
        start_date = datetime.today()
    day_num = start_date.weekday()
    day_num_target = weekdays.index(dayname)
    days_ago = (7 + day_num - day_num_target) % 7
    if days_ago == 0:
        days_ago = 7
    target_date = start_date - timedelta(days=days_ago)
    return target_date

在交互式解釋器中使用如下:

>>> datetime.today() # For reference
datetime.datetime(2012, 8, 28, 22, 4, 30, 263076)
>>> get_previous_byday('Monday')
datetime.datetime(2012, 8, 27, 22, 3, 57, 29045)
>>> get_previous_byday('Tuesday') # Previous week, not today
datetime.datetime(2012, 8, 21, 22, 4, 12, 629771)
>>> get_previous_byday('Friday')
datetime.datetime(2012, 8, 24, 22, 5, 9, 911393)
>>>

可選的 start_date參數(shù)可以由另外一個 datetime實(shí)例來提供。比如:

>>> get_previous_byday('Sunday', datetime(2012, 12, 21))
datetime.datetime(2012, 12, 16, 0, 0)
>>>

討論

上面的算法原理是這樣的:先將開始日期和目標(biāo)日期映射到星期數(shù)組的位置上(星期一索引為0), 然后通過模運(yùn)算計算出目標(biāo)日期要經(jīng)過多少天才能到達(dá)開始日期。然后用開始日期減去那個時間差即得到結(jié)果日期。

如果你要像這樣執(zhí)行大量的日期計算的話,你最好安裝第三方包 python-dateutil 來代替。 比如,下面是是使用 dateutil 模塊中的 relativedelta() 函數(shù)執(zhí)行同樣的計算:

>>> from datetime import datetime
>>> from dateutil.relativedelta import relativedelta
>>> from dateutil.rrule import *
>>> d = datetime.now()
>>> print(d)
2012-12-23 16:31:52.718111

>>> # Next Friday
>>> print(d + relativedelta(weekday=FR))
2012-12-28 16:31:52.718111
>>>

>>> # Last Friday
>>> print(d + relativedelta(weekday=FR(-1)))
2012-12-21 16:31:52.718111
>>>

計算當(dāng)前月份的日期范圍

問題

你的代碼需要在當(dāng)前月份中循環(huán)每一天,想找到一個計算這個日期范圍的高效方法。

解決方案

在這樣的日期上循環(huán)并需要事先構(gòu)造一個包含所有日期的列表。 你可以先計算出開始日期和結(jié)束日期, 然后在你步進(jìn)的時候使用 datetime.timedelta對象遞增這個日期變量即可。

下面是一個接受任意 datetime 對象并返回一個由當(dāng)前月份開始日和下個月開始日組成的元組對象。

from datetime import datetime, date, timedelta
import calendar

def get_month_range(start_date=None):
    if start_date is None:
        start_date = date.today().replace(day=1)
    _, days_in_month = calendar.monthrange(start_date.year, start_date.month)
    end_date = start_date + timedelta(days=days_in_month)
    return (start_date, end_date)

有了這個就可以很容易的在返回的日期范圍上面做循環(huán)操作了:

>>> a_day = timedelta(days=1)
>>> first_day, last_day = get_month_range()
>>> while first_day < last_day:
...     print(first_day)
...     first_day += a_day
...
2012-08-01
2012-08-02
2012-08-03
2012-08-04
2012-08-05
2012-08-06
2012-08-07
2012-08-08
2012-08-09
#... and so on...

討論

上面的代碼先計算出一個對應(yīng)月份第一天的日期。 一個快速的方法就是使用 datedatetime 對象的 replace()方法簡單的將days 屬性設(shè)置成1即可。 replace() 方法一個好處就是它會創(chuàng)建和你開始傳入對象類型相同的對象。 所以,如果輸入?yún)?shù)是一個 date 實(shí)例,那么結(jié)果也是一個 date 實(shí)例。 同樣的,如果輸入是一個datetime 實(shí)例,那么你得到的就是一個 datetime實(shí)例。

然后,使用 calendar.monthrange()函數(shù)來找出該月的總天數(shù)。 任何時候只要你想獲得日歷信息,那么 calendar 模塊就非常有用了。 monthrange() 函數(shù)會返回包含星期和該月天數(shù)的元組。

一旦該月的天數(shù)已知了,那么結(jié)束日期就可以通過在開始日期上面加上這個天數(shù)獲得。 有個需要注意的是結(jié)束日期并不包含在這個日期范圍內(nèi)(事實(shí)上它是下個月的開始日期)。 這個和 Python 的 slicerange 操作行為保持一致,同樣也不包含結(jié)尾。

為了在日期范圍上循環(huán),要使用到標(biāo)準(zhǔn)的數(shù)學(xué)和比較操作。 比如,可以利用 timedelta 實(shí)例來遞增日期,小于號<用來檢查一個日期是否在結(jié)束日期之前。

理想情況下,如果能為日期迭代創(chuàng)建一個同內(nèi)置的range() 函數(shù)一樣的函數(shù)就好了。 幸運(yùn)的是,可以使用一個生成器來很容易的實(shí)現(xiàn)這個目標(biāo):

def date_range(start, stop, step):
    while start < stop:
        yield start
        start += step

下面是使用這個生成器的例子:

>>> for d in date_range(datetime(2012, 9, 1), datetime(2012,10,1),
                        timedelta(hours=6)):
...     print(d)
...
2012-09-01 00:00:00
2012-09-01 06:00:00
2012-09-01 12:00:00
2012-09-01 18:00:00
2012-09-02 00:00:00
2012-09-02 06:00:00
...
>>>

這種實(shí)現(xiàn)之所以這么簡單,還得歸功于 Python 中的日期和時間能夠使用標(biāo)準(zhǔn)的數(shù)學(xué)和比較操作符來進(jìn)行運(yùn)算。

字符串轉(zhuǎn)換為日期

問題

你的應(yīng)用程序接受字符串格式的輸入,但是你想將它們轉(zhuǎn)換為 datetime 對象以便在上面執(zhí)行非字符串操作。

解決方案

使用 Python 的標(biāo)準(zhǔn)模塊 datetime 可以很容易的解決這個問題。比如:

>>> from datetime import datetime
>>> text = '2012-09-20'
>>> y = datetime.strptime(text, '%Y-%m-%d')
>>> z = datetime.now()
>>> diff = z - y
>>> diff
datetime.timedelta(3, 77824, 177393)
>>>

討論 datetime.strptime() 方法支持很多的格式化代碼, 比如 %Y代表4位數(shù)年份, %m 代表兩位數(shù)月份。 還有一點(diǎn)值得注意的是這些格式化占位符也可以反過來使用,將日期輸出為指定的格式字符串形式。

比如,假設(shè)你的代碼中生成了一個 datetime 對象, 你想將它格式化為漂亮易讀形式后放在自動生成的信件或者報告的頂部:

>>> z
datetime.datetime(2012, 9, 23, 21, 37, 4, 177393)
>>> nice_z = datetime.strftime(z, '%A %B %d, %Y')
>>> nice_z
'Sunday September 23, 2012'
>>>

還有一點(diǎn)需要注意的是, strptime() 的性能要比你想象中的差很多, 因為它是使用純 Python 實(shí)現(xiàn),并且必須處理所有的系統(tǒng)本地設(shè)置。 如果你要在代碼中需要解析大量的日期并且已經(jīng)知道了日期字符串的確切格式,可以自己實(shí)現(xiàn)一套解析方案來獲取更好的性能。 比如,如果你已經(jīng)知道所以日期格式是 YYYY-MM-DD ,你可以像下面這樣實(shí)現(xiàn)一個解析函數(shù):

from datetime import datetime
def parse_ymd(s):
    year_s, mon_s, day_s = s.split('-')
    return datetime(int(year_s), int(mon_s), int(day_s))

實(shí)際測試中,這個函數(shù)比datetime.strptime() 快7倍多。 如果你要處理大量的涉及到日期的數(shù)據(jù)的話,那么最好考慮下這個方案!

結(jié)合時區(qū)的日期操作

問題

你有一個安排在2012年12月21日早上9:30的電話會議,地點(diǎn)在芝加哥。 而你的朋友在印度的班加羅爾,那么他應(yīng)該在當(dāng)?shù)貢r間幾點(diǎn)參加這個會議呢?

解決方案

對幾乎所有涉及到時區(qū)的問題,你都應(yīng)該使用 pytz 模塊。這個包提供了 Olson 時區(qū)數(shù)據(jù)庫, 它是時區(qū)信息的事實(shí)上的標(biāo)準(zhǔn),在很多語言和操作系統(tǒng)里面都可以找到。

pytz 模塊一個主要用途是將 datetime 庫創(chuàng)建的簡單日期對象本地化。 比如,下面如何表示一個芝加哥時間的示例:

>>> from datetime import datetime
>>> from pytz import timezone
>>> d = datetime(2012, 12, 21, 9, 30, 0)
>>> print(d)
2012-12-21 09:30:00
>>>

>>> # Localize the date for Chicago
>>> central = timezone('US/Central')
>>> loc_d = central.localize(d)
>>> print(loc_d)
2012-12-21 09:30:00-06:00
>>>

一旦日期被本地化了, 它就可以