Python 秘籍手冊(全)
原文:Python Recipes Handbook
協議:CC BY-NC-SA 4.0
一、字串和文字
自從計算的早期以來,計算中使用的資料儲存在基本的文字檔案中。任何編寫過 shell 指令碼的人都非常清楚,Unix 系統及其工具是基於這樣一種假設構建的,即處理文字將是程式的大部分工作。Python 也不例外;它提供了幾個程式設計元素來幫助完成基本的文字處理任務。
首先,讓我們注意一下 Python 是如何儲存字串的。字串是不可變的列表,所以它們不能被改變。對字串內容的任何更改都需要複製到記憶體中的新位置。當您試圖最佳化程式碼的任何文字處理部分時,您必須始終牢記這一點。
1-1.串聯字串
問題
你想用一些更小的字串來構建字串。這個過程稱為串聯。
解決辦法
構建字串最簡單的方法是使用+
運算子。在這種情況下,字串被放在一起,並返回完整的新字串。
它是如何工作的
清單 1-1 展示了一個例子。
>>> new_str = "hello " + "world"
>>> print(new_str)
hello world
Listing 1-1.Basic Concatenation
這段程式碼返回字串“hello world”。如果希望兩個單詞之間有一個空格,需要顯式地給其中一個字串新增一個空格。
您也可以從一個較小字串的多個副本建立字串。這是透過使用*
運算子並乘以您想要的副本數來完成的。參見清單 1-2 中的示例。
>>> new_str = "Hello" * 3
>>> print(new_str)
HelloHelloHello
Listing 1-2.
Multiplicative Concatenation
這將返回字串“HelloHelloHello”。
當您只處理字串時,這兩個運算子工作得很好。如果您想使用其他資料型別,您可以使用上面的例子,首先將您的資料傳遞到函式str()
中。透過這種方式,您可以將數字新增到構造的字串中。清單 1-3 中顯示了一個例子。
>>> New_str = "Count = " + str(42)
>>> print(New_str)
Count = 42
Listing 1-3.Concatenating Non-Strings
1-2.比較字串
問題
您想要比較字串,檢查兩個字串是否具有相同的值,或者檢查兩個名稱是否指向同一個 string 物件。
解決辦法
有兩種方法進行比較,使用is
或使用==
。第一種是測試兩個變數名是否引用同一個物件的方法,第二種是比較每個變數的實際值的方法。
它是如何工作的
要測試相同的文字是否儲存在兩個獨立的字串中,使用類似清單 1-4 中的程式碼。
str1 = "Hello"
str2 = "World"
if str1 == str2:
print("The strings are equal")
else:
print("The strings are not equal")
Listing 1-4.Comparing Strings
此程式碼返回“字串不相等”。您可以使用任何常用的比較運算子,如!=
、<
或>.
Note
當進行大於或小於比較時,字串是逐字母比較的。另外,Python 對待大寫字母和小寫字母的方式不同。大寫字母在小寫字母之前,所以 Z 在 a 之前。
1-3.搜尋子字串
問題
你想在一個字串物件中搜尋一個子串。
解決辦法
您可以使用in
運算子來檢測子字串是否存在。您可以透過使用 string 物件的find()
方法來定位該子字串的起始索引。
它是如何工作的
在 Python 中,有一個名為in
的多型運算子,可以用來檢視一個資料元素是否存在於一個更大的資料元素集合中。當您需要檢視一個子字串是否存在於另一個 string 物件中時,這也是可行的。清單 1-5 中給出了用法。
>>> Str1 = 'This is a string'
>>> 'is' in Str1
True
>>> 'me' in Str1
False
Listing 1-5.Looking for a Substring
如您所見,它返回一個布林值,告訴您是否找到了子串。如果需要查詢子字串的位置,請使用 string 物件的 find 方法。使用上面的程式碼,您可以使用
>>> Str1.find('is')
2
>>> Str1.find('me')
-1
Listing 1-6.Finding the Index of a Substring
這段程式碼返回子字串第一個例項的索引。如果要查詢其他例項,可以包含一個開始和/或結束索引值,以便進行搜尋。因此,要找到第二個例項is
,使用清單 1-7 中的程式碼。
>>> Str1.find('is', 3)
5
Listing 1-7.Finding a Substring Beyond the First One
1-4.獲取子字串
問題
你需要從一個字串物件中獲取一個子串。
解決辦法
一旦找到子字串的索引,就可以透過複製原始字串的一部分來獲得它。這是透過使用切片符號從字串中獲取元素來實現的。
它是如何工作的
切片由開始索引和結束索引定義。要獲取字串中的第一個單詞,您可以使用清單 1-8 中顯示的任一選項。
>>> Str2 = 'One two three'
>>> Str2[0:2]
On
>>> Str2[:2]
On
>>> Str2[8:]
three
Listing 1-8.Slice Notation
Note
切片適用於 Python 中的所有列表。您還可以使用負索引值來向後計數,而不是向前計數。
1-5.替換文字匹配
問題
你需要用新的內容替換字串的一部分。
解決辦法
因為 Python 字串是不可變的,所以替換子字串需要將原始字串切碎,然後將所有內容重新連線成一個新字串。
它是如何工作的
讓我們使用上述切片和拼接的例子,並將它們放在一起,如清單 1-9 所示。
>>> str1 = "Here are a string"
>>> corrected_str1 = str1[:5] + "is" + str1[7:]
>>> print(corrected_str1)
Here is a string
Listing 1-9.Manual String Replacement
string 物件包含一個名為replace()
的方法,該方法也可以用來替換一個或多個子字串例項。您需要提交舊的子串、要替換的新子串以及要替換的例項數。參見清單 1-10 。
>>> corrected_str1 = str1.replace("are", "is", 1)
Listing 1-10.Using the replace() Function
如果省略 count,此方法將替換字串中的每個例項。
1-6.反轉字串
問題
你需要反轉一個字串物件的內容。
解決辦法
反轉字串可以透過使用切片符號以相反的順序選擇出單個字元來完成。
它是如何工作的
Python 包含了擴充套件切片的概念,它包含第三個引數來定義在列表中移動時的步幅長度。如果這個步幅長度是負的,您是在告訴 Python 向後遍歷列表。反轉字串的一行程式類似於清單 1-11 中的程式碼。
>>> str1 = "Hello World"
>>> str2 = str1[::-1]
>>> print(str2)
dlrow olleH
Listing 1-11.Reversing a String with Slices
因為這是一種特殊型別的切片,所以可以用它來獲取一個子串,並在一個命令中全部反轉。
1-7.修剪空白
問題
您需要從使用者輸入中刪除空白。
解決辦法
string 物件的strip()
方法可以用來從字串中刪除任何多餘的空白字元。
它是如何工作的
當您的程式碼需要接受來自使用者的文字輸入時,您通常需要能夠修剪掉可能存在於字串開頭或結尾的任何空白字元。如果您希望簡單地刪除字串開頭和結尾的所有空白字元,您可以使用strip()
方法。參見清單 1-12 。
>>> str1 = "Space"
>>> str2 = str1.strip()
>>> print(str2)
Space
Listing 1-12.Stripping Whitespace
如果您想從字串的開頭和結尾去掉某個特定的字元,您可以將它作為引數傳遞給方法。您可以分別使用方法lstrip()
或rstrip()
從開頭或結尾去掉不需要的字元。
1-8.改變大小寫
問題
您需要將字元的大小寫設定為全部大寫或全部小寫。
解決辦法
方法可以對內容執行大小寫更改。
它是如何工作的
處理使用者輸入時出現的另一個問題是,您可能需要將所有字元設定為大寫或小寫。這通常是對放入資料庫的資訊進行的,以簡化兩個值之間的比較。這樣可以避免前面提到的大寫和小寫字元被區別對待的問題。在這兩種情況下,這都是透過 string 物件提供的方法來完成的。參見清單 1-13 。
>>> str1 = "Hello World"
>>> print(str1.lower())
hello world
>>> print(str1.upper())
HELLO WORLD
Listing 1-13.Changing the Case of a String
Note
你可以用方法capitalize()
將一個字串大寫。
1-9.轉換為數字
問題
您需要將使用者輸入的數字轉換為數字資料型別。
解決辦法
有一些轉換函式可以將字串轉換成其他資料型別。對於數字,有int()
、float()
、long()
和complex()
函式。
它是如何工作的
這些函式接受一個字串,並返回您所請求型別的數字。該字串應該與直接在 Python 中輸入的數字文字具有相同的形式。如果字串與請求的型別轉換的預期格式不匹配,將會出現錯誤。
預設的基數是 10。您可以輸入不同的基數,以便建立不同的數字。例如,如果您正在輸入十六進位制數字,您可以使用清單 1-14 中的程式碼。
>>> hex1 = int("ae", 16)
>>> hex1
174
Listing 1-14.Type Cast Functions
可能的基數是 2 到 36。
1-10.迭代字串中的字元
問題
您需要遍歷字串物件的字元,並對每個字元應用一些處理。
解決辦法
您可以使用一個for
迴圈迭代每個字元。
它是如何工作的
如果您需要處理給定字串的每個字元,那麼您需要能夠遍歷該字串。您可以構建一個簡單的迴圈,使用索引並從列表中提取每個元素。參見清單 1-15 。
str1 = "0123456789"
for i in range(10):
print(str1[i], " and ")
Listing 1-15.Iterating Over a String
此程式碼返回文字“0 和 1 以及 2 和 3”。更 Pythonic 化的方法是使用迭代器。令人高興的是,列表自動支援迭代器方法。程式碼可以更改為清單 1-16 中的程式碼。
for i in str1:
print(i, " and ")
Listing 1-16.Using an Iterator
1-11.文字統計
問題
您需要找到字串物件的統計資料。
解決辦法
如果您對檢視給定字串的總體統計資訊感興趣,可以使用幾種不同的方法和函式來收集這類資訊。
它是如何工作的
最基本的統計是字串中的字元數,由函式len()
給出。您可以使用函式min()
和max()
找到最小和最大字元值。要獲得更詳細的資訊,您可以使用count()
方法來檢視某些字元在字串中出現了多少次。見清單 1-17 。
>>> str1 = "Hello world"
>>> len(str1)
11
>>> min(str1)
' '
>>> max(str1)
'w'
>>> str1.count('o')
2
Listing 1-17.String Statistics
1-12.編碼 Unicode
問題
您需要將字串編碼為 Unicode。
解決辦法
Python 曾經有一個unicode
資料型別用於儲存 Unicode 編碼的字串。從版本 3 開始,Python 字串文字預設儲存為 Unicode 字串。
它是如何工作的
如果您使用的是 Python 3,標準字串物件已經儲存為 Unicode。如果您使用的是 Python 2,您可以透過使用建構函式或定義 Unicode 文字來建立它們。參見清單 1-18 。
>>> ustr1 = unicode("Hello")
>>> ustr2 = u’Hello’
>>> ustr1 == ustr2
True
Listing 1-18.Using Unicode
Unicode 值實際上儲存為整數。您可以將這些值編碼成特定的編碼字串。舉個例子,
ustr1.encode("utf-8")
給你一個編碼為 UTF 8 的新字串。您還可以使用decode()
方法將這個字串解碼回普通的 Unicode 字串。
1-13.翻譯
問題
你需要翻譯字串的內容。
解決辦法
您可以將翻譯對映應用於整個字串。如果你想完成幾個替換,這是很方便的。
它是如何工作的
您可以使用字串的translate()
方法來應用這種對映。雖然您可以手動建立轉換表,但是 string 資料型別包含一個名為maketrans()
的幫助函式,它建立一個轉換表,將第一個引數中的每個字元對映到第二個引數中相同位置的字元。如清單 1-19 所示。
>>> str1 = "Hello world"
>>> translate_table = str1.maketrans("abcd", "efgh")
>>> str1.translate(translate_table)
Hello worlh
Listing 1-19.
Translating Strings
translate
方法可選地接受第二個引數,該引數包含一組要從給定字串中刪除並執行翻譯的字元。如果您希望能夠從字串中刪除一組字元,可以透過將轉換表設定為 None 來實現。這樣,你就可以用清單 1-20 中的程式碼去掉字元 l 和 w。
>>> str1.translate(str.maketrans({'l':None,'W':None}))
Heo ord
Listing 1-20.Using translate to Delete Characters
二、數字、日期和時間
電子計算機的最初目的是計算物理問題的答案。這取決於數字的有效使用和正確處理。這仍然消耗了普通計算機程式的大部分 CPU 週期。除了處理數字,在很多情況下,您還需要能夠處理日期和時間。當您需要比較不同時區或閏日的年份時,這就有點麻煩了。
2-1.建立整數
問題
你需要建立整數。
解決辦法
建立整數有幾種方法。最簡單的是簡單地寫一個整數。還有一些建立函式,允許你用任意的基數來建立任意的整數。在 Python 中,整數是無限範圍內的數字,受限於可用的記憶體量。
它是如何工作的
清單 2-1 展示了一個例子。
>>> a = 123456789
>>> a.bit_length()
27
Listing 2-1.Integer Literals
這段程式碼建立了一個新的 integer 物件,透過變數a
來訪問。您可以使用方法bit_length()
來檢視使用了多少位來儲存這個整數。整數是不可變的,所以如果你試圖透過數學運算來改變它們,你最終會得到一個新的整數。
在清單 2-2 中,您可以看到如何使用基數 10 以外的東西來建立整數。
>>> b = int('110',2)
>>> b
6
Listing 2-2.Integer Object Instantiation
2-2.建立浮點
問題
你需要建立浮點數。
解決辦法
與整數一樣,您可以建立浮點文字,也可以使用函式float()
。
它是如何工作的
使用浮點文字時,需要包含小數點或指數。參見清單 2-3 中的一些例子。
>>> b = 1.
>>> b
1.0
>>> c = 1e2
>>> c
100.0
Listing 2-3.Using Float Literals
浮點的實際儲存表示依賴於實現,並且在不同的架構上是不同的。
2-3.將浮點數舍入為整數
問題
你想把浮點數轉換成整數。
解決辦法
有兩種方法可以將浮點數轉換成整數。您可以將數字截斷到小數點之前的部分,也可以將數字四捨五入到最接近的整數值。
它是如何工作的
可以使用和上面提到的相同的int()
函式。當您將一個浮點作為輸入傳遞給它時,它會將數字向 0 截斷。參見清單 2-4 。
>>> c = 1234.567
>>> int(c)
1234
Listing 2-4.Truncating Floating Point Numbers
在math
模組中有一個函式可以將浮點數截斷成整數部分。它叫做math.trunc()
,清單 2-5 顯示瞭如何使用它。
>>> import math
>>> math.trunc(c)
1234
Listing 2-5.Using math.trunc()
如你所見,它基本上去掉了小數部分,給你剩下的部分。如果您需要得到一個取整的值,檢視清單 2-6 。
>>> round(c)
1235
Listing 2-6.Rounding Floating Point Numbers
如果有必要,您可以四捨五入到某個固定的小數位數,如清單 2-7 所示。
>>> round(c, 1)
1234.6
Listing 2-7.Rounding to One Decimal Place
2-4.格式化數字
問題
你需要得到一個數字的字串表示。
解決辦法
內建函式format()
可以用來輸出數字的各種字串表示。
它是如何工作的
format
函式採用一種格式規範,定義如何將數字轉換成字串。這種字串的一般格式如清單 2-8 所示。
[sign][width][.precision][type]
Listing 2-8.Format Specification
如果符號是+
,你的數字會一直有一個前導符號。如果是-
,只有數字是負數才會加前導符號。如果您使用空格,如果數字是負數,您將得到一個前導減號,如果數字是正數,您將得到一個前導空格。
寬度值定義了總數的最小大小。如果寬度大於數字的大小,您將獲得前導空格來填充到正確的大小。精度定義了在顯示數字時應該使用多少個小數位。當然,這隻對浮點數有效。
該型別可以是幾種不同的可能性之一:
| 型別 | 意義 | | --- | --- | | `B` | 二進位制格式 | | `D` | 十進位制整數 | | `O` | 八進位制整數 | | `x` | 十六進位制格式,a-f 使用小寫字母 | | `X` | 十六進位制格式,A-F 使用大寫字母 | | `n` | 這與`d`相同 | | 沒有人 | 這與`d`相同 |對於浮點數,有一個完整的其他型別可供選擇:
| 型別 | 意義 | | --- | --- | | `e` | 指數格式,用`e`標記指數。預設精度為 6。 | | `E` | 指數格式,用`E`標記指數。預設精度為 6。 | | `f` | 預設精度為 6 的定點。 | | `F` | 與`f`相同,只是 nan 轉換為 NAN,inf 轉換為 INF。 | | `g` | 常規格式,其中數字被舍入到給定的精度。如果數字太大,則以指數形式顯示,用`e`標記指數。預設精度為 6。 | | `G` | 這與`g`相同,只是指數標有`E`。 | | `n` | 號碼。這與`g`相同,只是它使用當前的區域設定來獲取正確的分隔符。 | | `%` | 百分比,其中數字乘以 100,以`f`格式顯示,帶有百分比符號。 | | 沒有人 | 這個和`g`一樣。 |示例如清單 2-9 所示。
>>> format(c)
'1234.567'
>>> format(c, '+.2f')
'+1234.57'
>>> format(.25, '%')
'25.000000%'
Listing 2-9.Formatting Numbers
2-5.使用任意精度的數字
問題
您需要處理任意大小和精度的浮點數。
解決辦法
Python 提供了一個名為decimal
的模組來處理使用者定義的精度數。
它是如何工作的
decimal
模組提供了一個名為Decimal
的新物件,它將浮點數的儲存和操作從底層架構中抽象出來。見清單 2-10 。
>>> from decimal import *
>>> getcontext()
Context(prec=28, rounding=ROUND_HALF_EVEN, Emin=-999999, Emax=999999, capitals=1, clamp=0, flags=[], traps=[InvalidOperation, DivisionByZero, Overflow])
>>> getcontext().prec = 10
Listing 2-10.Importing the Decimal Object and Setting Precision
清單 2-10 顯示瞭如何設定decimal
模組中使用的精度。清單 2-11 展示瞭如何使用Decimal
類。
>>> a = Decimal(10.5)
>>> a
Decimal('10.5')
>>> 2*a
Decimal('21.0')
Listing 2-11.Using the Decimal Class
2-6.生成隨機數
問題
為了在程式碼中引入隨機化,您需要生成隨機數。
解決辦法
Python 中的random
模組提供了為各種分佈生成偽隨機數的函式。
它是如何工作的
在沒有非常專業的硬體的情況下,所有隨機數都是使用一類稱為偽隨機數生成器(PRNGs)的函式來生成的。可用的更好的演算法之一是稱為 Mersenne twisters 的函式類。Python random
模組使用 Mersenne twisters 來生成隨機數。使用當前系統時間作為種子來初始化生成器,參見清單 2-12 。
>>> import random
>>> random.seed()
Listing 2-12.Initializing the Random Module
您可以透過呼叫random.random()
獲得 0.0 到 1.0 之間的下一個隨機浮點數,如清單 2-13 所示。
>>> random.random()
0.35060766413719124
Listing 2-13.Getting the Next Random Number
您還可以透過使用random.choice()
函式在許多可能性中進行隨機選擇,如清單 2-14 所示。
>>> items = [1, 2, 3, 4]
>>> random.choice(items)
3
Listing 2-14.Making a Random Choice
2-7.獲取當前日期和時間
問題
您需要從系統中獲取當前日期和時間。
解決辦法
Python 提供了一個名為datetime
的模組,該模組提供了處理日期、時間和時區的物件。
它是如何工作的
datetime
模組提供了一個datetime
類,包含所有的日期和時間資訊,以及任何時區資訊。清單 2-15 展示瞭如何獲取當前日期和時間。
>>> import datetime
>>> curr_datetime = datetime.datetime.now()
>>> curr_datetime.year
2016
>>> curr_datetime.weekday()
2
Listing 2-15.Getting the Current Date and Time
在datetime
物件中有許多可用的幫助函式和屬性,允許您以任何需要的方式解析日期和時間資訊。
如果您只需要日期或時間部分,那麼可以使用date
和time
類。有datetime
類的幫助器方法,即date()
和time()
,它們返回適當型別的物件。清單 2-16 展示瞭如何獲取當前日期和時間的time
和date
物件。
>>> import datetime
>>> curr_datetime = datetime.datetime.now()
>>> curr_date = curr_datetime.date()
>>> curr_time = curr_datetime.time()
Listing 2-16.Getting Objects for the Current Date and Time
2-8.計算日期/時間差異
問題
您需要能夠找出兩個時間和/或兩個日期之間的差異。
解決辦法
Python 提供了作為datetime
模組一部分的timedelta
物件。
它是如何工作的
您可以建立一個代表某個日期或時間跨度的timedelta
物件。如果你有兩個datetime
物件,你可以將它們相減得到一個timedelta
物件,如清單 2-17 所示。
>>> time1 = datetime.datetime.now()
>>> time2 = datetime.datetime.now()
>>> timediff = time2 - time1
>>> timediff.days
0
>>> timediff.seconds
7
>>> timediff.total_seconds()
7.532031
Listing 2-17.Finding the Difference Between Two Datetimes
如果你想建立一個固定的timedelta
,比方說一週,你想新增到一個給定的datetime
物件,你可以建立它並使用它,如清單 2-18 所示。
>>> timediff = datetime.timedelta(days=7)
>>> time1 = datetime.datetime.now()
>>> time1.day
10
>>> time2 = time1 + timediff
>>> time2.day
17
Listing 2-18.Creating Timedeltas
2-9.格式化日期和時間
問題
出於顯示目的,您需要生成datetime
物件的字串表示。
解決辦法
datetime
物件有一個字串格式方法,可以生成一個由規範字串定義的字串。
它是如何工作的
方法strftime()
獲取一個規範字串,並返回一個字串,其中替換了相關的值。清單 2-19 中顯示了一個基本示例。
>>> time3 = datetime.datetime.now()
>>> time3.strftime("%A %d. %B %Y %I:%M%p")
'Wednesday, 10\. February 2016 09:39AM'
Listing 2-19.Formatting a Datetime String
可能的格式選項如下:
| 管理的 | 意義 | | --- | --- | | `%a` | 作為區域縮寫名稱的工作日 | | `%A` | 作為區域全名的工作日 | | `%w` | 以 0 到 6 之間的數字表示的星期幾 | | `%d` | 以零填充數字表示的月份中的某一天 | | `%b` | 作為區域縮寫名稱的月份 | | `%B` | 月份作為區域設定全名 | | `%m` | 以零填充數字表示的月份 | | `%y` | 以零填充的兩位數表示的年份 | | `%Y` | 以零填充的四位數表示的年份 | | `%H` | 小時(24 小時制)作為零填充數字 | | `%I` | 以零填充數字表示的小時(12 小時制) | | `%p` | 等同於 AM 或 PM 的區域設定 | | `%M` | 以零填充數字表示的分鐘 | | `%S` | 秒作為零填充數字 | | `%f` | 微秒作為零填充數字 | | `%z` | UTC 偏移量,形式為+HHMM 或–HHMM | | `%Z` | 時區名稱 | | `%j` | 以零填充的三位數表示的一年中的某一天 | | `%U` | 一年中的第一週,將星期日作為一週的第一天 | | `%W` | 一年中的第一週,使用星期一作為一週的第一天 | | `%c` | 日期和時間的區域設定的適當表示 | | `%x` | 區域設定對日期的適當表示 | | `%X` | 區域設定對時間的適當表示 | | `%%` | 文字% |2-10.從字串中讀取日期和時間
問題
您需要獲取使用者輸入並將其轉換成一個datetime
物件。
解決辦法
datetime
類可以基於輸入字串例項化一個新物件。
它是如何工作的
如果你分別接受日期和時間的每個元素,你可以在datetime
類中直接使用它們,如清單 2-20 所示。
>>> date4 = datetime.datetime(year=1999, month=9, day=21)
>>> date4.weekday()
1
Listing 2-20.Creating a Datetime Object
如果您有一個完整的字串,使用您自己設計的格式,您可以在strptime()
函式中使用它以及一個格式規範字串。清單 2-21 中給出了一個簡單的例子。
>>> date5 = datetime.datetime.strptime("1999-09-21", "%Y-%m-%d")
>>> date5.weekday()
1
Listing 2-21.Using a Format String
格式規範字串採用與上面的strftime()
函式相同的符號。
三、迭代器和生成器
通常,您需要處理來自某個來源的一些資料序列。在 Python 中實現這一點的方法是使用迭代器。標準 Python 中許多可用的資料型別都包含一個可以使用的 iterable 介面。對於那些沒有的,您可以建立一個生成器,然後提供一個 iterable 介面。
3-1.遍歷列表的內容
問題
你想遍歷一個列表的內容。
解決辦法
雖然列表是可迭代的,但是您需要使用iter
函式來訪問相關的迭代器。
它是如何工作的
清單 3-1 展示瞭如何訪問可迭代資料型別的相關迭代器。
>>> my_list = [1, 2, 3, 4]
>>> new_iter = iter(my_list)
>>> new_iter
<list_iterator at 0x1ffb0b8e470>
Listing 3-1.Accessing an Iterator
您可以使用傳統的迭代器技術來使用這個新的iterator
物件,如清單 3-2 所示。
>>> next(new_iter)
1
>>> next(new_iter)
2
Listing 3-2.Using an Iterator
一旦清空了iterator
,就會引發一個異常,如清單 3-3 所示。
>>> next(new_iter)
4
>>> next(new_iter)
---------------------------------------------------------------------------
StopIteration Traceback (most recent call last)
<ipython-input-13-ed6082c80a14> in <module>()
----> 1 next(new_iter)
StopIteration:
Listing 3-3.Getting a StopIteration Exception
3-2.提取迭代器的內容
問題
您需要列舉一個迭代器來檢視它包含了哪些元素。
解決辦法
enumerate
內建函式可以接受一個iterable
物件,並返回一個包含計數和值的元組列表。
它是如何工作的
enumerate
內建函式將一個iterable
物件作為輸入,返回由一個計數加一個值組成的元組。實際返回的enumerate
物件本身是可迭代的,所以你可以像使用其他迭代器一樣使用它。清單 3-4 中顯示了一個例子。
>>> pets = ['dogs', 'cats', 'lizards', 'pythons']
>>> pet_enum = enumerate(pets)
>>> next(pet_enum)
(0, 'dogs')
>>> next(pet_enum)
(1, 'cats')
Listing 3-4.Iterating Over an Enumerator
預設情況下,計數從 0 開始。您可以透過使用 start 引數來改變這一點,如清單 3-5 所示。
>>> pet_enum2 = enumerate(pets, start=5)
>>> next(pet_enum2)
(5, 'dogs')
Listing 3-5.Enumerating with a Different Count Start
如果您需要立即獲得所有列舉值以進行進一步處理,您可以使用清單 3-6 中所示的程式碼建立一個元組列表。
>>> pet_list = list(enumerate(pets))
>>> pet_list
[(0, 'dogs'), (1, 'cats'), (2, 'lizards'), (3, 'pythons')]
Listing 3-6.Making an Enumerated List
3-3.過濾迭代器
問題
您只需要從迭代器中過濾出選定的專案。
解決辦法
內建的filter
函式可以選擇性地只返回那些對於某些過濾函式為真的元素。
它是如何工作的
內建的filter
函式以過濾函式為引數。這個過濾函式應該為您感興趣的迭代器元素返回 true。在清單 3-7 中,您會看到一個返回從 0 到 9 的奇數的例子。
>>> odd_nums = filter(lambda x: x%2, range(10))
>>> next(odd_nums)
1
>>> next(odd_nums)
3
Listing 3-7.Getting the Odd Numbers Below 10
如您所見,filter
返回了一個迭代器,您可以以這種方式使用它。如果你一次需要所有的元素,你可以使用list
函式,如清單 3-8 所示。
>>> odd_list = list(filter(lambda x: x%2, range(10)))
>>> odd_list
[1, 3, 5, 7, 9]
Listing 3-8.Getting a List of Odd Numbers
如果要使用負片濾鏡,需要使用itertools
包。它包括filterfalse
函式,該函式返回那些為某些過濾函式返回 false 的元素。清單 3-9 展示瞭如何使用它來獲得所有的偶數。
>>> import itertools
>>> even_list = list(itertools.filterfalse(lambda x: x%2, range(10)))
>>> even_list
[0, 2, 4, 6, 8]
Listing 3-9.Getting a List of Even Numbers
對於這個例子,我們將忽略關於 0 是否是偶數的爭論。
3-4.迭代檔案的內容
問題
您需要遍歷檔案的內容進行處理。
解決辦法
open
函式返回一個檔案物件,可以逐行迭代進行處理。
它是如何工作的
從open
函式返回的檔案物件是一個可迭代物件。迭代內容的通常方式是在一個for
迴圈中,如清單 3-10 所示。
>>> file1 = open('file.csv')
>>> for line in file1:
. . . : print(line)
. . ..:
1,one
2,two
3,three
4,four
Listing 3-10.Looping Over a File
然而,返回的 file 物件實際上是一個 iterable 函式,所以您可以像使用其他迭代器一樣使用它。清單 3-11 顯示了一個例子。
>>> file1 = open('file.csv')
>>> next(file1)
'1,one\n'
>>> next(file1)
'2,two\n'
Listing 3-11.Iterating Over a File
3-5.對沒有迭代器的資料進行迭代
問題
您需要建立一個不可迭代的資料的可迭代版本。
解決辦法
Python 的許多內建資料結構已經是可迭代的,因此對生成器的需求較少。但是,當您確實需要一個生成器時,通常沒有其他可用的解決方案。
它是如何工作的
本質上,任何將控制權交還給呼叫它的部分的函式都是生成器。Python 理解您打算在使用yield
語句時建立一個生成器。它會自動儲存函式在執行yield
語句時的狀態,這樣當next()
被呼叫時你就可以返回到該狀態。清單 3-12 顯示了一個生成方塊序列的簡單例子。
def squares(value=0):
while True:
value = value + 1
yield (value-1)*(value-1)
>>> generator = squares()
>>> next(generator)
0
>>> next(generator)
1
>>> next(generator)
4
>>> next(generator)
9
Listing 3-12.Generating the Sequence of Squares
Python 3.3 中的新特性是yield from
語句。這是建立生成器函式的一種方式,它使用其他迭代器來生成所需的值。例如,您可以建立一個生成器,它可以計數到某個值,然後再返回,如清單 3-13 所示。
def up_down(value=1):
yield from range(1, value, 1)
yield from range(value, 0, -1)
>>> list(up_down(3))
[1, 2, 3, 2, 1]
Listing 3-13.Generating a Count-Up Count-Down Function
3-6.建立迭代器的標準類
問題
在程式設計時,有幾種情況下您可能有最好作為某種型別的迭代器實現的資料結構。換句話說,您需要建立迭代器的一個標準類。
解決辦法
itertools
模組提供了大量常用迭代器類別的選擇,您可以在許多情況下使用它們。
它是如何工作的
迭代器有幾大類,在很多情況下非常有用。例如,清單 3-14 展示瞭如何製作一個累加器。
>>> import itertools
>>> accumulator = itertools.accumulate(range(10))
>>> next(accumulator)
0
>>> next(accumulator)
1
>>> next(accumulator)
3
>>> next(accumulator)
6
Listing 3-14.Creating an Accumulator
作為一個更復雜的例子,您可以用清單 3-15 中的程式碼得到兩個小於 5 的數字的所有組合。
>>> list(itertools.combinations(range(5), 2))
[(0,1),
(0, 2),
(0, 3),
(0, 4),
(1, 2),
(1, 3),
(1, 4),
(2, 3),
(2, 4),
(3, 4)]
Listing 3-15.Generating Combinations of Pairs
四、檔案和輸入輸出
對於任何型別的資料處理,檔案是絕對必要的。很快就會出現資訊太多而無法讓使用者手動輸入的情況。您需要能夠直接從各種型別的檔案中匯入資訊。
在本章中,你將會看到為了讀寫檔案你需要處理的最常見的任務。您還將看到如何處理其他形式的輸入和輸出。
4-1.複製檔案
問題
您需要將檔案從一個位置複製到另一個位置。
解決辦法
shutil
模組中有兩個函式可用於複製檔案。
它是如何工作的
複製檔案涉及檔案的兩個部分,所述檔案的實際內容和描述該檔案的後設資料。清單 4-1 展示瞭如何只複製檔案的內容。
>>> import shutil
>>> new_file_path = shutil.copy('file1.txt', 'file2.txt')
Listing 4-1.Copying File Contents
這會將file1.txt
複製到file2.txt
,將file2.txt
的路徑返回到變數new_file_path
。如果目錄名作為第二個引數給出,那麼檔案將被複制到這個目錄中,並使用原始檔名作為新檔名。該功能還將保留檔案的許可權。如果file1.txt
是一個符號連結,這個函式呼叫將把file2.txt
建立為一個單獨的檔案。如果您想實際建立一個符號連結的副本,您可以使用清單 4-2 中的例子。
>>> shutil.copy('file1.txt', 'file2.txt', follow_symlinks=False)
Listing 4-2.Copying Symlinks
如果想保留更多與檔案相關的後設資料,可以使用copy2()
,如清單 4-3 所示。
>>> shutil.copy2('file1.txt', 'file2.txt')
Listing 4-3.Copying File Contents Plus Metadata
可以複製的後設資料量因作業系統和平臺而異。它會盡可能多的複製,最壞的情況是隻能做和copy()
一樣多的工作。注意copy2()
永遠不會返回失敗。它也接受follow_symlinks
引數,就像copy()
一樣。
4-2.移動檔案
問題
您需要將檔案從一個位置移動到另一個位置。這也是重新命名檔案的方法,將檔案從一個檔名移動到另一個檔名。
解決辦法
因為重新命名和移動檔案對檔案系統來說本質上是一樣的,所以您可以使用shutils.move()
或os.rename()
來獲得相同的結果。
它是如何工作的
使用move
功能時,根據源和目的地的位置有不同的行為。如果它們都在同一個檔案系統上,則使用函式os.rename()
。否則,使用複製功能將檔案複製到目標位置,然後刪除原始檔。預設的複製函式是copy2
,但是你可以提交一些其他的函式,如清單 4-4 所示。
>>> import shutil
>>> shutil.move('file1.txt', 'dir2', copy_function=copy)
Listing 4-4.Moving a File
複製函式需要接受一個源和一個目的地,以便被move
函式使用。
如果檔案在同一個檔案系統上,您可以直接使用rename
函式,如清單 4-5 所示。
>>> import os
>>> os.rename('file1.txt', 'dir2')
Listing 4-5.Renaming a File
需要注意的主要事項是,如果目標已經存在,並且您有許可權寫入它,它將被源自動替換。
4-3.讀取和寫入文字檔案
問題
您需要開啟、讀取和寫入文字檔案。
解決辦法
您可以使用提供的內建open
函式開啟檔案,然後使用read
和write
方法讀取和寫入檔案。
它是如何工作的
在 Python 中,訪問檔案是透過檔案描述符物件完成的。這是呼叫內建open
函式返回的物件,如清單 4-6 所示。
>>> fd1 = open('file1.txt')
>>> entire_file = fd1.read()
Listing 4-6.Opening a Text File for Reading
檔案描述符的read
方法將讀入檔案的全部內容。這通常不是你想做的。為了讀入檔案的一部分,可以傳遞一個大小引數,如清單 4-7 所示。
>>> chunk1 = fd1.read(100)
Listing 4-7.Reading the First 100 Bytes of a File
逐行讀入資料是如此常見,以至於提供了一種方法來做到這一點。在清單 4-8 中,您可以看到如何在一行中讀取,或者如何遍歷整個檔案。
>>> line1 = fd1.readline()
OR
>>> for line in fd1:
>>> do_my_process(line)
Listing 4-8.Reading a File Line by Line
如果檔案不太大,您可以將全部內容讀入一個列表,其中每個元素都是檔案中的一行。清單 4-9 提供了一個例子。
>>> file_list = fd1.readlines()
>>> first_line = file_list[0]
Listing 4-9.Reading a File into a List
編寫一個文字檔案只需要一些小的改動。當呼叫open
函式時,預設情況下你將開啟只讀檔案。為了開啟檔案進行寫入,您需要使用不同的模式。感興趣的模式如下:
4-4.讀取和寫入 XML 檔案
問題
您需要讀入並處理 XML 檔案,然後寫出結果。
解決辦法
Python 標準庫包括一個 XML 解析器,它可以生成一個元素樹,您可以使用它。
它是如何工作的
Python 標準庫包括一個ElementTree
類,它提供了一種處理 XML 結構化資料的簡化方法。首先,您需要用清單 4-10 中的程式碼開啟一個 XML 檔案。
>>> import xml.etree.ElementTree as ET
>>> my_tree = ET.parse(‘my_data.xml’)
>>> root = my_tree.getroot()
Listing 4-10.Opening an XML File
一旦有了根元素,就可以查詢它並獲得標籤和屬性值,如清單 4-11 所示。
>>> root.tag
'tag value'
>>> root.attrib
{ }
Listing 4-11.Looking at Element Attributes
您可以輕鬆地遍歷任何給定元素的子元素。例如,清單 4-12 展示瞭如何遍歷根元素的所有子元素。
for child in root:
# look at the tag
print(child.tag)
Listing 4-12.Iterating Through the Children of the Root Element
元素也可以作為列表來訪問。這意味著您可以使用列表符號,如清單 4-13 所示。
>>> child1 = root[0]
Listing 4-13.Getting the First Child of an Element
使用ElementTree
類修改現有的 XML 檔案或建立一個新檔案也非常容易。可以直接改變元素的文字值,可以使用set()
方法設定元素屬性。然後,您可以使用write()
方法儲存更改。清單 4-14 展示瞭如何建立一個全新的 XML 檔案。
>>> a = ET.Element(‘a’)
>>> b = ET.SubElement(a, ‘b’)
>>> c = ET.SubElement(a, ‘c’)
>>> a.write(‘new_file.xml’)
Listing 4-14.Creating a New XML File
4-5.建立目錄
問題
您需要建立一個新目錄來寫出檔案。
解決辦法
Python 包含了一個名為pathlib
的新模組,它提供了一種物件導向的路徑處理方式。
它是如何工作的
處理路徑的核心類是Path
。您需要首先建立一個新的Path
物件並設定新的目錄名,如清單 4-15 所示。然後可以呼叫mkdir()
方法在檔案系統上建立實際的新目錄。
>>> from pathlib import Path
>>> p = Path('.')
>>> p.joinpath('subdir1')
>>> p.mkdir()
Listing 4-15.Creating a New Subdirectory
4-6.監視目錄的變化
問題
您需要監視一個目錄,並在發生變化時進行註冊,比如建立了一個新檔案。
解決辦法
Path
類包含了一個檢查目錄或檔案詳細屬性的方法。
它是如何工作的
要檢查當前目錄中是否發生了任何更改,您需要用清單 4-16 中的程式碼獲取當前狀態。
>>> import pathlib
>>> p = pathlib.Path('.')
>>> modified_time = p.stat().st_mtime
Listing 4-16.Finding the Status of the Current Directory
然後,您可以迴圈檢視這個最後修改時間是否已經更改。如果是這樣,那麼您知道目錄已經更改。如果您需要知道變更是什麼,那麼您需要建立一個內容列表,並在變更註冊前後進行比較。您可以使用glob
方法生成當前所有內容的列表,如清單 4-17 所示。
>>> import pathlib
>>> dir_list = sorted(pathlib.Path('.').glob('*'))
Listing 4-17.Getting the Contents of a Directory
4-7.遍歷目錄中的檔案
問題
為了處理一組檔案,您需要遍歷目錄的內容。
解決辦法
Path
類包含一個迭代內容的函式,給你一個子Path
物件的列表。
它是如何工作的
如果想遍歷當前目錄的內容,可以使用清單 4-18 中的程式碼。
import pathlib
p = pathlib.Path('.')
for child in p.iterdir():
# Do something with the child object
my_func(child)
Listing 4-18.Iterating Over the Contents of the Current Directory
如果您只想處理檔案,您需要包含一個檢查,如清單 4-19 所示。
import pathlib
for child in pathlib.Path('.').iterdir():
if child.is_file():
my_func(child)
Listing 4-19.Iterating Over the Files in the Current Directory
4-8.儲存資料物件
問題
您需要儲存 Python 物件,以便將來在另一個 Python 程式執行中使用。
解決辦法
Pickling objects 是序列化 Python 物件以便以後重用的標準方式。
它是如何工作的
Python 標準庫包括模組pickle
。您需要用常用的open
函式開啟一個檔案,將其傳遞給 pickle 函式。當您開啟檔案時,您需要記住包括二進位制標誌。清單 4-20 展示瞭如何將一個 Python 物件打包到一個檔案中。
>>> import pickle
>>> file1 = open('data1.pickle', 'wb')
>>> pickle.dump(data_obj1, file1)
Listing 4-20.Pickling a Python Object
為了以後重用這些資料,您可以使用清單 4-21 中的程式碼將其重新載入到 Python 中。
>>> file2 = open('data1.pickle', 'rb')
>>> data_reload = pickle.load(file2)
Listing 4-21.Loading a Pickled Object
完成後,不要忘記關閉檔案控制代碼。
4-9.壓縮檔案
問題
你需要壓縮檔案以節省空間。
解決辦法
標準庫中有許多模組可以幫助您處理 zip、gzip、bzip2、lzma 和 tar 檔案。
它是如何工作的
首先,讓我們看看如何處理 zip 檔案。處理壓縮檔案的第一步是開啟它們。這類似於 Python 中的open
函式,如清單 4-22 所示。
>>> import zipfile
>>> my_zip = zipfile.ZipFile('my_file.zip', mode='r')
Listing 4-22.Opening a Zip File
該模式的使用方式與open
功能相同。要讀取當前存在的 zip 檔案,可以使用模式'r'
。如果你想建立一個新的 zip 檔案,你可以使用模式'w'
。您也可以使用模式'a'
修改現有的 zip 檔案。
您可以使用write
方法將檔案新增到 zip 檔案中,如清單 4-23 所示。
>>> import zipfile
>>> my_zip = zipfile.ZipFile(‘archive1.zip’, mode=’w’)
>>> my_zip.write(‘file1.txt’)
>>> my_zip.close()
Listing 4-23.Adding a File to a Zip Archive
要從現有的 zip 存檔中提取檔案,請使用清單 4-24 中所示的程式碼。
>>> import zipfile
>>> my_zip = zipfile.ZipFile(‘archive1.zip’, mode=’r’)
>>> my_zip.extract(‘file1.txt’)
>>> my_zip.close()
Listing 4-24.Extracting One File from a Zip Archive
你可以用方法extractall()
提取一切。如果您不知道給定的歸檔中有哪些檔案,您可以使用方法namelist()
獲得一個列表。
如果您需要直接處理 zip 存檔的內容,您可以從存檔中讀取位元組和向存檔中寫入位元組。一旦您開啟了 zip 檔案,您就可以使用清單 4-25 中的程式碼來讀寫歸檔檔案
>>> import zipfile
>>> my_zip = zipfile.ZipFile(‘file1.zip’, ‘a’)
>>> data_in = my_zip.read(‘data1.txt’)
>>> my_zip.write(‘data2.txt’, data_out)
>>> my_zip.close()
Listing 4-25.Reading and Writing Bytes from a Zip Archive
與處理多個檔案的壓縮存檔相反,gzip
和bzip2
模組都處理單個檔案的壓縮。要開啟任何一種型別,使用清單 4-26 中的樣板程式碼。
>>> import gzip
>>> my_gzip = gzip.GzipFile(‘data1.txt.gz’)
>>> import bz2
>>> my_bzip = bz2.BZ2File(‘data2.txt.bz’)
Listing 4-26.Opening Gzip or Bzip2 Files
在這兩種情況下,您都會得到一個實現BufferedIOBase
的物件。然後,您可以讀寫和操作壓縮檔案中的資料。建立新的 gzip 或 bzip2 物件時,請記住使用適當的模式。
五、Pandas 的 Python 資料分析
Python 真正大的增長領域之一是在科學領域,其中資料分析是一個巨大的組成部分。令人高興的是,Python 包含了一個用於資料分析的瑞士軍隊工具,即pandas
包,它可以透過 pip 從 PyPi 儲存庫中安裝。pandas
提供了很多資料處理和資料處理工具,如果你來自 R 背景,你可能已經習慣了。引入了幾種新的資料型別,以及以非常高效的方式處理實際資料的附加功能。它透過構建由numpy
包提供的功能來實現這種效率。
5-1.使用 1D 資料
問題
您需要處理一維資料,如陣列。
解決辦法
pandas 包含了一種新的資料型別,稱為Series
,用於一維資料。
它是如何工作的
一旦匯入了pandas
包,就可以使用Series
建構函式獲取一個已經存在的資料物件,比如一個列表,並將其轉換成pandas
可以處理的格式。清單 5-1 展示瞭如何轉換一個基本列表。
>>> import pandas as pd
>>> data = [1,2,3,4]
>>> data_pd = pd.Series(data)
Listing 5-1.Converting a List into a Series
您可以選擇包含一個索引陣列來索引值。在清單 5-1 的例子中,索引只是資料陣列中的數字索引。此外,預設情況下,資料留在原處。如果在建立Series
物件時需要建立資料的新副本,可以包含引數copy=True. pandas
來推斷新Series
物件使用的資料型別。可以用dtype
引數顯式設定。可能的值來自numpy
。清單 5-2 展示瞭如何將清單 5-1 中的資料視為浮點。
>>> data_pd.dtype
dtype('int64')
>>> import numpy as np
>>> data_pd2 = pd.Series(data, dtype=np.float64)
>>> data_pd2.dtype
dtype('float64')
Listing 5-2.Explicitly Setting the dtype
所有常用的運算子都被過載,以便與Series
物件一起使用,如清單 5-3 所示。
>>> 2 * data_pd
0 2
1 4
2 6
3 8
dtype: int64
Listing 5-3.Basic Arithmetic with Series
訪問元素使用與列表相同的語法,如清單 5-4 所示。
>>> data_pd[2]
3
>>> data_pd[3] = 9
Listing 5-4.Accessing Series Data
5-2.使用 2D 資料
問題
你需要處理二維資料。
解決辦法
pandas
包括一個名為DataFrame
的最佳化物件,支援二維資料集。
它是如何工作的
pandas
包含一個名為DataFrame
的新物件,它為 2D 資料集建立一個物件。您可以從列表詞典或詞典列表中建立新的DataFrame
。清單 5-5 展示瞭如何從一個列表字典中建立一個新的DataFrame
。
>>> d1 = {'one' : [1,2,3,4], 'two' : [9,8,7,6]}
>>> df1 = pd.DataFrame(d1)
>>> df1
One two
0 1 9
1 2 8
2 3 7
3 4 6
Listing 5-5.Creating a DataFrame
同樣,標準算術運算子被過載,以便與DataFrames
一起使用。既然有了兩個維度,訪問元素就有點複雜了。預設情況下,定址是基於列的。這意味著您可以使用適當的標籤直接訪問給定的列,如清單 5-6 所示。
>>> df1['one']
0 1
1 2
2 3
3 4
Name: one, dtype: int64
>>> df1['one'][2]
3
Listing 5-6.Accessing DataFrame
Columns
如果想按行訪問資料,需要使用DataFrame
的iloc
或loc
屬性,如清單 5-7 所示。
>>> df1.loc[1]
one 2
two 8
Name: 1, dtype: int64
>>> df1.loc[1][1]
8
Listing 5-7.Accessing DataFrame Rows
5-3.使用 3D 資料
問題
你需要用pandas
處理三維資料集。
解決辦法
pandas
包含一個名為Panel
的新物件,可以有效地處理三維資料集。
它是如何工作的
類似於建立一個DataFrame
,建立一個新的Panel
物件可以用一個DataFrames
的字典來完成。清單 5-8 顯示了一個基本的例子。
>>> data_dict = {'item1' : pd.DataFrame(np.random.randn(4, 3)), 'item2' : pd.DataFrame(np.random.randn(4, 2))}
>>> data_panel = pd.Panel(data_dict)
Listing 5-8.Creating a Panel
您可以使用標籤訪問新Panel
中的每個DataFrames
。例如,您可以用清單 5-9 中的程式碼得到item2 DataFrame
。
>>> data_panel['item2']
0 1 2
0 -2.126160 -0.677561 NaN
1 -2.110622 -1.535510 NaN
2 -0.387182 -1.412219 NaN
3 -0.370628 0.305436 NaN
Listing 5-9.Accessing DataFrames within a Panel
一旦有了一個DataFrame
,就可以像上一節一樣訪問單個元素。
5-4.從 CSV 檔案匯入資料
問題
您需要從 CSV(逗號分隔值)檔案中匯入離線收集的資料。
解決辦法
Pandas 包含一個名為read_csv()
的方法,可以匯入和解析 CSV 檔案。
它是如何工作的
在pandas
包中有一個頂級方法可以讀入 CSV 檔案。清單 5-10 顯示了最基本的用法。
>>> csv_data = pd.read_csv('data_file.csv')
Listing 5-10.Reading a CSV File
預設情況下,pandas
將從 CSV 檔案的第一行讀取列名。如果在其他行上有列名,可以使用引數header=X
將pandas
重定向到行X
來提取列名。如果您的檔案沒有列名,您可以使用引數header=None
,然後用引數names=[col1, col2, …]
提交列名列表。另外,預設情況下,pandas
會將第一列視為每一行的標籤。如果行標籤在另一列中,可以使用引數index_col=X
。如果您根本沒有任何行標籤,您可能想要使用引數index_col=False
來強制pandas
不使用任何列。
5-5.儲存到 CSV 檔案
問題
您希望將資料儲存到 CSV 檔案中,以便與其他人或應用共享。
解決辦法
Series
和DataFrame
物件包含一個名為to_csv()
的方法。
它是如何工作的
如果你需要儲存你已經處理過的資料,你可以呼叫帶有檔名的to_csv()
方法,如清單 5-11 所示。
>>> series_data.to_csv('export_file.csv')
Listing 5-11.Saving to a CSV File
有一些引數可用於更改引用所使用的分隔符或字元。預設情況下,pandas
將寫出列標題和行標籤。如果你只想要資料,你可以使用清單 5-12 中的程式碼。
>>> data2.to_csv('data_file.csv', header=False, index=False)
Listing 5-12.Saving Data without Headers and Labels
預設情況下,pandas
將覆蓋已經存在的輸出檔案。如果您想附加到一個已經存在的檔案,您可以用引數mode='a'
改變輸出模式。
5-6.從電子表格匯入
問題
您希望從電子表格中匯入現有資料。
解決辦法
pandas
包括一個從電子表格檔案匯入單個工作表的方法,以及一個包裝類,如果你需要從一個給定的檔案中處理多個工作表的話。
它是如何工作的
如果你只需要從一個檔案中匯入一個工作表,你可以使用清單 5-13 中的程式碼來完成。
>>> data_frame1 = pd.read_excel('data_file.xsl', sheetname='Sheet1')
Listing 5-13.Importing a Single Spreadsheet Sheet
這將把資料作為單個DataFrame
物件匯入。
如果您希望處理多個工作表,為了方便訪問,將檔案一次性載入到包裝類中會更有效。幸運的是,pandas
有這樣一個包裝類,如清單 5-14 所示。
>>> excel_data = pd.ExcelFile('data_file.xsl')
Listing 5-14.Wrapping a Spreadsheet
in pandas
然後,您可以將這個物件傳遞給read_excel()
方法,而不是一個檔名。
5-7.儲存到電子表格
問題
您想要將您的DataFrame
儲存到電子表格檔案中。
解決辦法
DataFrame
類包含一個名為to_excel()
的方法,它將資料寫出到一個檔案中。
它是如何工作的
編寫輸出的最簡單方法如清單 5-15 所示。
>>> df.to_excel('output_file.xsl', sheet='Sheet1')
Listing 5-15.Writing Output to a Spreadsheet File
pandas
會根據副檔名選擇不同的編寫引擎。您也可以使用檔名結尾.xslx
儲存檔案。
5-8.得到頭部和尾部
問題
您希望查詢資料以瞭解其結構。
解決辦法
有一些函式可以用來檢視給定資料集的開頭或結尾。一旦你開始使用大型資料集,它們就非常有用。
它是如何工作的
Series
和DataFrame
物件都有名為head()
和tail()
的方法。預設情況下,對於給定的資料集,它們會分別給出前五個條目或後五個條目。如果您想檢視更多或更少的資料,您可以包含一個引數來表示您想要檢視的條目數量,如清單 5-16 所示。
>>> data_series = pd.Series(np.random.randn(1000))
>>> data_series.head(2)
0 -0.399614
1 1.307006
dtype: float64
>>> data_series.tail(2)
998 0.866424
999 -0.321825
dtype: float64
Listing 5-16.Getting the First and Last Two Data Entries
5-9.彙總資料
問題
您希望獲得資料集的統計摘要。
解決辦法
pandas
中引入的新資料物件包括一組用於提供資料彙總統計的方法。
它是如何工作的
有幾種方法可用於個體統計,如均值或標準差。還有一個名為describe()
的方法提供完整的摘要,如清單 5-17 所示。
>>> data_series.describe()
count 1000.000000
mean -0.029861
std 0.990916
min -3.261506
25% -0.697940
50% -0.048408
75% 0.646266
max 3.595167
dtype: float64
Listing 5-17.Describing Your Data
這些值中的每一個都可以透過單獨的方法獲得。例如,清單 5-18 展示瞭如何驗證標準差等於方差的平方根。
>>> data_series.std() ** 2
0.9819137628988116
>>> data_series.var()
0.9819137628988115
Listing 5-18.Comparing the Standard Deviation and the Variance
這看起來不錯,在浮點數的精度範圍內。
5-10.分類資料
問題
您希望對資料進行一些排序作為預處理。
解決辦法
Series
和DataFrame
物件包含按索引或按值排序的方法。
它是如何工作的
如果您的資料是以隨機順序輸入的,您可能需要在進行實際分析之前做一些預處理。清單 5-19 展示瞭如何根據行或列標籤對DataFrame
進行排序。
>>> df = pd.DataFrame({'one' : [1,2,3], 'two' : [4,5,6], 'three' : [7,8,9]}, index=['b','c','a'])
>>> df
one three two
b 1 7 4
c 2 8 5
a 3 9 6
>>> df.sort_index()
one three two
a 3 9 6
b 1 7 4
c 2 8 5
>>> df.sort_index(axis=1, ascending=False)
two three one
b 4 7 1
c 5 8 2
a 6 9 3
Listing 5-19.Sorting a DataFrame by Index
您可能希望對資料進行排序的另一種方式是根據實際資料值。在這種情況下,您需要決定根據哪一列進行排序。清單 5-20 展示瞭如何按降序對第二列進行排序。
>>> df.sort_values(by='two', ascending=False)
one three two
a 3 9 6
c 2 8 5
b 1 7 4
Listing 5-20.Sorting a DataFrame by Values
Note
從版本 0.17.0 開始,這些方法返回一個新的排序物件,除非您使用引數inplace=True
。在這個版本之前,排序發生在原始資料物件中。
5-11.按行或按列應用函式
問題
您需要對整個行或列執行函式。
解決辦法
DataFrames
和Panels
都有一個叫做apply()
的方法,可以用來跨列或行應用函式。
它是如何工作的
清單 5-21 展示瞭如何找到每一列和每一行的平均值。
>>> df = pd.DataFrame({'one' : [1,2,3], 'two' : [4,5,6], 'three' : [7,8,9]}, index=['b','c','a'])
>>> df.apply(np.mean)
one 2.0
three 8.0
two 5.0
dtype: float64
>>> df.apply(np.mean, axis=1)
b 4.0
c 5.0
a 6.0
dtype: float64
Listing 5-21.Averaging Rows and Columns
這是使用 lambda 函式的好地方,如果你的函式足夠簡單,可以放入一個表示式中。清單 5-22 顯示了一個簡單的 lambda 函式的例子,該函式簡單地將資料幀中的值加倍。
>>> df.apply(lambda x: 2*x, axis=1)
one three two
b 2 14 8
c 4 16 10
a 6 18 12
[3 rows x 3 columns]
Listing 5-22.Applying a Lambda Function on a Data Set
5-12.按元素應用函式
問題
您需要將函式應用於資料集中的所有元素或元素的某個子集。
解決辦法
新的 pandas 資料物件有兩個名為map()
和applymap()
的有用方法,可用於將函式應用到單個元素組。
它是如何工作的
有時候,您需要對資料集的單個元素應用一些函式。清單 5-23 展示瞭如何對一個資料集的所有元素求平方。
>>> df = pd.DataFrame({'one' : [1,2,3], 'two' : [4,5,6], 'three' : [7,8,9]}, index=['b','c','a'])
>>> df.applymap(lambda x: x*x)
one three two
b 1 49 16
c 4 64 25
a 9 81 36
Listing 5-23.Squaring Data Elements
如果你只想對一個給定的列應用一個函式,清單 5-24 展示瞭如何將列2
中的值加倍。
>>> df['two'].map(lambda x: 2*x)
b 8
c 10
a 12
Name: two, dtype: int64
Listing 5-24.Doubling a Single Column of Elements
5-13.迭代資料
問題
作為處理工作流的一部分,您需要迭代您的資料集。
解決辦法
資料物件是可迭代的物件,在大多數情況下,當你需要迴圈所有的元素時,可以使用它。
它是如何工作的
如果您想對pandas
提供的一個新資料物件進行基本迭代,您需要意識到它們的行為略有不同。迭代的基本型別有
Series
:迭代每個元素DataFrame
:遍歷各列Panel
:迭代專案標籤
例如,清單 5-25 展示瞭如何使用迭代計算每一列的平均值。
>>> df = pd.DataFrame({'one' : [1,2,3], 'two' : [4,5,6], 'three' : [7,8,9]}, index=['b','c','a'])
>>> for col in df:
....: print(df[col].mean())
....:
2.0
8.0
5.0
Listing 5-25.Averaging Each Column of a DataFrame
如果您需要迭代一個DataFrame
的行,清單 5-26 顯示了一個例子。
>>> for row_index,row in df.iterrows():
....: print(row_index)
....: print(row)
....:
b
one 1
three 7
two 4
Name: b, dtype: int64
c
one 2
three 8
two 5
Name: c, dtype: int64
a
one 3
three 9
two 6
Name: a, dtype: int64
Listing 5-26.Iterating Over Rows
然而,請注意,迭代可能會相當慢。只要有可能,您可能會希望找到另一種方式來表達您需要執行的處理工作流。
六、函式
任何現代程式語言都需要某種方式將函式組合成可重用的程式碼塊。在 Python 中,可重用程式碼的最基本單元之一是函式。在這一章中,你將會看到函式是如何在 Python 中工作的,以及它們可能會做一些令人驚訝的事情的地方。
6-1.建立基本函式
問題
您希望建立一個基本函式來處理一個簡單的任務,比如求 2 的平方。
解決辦法
在 Python 中,可以使用內建的def
關鍵字來建立一個新函式。
它是如何工作的
清單 6-1 給出了一個例子。
def square_of_two():
ans = 2 * 2
return ans
Listing 6-1.Defining a Basic Function
如您所見,函式程式碼的主體是由縮排級別定義的。第一行使用def
並建立一個具有給定名稱的新函式。在本例中,您建立了一個名為square_of_two()
的函式。現在,您可以像呼叫任何其他函式一樣呼叫該函式。例如,參見清單 6-2 。
>>> square_of_two()
4
>>> my_ans = square_of_two()
Listing 6-2.Calling a Function
如果您的函式應該向呼叫語句返回值,那麼您需要使用內建的return
關鍵字。如果你的新函式中有條件或迴圈,你需要增加這些結構的縮排量,如清單 6-3 所示。
def fact_two():
a = 2
ans = a
while a > 1:
ans = ans * (a-1)
a = a - 1
return ans
Listing 6-3.Factorial of Two Function
這將返回 2 的階乘,即 2。
6-2.使用命名引數而不是位置引數
問題
您希望將引數傳遞給函式,可以選擇使用名稱。這允許引數以任意順序傳遞給函式。
解決辦法
由於變數名稱在 Python 中是非型別化的,所以只需在括號內的引數列表中新增名稱。這些可以基於位置使用,也可以透過名稱顯式使用。
它是如何工作的
新增引數非常簡單,如清單 6-4 所示。
def square_num(a):
return a*a
Listing 6-4.Squaring Any Number
然後你可以用兩種不同的方式呼叫這個函式,或者透過位置或者透過標籤。清單 6-5 顯示了這將會是什麼樣子。
>>> square_num(2)
4
>>> square_num(a=3)
9
Listing 6-5.Calling Functions with Parameters
如果有多個引數,可以混合使用位置引數和命名引數。唯一需要注意的是,在使用任何命名引數之前,需要包含所有位置引數。清單 6-6 顯示了一個簡單的例子。
def multiplication(a, b, c):
return a*b*c
>>> multiplication(1, 2, 3)
6
>>> multiplication(2, c=3, b=1)
6
Listing 6-6.Multiplying Many Numbers
如果你嘗試類似於multiplication
(1, a=2, c=3)
的東西,你會得到一個錯誤,因為根據位置規則,你已經給了 a 一個值,然後你試圖用命名引數a=2
給它另一個值。同樣,如果您試圖在一個命名引數之後使用一個位置引數,比如multiplication(a=1,b=2,3)
,您也會得到一個錯誤。
6-3.在函式中使用預設值
問題
如果沒有傳遞預設值,您希望允許函式使用預設值。
解決辦法
定義函式及其輸入引數時,如果沒有提供任何引數,可以包含一個預設值。
它是如何工作的
在定義函式時,您可以簡單地說明需要什麼樣的預設值,如清單 6-7 所示。
def multiplication(a=1,b=2,c=3):
return a*b*c
Listing 6-7.Defining Default Parameter Values
關於位置引數和命名引數的所有規則仍然適用。清單 6-8 展示了一些例子。
>>> multiplication()
6
>>> multiplication(5)
30
>>> multiplication(1,1)
3
>>> multiplication(c=1)
2
Listing 6-8.Multiplication Examples
既然您有了預設值,那麼對於那些沒有親自閱讀過程式碼的人來說,您就有了一些看不見的操作。在這種情況下,切換到使用嚴格命名的引數有助於澄清程式碼並幫助將來的程式碼維護是有意義的。
6-4.實現遞迴演算法
問題
您需要在 Python 程式中實現一個遞迴演算法。
解決辦法
Python 支援遞迴,因此您可以從函式內部呼叫函式。
它是如何工作的
因為 Python 支援遞迴,所以可以簡單地直接呼叫函式。典型的例子是計算階乘,如清單 6-9 所示。
def fact(a):
if a == 1:
return 1
else:
return a * fact(a-1)
>>> fact(5)
120
Listing 6-9.Calculating Factorials Through Recursion
雖然有一些演算法實際上只作為遞迴函式工作,但應該謹慎對待它們。遞迴函式實際上是巢狀的函式呼叫。這意味著遞迴的每一步都需要在下一次呼叫之前將當前狀態推送到堆疊上。根據需要跟蹤的內容,您可能會很快用完大量 RAM。此外,每個函式呼叫都需要更多時間來進行上下文切換。因此,在開始使用遞迴函式之前,一定要確保這是唯一的解決方案。
6-5.使用 Lambda 函式建立臨時匿名函式
問題
您臨時需要一個函式(例如,作為另一個函式的引數),並且不需要透過名稱訪問它。
解決辦法
Python 有內建的lambda
語句機制,它提供了建立和使用匿名函式的能力。
它是如何工作的
為了展示如何使用 lambda 函式,我們需要一個需要 lambda 函式的示例。清單 6-10 展示了一個帶兩個引數的函式和一個函式,並以兩個給定的引數作為輸入執行給定的函式。
def apply_operator(a, b, f):
return f(a,b)
Listing 6-10.Applying a Function
如果您只想在上面的示例程式碼中使用一次性函式,可以在呼叫中直接使用 lambda 函式。清單 6-11 展示瞭如何應用乘法函式。
>>> apply_operator(2, 3, lambda x, y: x*y)
6
Listing 6-11.Applying a Multiplication Function
lambda 函式的最大限制是它們被限制在一個表示式行中。任何大於這個值的都需要定義為常規函式。
6-6.生成專門的函式
問題
您需要建立一個能夠為特殊情況生成專用函式的函式。例如,您可能希望對複數而不是常規浮點數使用不同的平均函式。
解決辦法
使用 lambda 函式,您可以生成專門的函式。
它是如何工作的
因為函式只是另一種型別的物件,所以它們可以從函式呼叫中返回。利用這一事實,您可以建立一個函式,它接受一些輸入引數,並踢出由它定義的函式。例如,清單 6-12 基於輸入值生成一個縮放函式。
def generate_scaler(a):
return lambda x: a*x
Listing 6-12.Generating Scaling Functions
然後,您可以使用這個生成器建立一個將數字縮放 2 或 3 的函式,如清單 6-13 所示。
>>> doubler = generate_scaler(2)
>>> doubler(3)
6
>>> tripler = generate_scaler(3)
>>> tripler(3)
9
Listing 6-13.Function Generator
Examples
七、類和物件
一旦有了將程式碼塊儲存到可重用函式中的方法,想要將多個函式捆綁到更大的可重用程式碼塊(稱為物件)中是很自然的事情。除了這些實際的程式碼,您可能還想以屬性的形式儲存資料。為了定義這些新物件,您需要建立類,然後將它們例項化為您可以使用的實際物件。在這一章中,你將會看到在你開始定義和使用新物件時出現的一些最常見的任務。
7-1.發現物件的型別(一切都是物件)
問題
Python 中的幾乎所有東西都是物件。重要的是弄清楚你擁有什麼樣的物品。
解決辦法
內建函式type()
返回作為引數傳入的物件的類。您還可以使用isinstance()
來測試給定的物件是否屬於某個特定類的同一型別。
它是如何工作的
清單 7-1 給出了一個例子。
>>> a = 1
>>> type(a)
<class 'int'>
>>> b = "Hello World"
>>> type(b)
<class 'str'>
Listing 7-1.Checking the Type of an Object
這將查詢給定的物件並返回輸入物件的型別物件。您可以使用它來檢查兩個物件是否屬於同一類。如果您想檢視一個物件是否屬於某個特定的類,您可以使用isinstance()
,如清單 7-2 所示。
>>> a = 1
>>> isinstance(a, int)
TRUE
Listing 7-2.Is an Object of a Particular Class?
7-2.建立類
問題
您希望建立一個新的類,封裝一組方法和屬性,以便在其他程式碼中使用。
解決辦法
關鍵字class
允許你定義一個新的類。
它是如何工作的
與函式一樣,定義一個新類就像使用class
關鍵字,然後擁有一個縮排的程式碼塊一樣簡單。你可以在清單 7-3 中看到一個非常簡單的例子。
class my_simple_class:
a = 3
b = "Hello World"
Listing 7-3.Creating a Simple Class
清單 7-4 展示了一個更復雜的例子,也包括一系列方法。
class my_complex_class:
a = 42
def method1():
print("The value of a is " + str(a))
b = "Hello World"
Listing 7-4.Creating a Complex Class
7-3.新增私有欄位
問題
您希望您的類的某些部分是私有的,也就是說,不能從其他程式碼段訪問。
解決辦法
在 Python 中,所有的方法和屬性都是公開可見的。作為一種解決方案,在屬性或方法的名稱中新增下劃線字元幾乎被普遍接受,因為它代表了一個不適合公共使用的元素。
它是如何工作的
定義一個只能在所討論的類中使用的屬性應該至少有一個下劃線字元作為字首。通常的做法是在元素名的開頭和結尾新增兩個下劃線字元,如清單 7-5 所示。
class priv_vars:
__a__ = "This is a private variable
"
Listing 7-5.Creating a Private Variable
有一種特殊形式的私有名稱,它促使 Python 使用名稱管理系統來重新命名元素。如果某個元素可能與其他元素同名,則可以新增至少兩個前導下劃線字元和最多一個尾隨下劃線字元。然後 Python 會將類名新增到元素名的前面。例如,如果你有一個像
class my_class1:
__a_ = 1
屬性__a_
將被重新命名為_my_class__a_
以避免名稱衝突。
7-4.子類
問題
您希望在另一個類中提供的功能基礎上進行構建。
解決辦法
子類化的能力是任何物件導向程式語言的關鍵。Python 支援從單個基類或多個基類繼承。
它是如何工作的
從一個類繼承只是在新類的定義中新增一個對相關類的引用。清單 7-6 中給出了一個簡單的例子。
class animal:
type = "mammal"
class dog(animal):
breed = "labrador"
>>> my_dog = dog()
>>> my_dog.breed
labrador
>>> my_dog.type
mammal
Listing 7-6.Inheriting from a Class
如您所見,類dog
繼承了animal
型別的屬性。解析方法和屬性的方式是首先檢查主類,看它是否存在。如果沒有,那麼 Python 將檢查任何繼承的類,看看該屬性或方法是否存在。這意味著您可以用自己的版本重寫繼承的方法或屬性。清單 7-7 中animal
型別改為bird
。
class bird(animal):
type = "bird"
>>> my_bird = bird()
>>> my_bird.type
bird
Listing 7-7.Overriding Class Attributes
如果您從多個類繼承,您可以簡單地將它們作為多個引數新增到您的類定義中,如清單 7-8 所示。
class eagle(animal, bird):
species = "eagle"
>>> my_eagle = eagle()
>>> my_eagle.type
animal
Listing 7-8.
Multiple Inheritance
如您所見,在處理多重繼承時,順序很重要。當 Python 嘗試解析方法或屬性的定義位置時,它從主類開始,然後從左到右依次遍歷繼承的類,直到找到所述方法或屬性的第一個例項。以上案例中,正確的繼承順序應該是class eagle(bird, animal)
。
7-5.初始化物件
問題
當例項化一個新物件時,需要執行一些初始值或初始處理步驟。
解決辦法
您可以使用私有方法__init__
在初始化時執行設定程式碼。
它是如何工作的
當 Python 例項化一個新物件時,它會檢視是否有一個名為__init__
的方法。如果找到了,這個函式會在例項化完成後立即執行。它還可以在例項化時獲取引數,這些引數可用於進一步處理設定步驟,如清單 7-9 所示。
class my_complex:
def __init__(self, real_part, imaginary_part):
self.r = real_part
self.i = imaginary_part
>>> complex_num = my_complex(2, 3)
>>> complex_num.r
2
Listing 7-9.
Initialization Functions
引數self
指的是被例項化的物件,允許你在初始化過程中與物件進行互動。
7-6.比較物件
問題
你需要比較兩個物體,看它們是否相同。
解決辦法
在比較物件時,有兩種不同的相等性思想:將一個物件與其自身進行比較,並檢視兩個不同的物件是否具有相同的屬性。
它是如何工作的
第一種型別的等式是測試兩個變數名是否實際指向同一個物件。這是 Python 中物件和指向它們的標籤分離的副作用。為了測試這種型別的等式,您需要使用運算子is
和is not
,如清單 7-10 所示。
>>> a = "string1"
>>> b = a
>>> a is b
True
Listing 7-10.Comparing Object Identities
下一種型別的比較包括比較兩個物件的內容,看它們是否有相同的值。值的概念在 Python 中不是一個通用的概念。物件值的計算由使用運算子==
、!=
、<=
、>=
、<
和>
時執行的運算子程式碼處理。根據您定義的任何類的詳細資訊,您可能希望覆蓋這些比較運算子的程式碼。例如,假設您已經建立了一個表示一本書的類,並且您希望使用頁數作為給定 book 物件的值。然後,您可以用清單 7-11 中的程式碼覆蓋這些運算子。
class book:
def __init__(self, pages):
self.pages = pages
def __lt__(self, other):
return self.pages < other
def __gt__(self, other):
return self.pages > other
....
Listing 7-11.Overriding Comparison Operators
依此類推,適用於所有的運算子方法。
7-7.建立後更改物件
問題
建立物件後,您需要對其進行更改。
解決辦法
在 Python 中,幾乎所有物件都是可塑的,可以根據它們的屬性和方法進行修改。內建物件,如str
或int
,沒有可塑性。從您自己的類定義中建立的物件是可擴充套件的,可以動態地建立和使用新的屬性。
它是如何工作的
例如,您可以使用清單 7-11 中定義的類,透過簡單地使用一個名為title
的新屬性來新增圖書的標題,如清單 7-12 所示。
>>> book1 = book(42)
>>> book1.title = "My Book"
>>> book1.title
My Book
Listing 7-12.Dynamically Created Attributes
您可以透過定義一個什麼都不做的類,利用這種延展性來建立非常快速、靈活的資料結構,如清單 7-13 所示。
class my_container:
pass
>>> container1 = my_container()
>>> container1.label = "The first container"
>>> container1.phone = "555-5555"
>>> container1.name = "John Doe"
Listing 7-13.Empty Classes for Data Storage
關鍵字pass
是一個無操作函式。它實際上佔用了表示式應該去的地方的空間,但是告訴 Python 實際上沒有任何程式碼要執行。這使您可以建立一個完全空的物件,以後可以新增到該物件中。
7-8.實現多型行為
問題
您需要包括根據輸入內容而變化的行為。
解決辦法
Python 透過它是一種鴨式語言的事實來處理多型性。基本上,當一個函式試圖使用一個物件作為引數時,它實際上會透過該物件需要實現的一些標準方法從該物件獲取值。
它是如何工作的
展示這種技術的最好方式是使用一個例子。清單 7-14 顯示了本例中使用的一系列類和函式。
class dog:
def talk(self):
print("bark")
class cat:
def talk(self):
print("meow")
def speak(animal):
animal.talk()
Listing 7-14.Polymorphic Classes and Methods
從這裡開始,根據您作為引數傳遞的動物型別,您將從函式speak()
獲得不同的行為。清單 7-15 展示了這種變化行為的一個例子。
>>> my_dog = dog()
>>> my_cat = cat()
>>> speak(my_dog)
bark
>>> speak(my_cat)
meow
Listing 7-15.Polymorphic Behavior
八、超程式設計
程式設計的一個關鍵原則是不要重複自己。每當你不止一次地做同樣的任務時,你應該看一看它,看看是否有什麼方法可以使它自動化。這是寫程式的首要原因。但是這同樣適用於編寫程式碼本身的任務。如果您正在重複程式碼塊,您應該後退一步,看看是否有一些更好的方法來達到相同的結果。
處理這個問題的一種技術是超程式設計。本質上,超程式設計是影響其他程式碼的程式碼。在 Python 中,通常的做法是使用裝飾器、元類或描述符。
8-1.使用函式裝飾器包裝現有程式碼
問題
您希望透過用其他程式碼包裝一個已經存在的函式來改變它的行為。如果不同的模組使用相同的裝飾名,那麼這個包裝器程式碼可以換入或換出,允許您以不同的方式修改原始函式。
解決辦法
透過在函式定義的頂部新增一個裝飾符,使用一個以&符號開始的標籤,可以包裝一個函式。
它是如何工作的
清單 8-1 展示了一個使用由line_profile
模組提供的裝飾器的例子。
from line_profile import *
@profile
def my_adder(x, y):
return x + y
Listing 8-1.Using the Profile Decorator
這段程式碼用來自line_profile
模組的分析程式碼包裝函式my_adder()
。這個模組不是標準庫的一部分,所以您需要將它安裝到您的系統上。這與用另一個函式顯式包裝一個函式是一樣的,如清單 8-2 所示。
from line_profiler import *
def my_adder(x, y):
return x + y
my_adder = profile(my_adder)
Listing 8-2.Wrapping a Function
8-2.編寫函式裝飾器來包裝現有程式碼
問題
你想為一個函式寫一個包裝器來增加額外的功能。
解決辦法
Python 包含了wraps
關鍵字,該關鍵字定義了一個可以包裝另一個函式並用作裝飾器的函式。
它是如何工作的
為了編寫自己的裝飾器,您需要使用來自functools
模組的wraps
關鍵字。這個關鍵字被用作裝飾器來幫助定義你自己的新裝飾器。清單 8-3 顯示了一個列印出被修飾函式的函式名的例子。
from functools import wraps
def function_name(func):
message = func.__qualname__
@wraps(func)
def wrapper((*args, **kwargs)):
print(message)
return func(*args, **kwargs)
return wrapper
Listing 8-3.A Decorator to Print Out Function Names
然後你可以像其他裝飾器一樣使用它,如清單 8-4 所示。
@function_name
def adder(x,y):
return x+y
Listing 8-4.Using Your Own Decorator
8-3.展開修飾函式
問題
您需要訪問已被修飾的函式的功能。
解決辦法
你可以透過使用函式的__wrapped__
屬性得到原始的解包函式。
它是如何工作的
假設裝飾器是使用來自functools
的wraps
函式正確編碼的,那麼您可以透過使用__wrapped__
屬性獲得原始函式,如清單 8-5 所示。
>>> adder(2,3)
adder
5
>>> adder.__wrapper__(2,3)
5
Listing 8-5.Getting the Unwrapped Function
8-4.使用元類改變類的結構
問題
您需要向類中新增額外的功能,類似於函式的裝飾器。這可以透過使用元類改變一個類是哪個物件的例項來實現。
解決辦法
元類的使用方式類似於子類。
它是如何工作的
當使用元類時,將它包含在類定義中,如清單 8-6 所示。
class my_counter(metaclass=Singleton):
pass
Listing 8-6.Using a Metaclass
預設情況下,類是型別class
的例項。清單 8-6 中的程式碼使新類成為Singleton
類的例項,而不是型別class
。您還可以在類定義中設定元類,如清單 8-7 所示。
class my_counter():
__metaclass__ = Singleton
pass
Listing 8-7.Setting the __metaclass__ Attribute
在清單 8-6 和 8-7 中,您的新類被建立為Singleton
類的一個例項。這是在 Python 中使用 singleton 設計模式的一種方法。
8-5.編寫元類
問題
您需要透過編寫自己的元類來改變類的例項化方式。
解決辦法
透過使用元類,您可以重新定義一個類實際上是如何例項化的,例如,允許您建立只能例項化一次的類(singleton 設計模式),或者被快取的類。這些示例用於日誌類或流資料解析器。
它是如何工作的
您可以透過構建一個覆蓋例項化過程中使用的一個或多個函式的類來建立元類。例如,您可以覆蓋__call__
函式來建立一個不能例項化的類,如清單 8-8 所示。
class CannotInit(type):
def __call__(self, *args, **kwargs):
raise TypeError("Cannot instantiate")
Listing 8-8.A Metaclass That Stops Instantiation
現在,當您試圖將它用作元類並直接例項化新類時,將會引發一個異常。
如果您需要更復雜的行為,例如在單例中,您可以覆蓋多個函式,如清單 8-9 所示。
class MySingleton(type):
def __init__(self, *args, **kwargs):
self.__instance = None
super().__init__(*args, **kwargs)
def __call__(self, *args, **kwargs):
if self.__instance is None:
self.__instance = super().__call__(*args, **kwargs)
return self.__instance
else:
return self.__instance
Listing 8-9.Creating a Singleton Metaclass
這段程式碼捕獲了使用這個元類的任何類的例項化和呼叫,因此一次只能存在一個例項。
8-6.使用簽名改變函式接受的引數
問題
您希望能夠在執行時控制函式的簽名。這允許您動態地更改函式接受的引數。例如,可以強制函式在一種情況下只使用關鍵字引數,然後在另一種情況下允許使用位置引數或關鍵字引數。
解決辦法
inspect
模組包括建立和使用函式簽名所需的工具。
它是如何工作的
清單 8-10 中的例子展示瞭如何建立一個新的簽名。
>>> from inspect import Signature, Parameter
>>> params = [Parameter('x', Parameter.POSITIONAL_OR_KEYWORD),
... Parameter('y', Parameter.POSITIONAL_OR_KEYWORD, default=42),
... Parameter('z', Parameter.KEYWORD_ONLY, default=None)]
>>> my_sig = Signature(params)
>>> print(my_sig)
(x, y=42, *, z=None)
Listing 8-10.Creating a Signature
在這段程式碼中,您使用Parameter
類建立一個函式引數列表。每種型別的引數都有關鍵字。需要注意的一點是,如果在一個普通的函式定義中有一個只包含關鍵字的引數列表,那麼可以使用星號來標記哪些引數是隻包含關鍵字的。當您列印出新建立的簽名時,就會顯示出來。
要使用這個簽名,您可以使用bind
方法獲取位置和關鍵字引數的一般列表,並將它們繫結到您建立的簽名中的引數。清單 8-11 中給出了一個例子。
def my_func(*args, **kwargs):
bound_values = my_sig.bind(*args, **kwargs)
for name, value in bound_values.arguments.items():
print(name, value)
Listing 8-11.Using a Signature
這樣,您可以讓同一個函式使用不同的簽名繫結引數,具體取決於您需要如何處理它們。
九、網路和網際網路
網際網路徹底改變了我們使用電腦的方式。以前,我們關注的是我們能用辦公桌上的硬體做什麼;現在我們可以考慮在分佈在全球的機器上可以做什麼工作。在本章中,您將學習透過網路(如網際網路)與其他機器通訊的一些核心技術。您將從檢視最底層的方法之一開始:使用套接字。秘籍的其餘部分將著眼於更高層次的技術,這些技術隱藏了圍繞網際網路通訊的許多複雜性。
9-1.開啟套接字連線
問題
您希望開啟一個原始網路套接字連線來處理非結構化資料通訊。
解決辦法
Python 標準庫包括一個socket
類,它公開了網路通訊的底層介面。
它是如何工作的
socket
類提供了一種在非常低的級別訪問網路硬體的方法。因此,它必須支援許多不同的網路協議。對於這些方法,您將關注最常見的情況,即希望透過乙太網連線建立 TCP/IP 套接字。socket
模組包含了socket
類和其他幾個你可以使用的實用函式和類。清單 9-1 展示瞭如何建立一個新的套接字並連線到一個遠端機器。
import socket
host = '192.168.0.1'
port = 5050
my_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
my_sock.connect((host, port))
Listing 9-1.Opening a Socket to a Remote Machine
在這段程式碼中,機器地址以包含 IP 地址的字串形式給出,埠以數字形式給出。這個例項化方法建立了一個socket
物件,它既可以用來建立到遠端機器的傳出連線,也可以用來監聽來自這些遠端機器的傳入連線請求。如果您只對建立一個到遠端機器的傳出連線感興趣,您可以使用create_connection()
方法,如清單 9-2 所示。
import socket
host = '192.168.0.1'
port = 5050
my_sock = socket.create_connection
((host, port))
Listing 9-2.Making an Outgoing Socket Connection
這個新建立的socket
物件現在可以用來向遠端機器傳送資料和從遠端機器接收資料。當您完成一個給定的套接字連線時,不要忘記關閉它,以便作業系統可以乾淨地關閉該連線並在它之後進行清理。清單 9-3 展示了這樣做的樣板程式碼。
my_sock.close()
Listing 9-3.Closing a Socket Connection
9-2.透過套接字讀/寫
問題
您希望透過開放的套接字連線與遠端機器通訊。
解決辦法
socket
類包含許多透過套接字連線傳送和接收資料的不同方法。
它是如何工作的
一旦套接字被開啟,您就可以使用來自socket
物件的方法來傳送和接收資料。傳送資料最基本的方法是使用send()
方法,如清單 9-4 所示。
msg = b'Hello World'
mesglen = len(msg)
totalsent = 0
while totalsent < msglen:
sent = my_sock.send(msg[totalsent:])
totalsent = totalsent + sent
Listing 9-4.Sending Data Over a Socket
這裡有幾點需要注意。首先,套接字透過網路傳送位元組,因此您的訊息需要是一個位元組串。第二件要注意的事情是,send()
方法並不保證在任何特定的呼叫中會傳送多少資料。它所做的只是返回在任何特定呼叫中成功傳送的位元組數,因此需要一個while
迴圈來繼續傳送,直到您確定所有內容都已傳輸完畢。如果您正在傳送簡單的資料塊,您可以使用sendall()
方法,如清單 9-5 所示,它將處理迴圈,直到所有的資料都被髮送完。
my_sock.sendall(b'Hello World')
Listing 9-5.Using sendall() with a Socket
接收資料的方法與傳送資料的方法非常相似。一個主要的區別是,您需要告訴方法一次讀入多少位元組。例如,清單 9-6 展示瞭如何讀入上面傳送的資料,並確保得到了所有資料。
data_in = my_sock.recv(1024)
Listing 9-6.Reading Data from a Socket
這是可行的,因為您確實知道正在傳送的訊息長度小於 1,024 位元組。如果訊息較長,或者是可變的,就必須一遍又一遍地迴圈,直到收集到所有獨立的塊,就像傳送資料時必須迴圈一樣。與sendall()
方法等效的接收方法是recv_into()
方法。它允許您將資料接收到一個預先構造的緩衝區中,當所有的資料都已被接收或緩衝區已被填滿時停止。清單 9-7 中給出了一個例子,展示瞭如何將多達 1024 個位元組讀入一個緩衝區。
buffer = bytearray(b' ' * 1024)
my_sock.recv_into(buffer)
Listing 9-7.Receiving Data Directly into a Buffer
9-3.用 POP 閱讀電子郵件
問題
您想從 POP 電子郵件伺服器上閱讀電子郵件。
解決辦法
Python 標準庫包含一個名為poplib
的模組,它封裝了與 POP 伺服器的通訊。
它是如何工作的
與 POP 伺服器通訊涉及幾個步驟。最基本的初始程式碼包括開啟到 POP 伺服器的連線和認證,如清單 9-8 所示。
import getpass, poplib
pop_serv = poplib.POP3('192.168.0.1')
pop_serv.user(getpass.getuser())
pop_serv.pass_(getpass.getpass())
Listing 9-8.Connecting to a POP Server and Authenticating
如您所見,您還匯入了模組getpass
。該模組幫助您的程式碼安全地向終端使用者詢問密碼。getuser()
方法還向作業系統查詢終端使用者的使用者名稱。如果它與 POP 伺服器的使用者名稱相同,那就太好了。否則,您需要將它硬編碼到您的指令碼中,或者您必須明確地向終端使用者詢問他們的 POP 使用者名稱。如果您的 POP 伺服器正在監聽非標準埠,您可以將其作為另一個引數。如果您使用的 POP 伺服器更加安全,您需要使用POP3_SSL
類來代替。現在,您可以與 POP 伺服器進行互動..清單 9-9 展示瞭如何獲取郵箱的當前狀態。
msg_count, box_size = pop_serv.stat()
Listing 9-9.Getting the Status of a POP Mailbox
您可以使用清單 9-10 中的程式碼獲得當前郵箱訊息的列表。
msg_list = pop_serv.list()
Listing 9-10.Listing the Messages in a POP Mailbox
當您想檢視單個電子郵件時,可以使用方法retr()
,如清單 9-11 所示。
message = pop_serv.retr(1)
Listing 9-11.Retrieving Individual E-Mails from a POP Server
此方法使用訊息索引。(示例中的索引 1)來決定檢索哪個電子郵件。它還將選定的電子郵件標記為已在 POP 伺服器上閱讀。您還可以使用dele()
方法清理您的郵箱,其中您將電子郵件索引作為引數。與任何與系統資源互動的程式碼一樣,不要忘記用類似清單 9-12 的程式碼徹底關閉任何開啟的連線。
pop_serv.quit()
Listing 9-12.Closing a POP Connection
9-4.用 IMAP 閱讀電子郵件
問題
你需要從 IMAP 郵件伺服器上閱讀郵件。
解決辦法
Python 標準庫包括一個名為imaplib
的模組,它簡化了與 IMAP 電子郵件伺服器的通訊。
它是如何工作的
imaplib
模組包含一個主類,它管理與 IMAP 伺服器的通訊。清單 9-13 展示瞭如何初始化和認證一個 IMAP 連線。
import imaplib, getpass
my_imap = imaplib.IMAP4('myimap.com')
my_imap.login(getpass.getuser(), getpass.getpass())
Listing 9-13.Creating an IMAP Connection
您可以使用getpass
模組從終端使用者那裡安全地獲取密碼。如果您的 IMAP 伺服器使用 SSL,您需要使用IMAP4_SSL
類。IMAP 提供了更多的組織結構來組織您的電子郵件。其中包括擁有多個可用郵箱的能力。因此,在處理個人電子郵件之前,您需要選擇一個郵箱。要獲得電子郵件列表,您需要實際進行搜尋。清單 9-14 展示瞭如何獲取預設郵箱中所有郵件的列表。
my_imap.select()
typ, data = my_imap.search(None, 'ALL')
Listing 9-14.Getting a List of E-Mails from IMAP
然後可以在data
變數中迴圈返回的電子郵件索引。對於這些索引中的每一個,您可以呼叫fetch()
方法。如果您願意,您可以有選擇地只獲取電子郵件的一部分。清單 9-15 展示瞭如何從 IMAP 伺服器獲取整個電子郵件。
email_msg = my_imap.fetch(email_id, '(RFC822)')
Listing 9-15.Fetching E-Mails from an IMAP Server
然後,您可以抽出電子郵件中您感興趣的部分。IMAP 有一套非常完整的命令,可以用來處理郵箱和個人電子郵件。例如,清單 9-16 向您展示瞭如何刪除一封電子郵件。
my_imap.store(email_id, '+FLAGS', '\\Deleted')
my_imap.expunge()
Listing 9-16.Deleting E-Mails from an IMAP Server
當您使用完 IMAP 伺服器後,不要忘記清理所有東西,如清單 9-17 所示。
my_imap.close()
my_imap.logout()
Listing 9-17.Shutting Down an IMAP Connection
9-5.傳送電子郵件
問題
你需要發一封電子郵件。
解決辦法
Python 標準庫包括一個名為smtplib
的模組,它可以處理與 SMTP 伺服器的通訊。
它是如何工作的
電子郵件是使用 SMTP 協議透過網際網路傳送的。smtplib
包括一個基類來處理與SMTP
類的連線,如清單 9-18 所示。
import smtplib, getpass
my_smtp = smtplib.SMTP('my.smtp.com')
my_smtp.login(getpass.getuser(), getpass.getpass())
Listing 9-18.Connecting to an SMTP Server
只有當您的 SMTP 伺服器需要身份驗證時,才需要最後一行。如果您的 SMTP 伺服器使用 SSL,您需要使用SMTP_SSL
類來代替。一旦您的連線開啟,您現在就可以傳送電子郵件,如清單 9-19 所示。
from_addr = 'me@email.com'
to_addr = 'you@email.com'
msg = 'From: me@email.com\r\nTo: you@email.com\r\n\r\nHello World'
my_smtp.sendmail(from_addr, to_addr, msg)
Listing 9-19.Sending an E-Mail Message
一旦你傳送了你的電子郵件,你可以用物件的方法來清理。
9-6.閱讀網頁
問題
你需要得到一個網頁的內容。
解決辦法
Python 標準庫包括一個名為urllib
的類模組,它處理幾種不同協議上的通訊。要與 web 伺服器對話,您需要使用子模組urllib.request
。
它是如何工作的
幾乎所有連線到 web 伺服器的複雜性都被封裝在模組urllib.request
中的程式碼中。對於一個基本的連線,您可以使用方法urlopen()
,如清單 9-20 所示。
import urllib.request
my_web = urllib.request.urlopen('http://www.python.org')
Listing 9-20.Connecting to a Web Server
然後,您可以從該 URL 讀取資料。與大多數網路通訊一樣,資料是作為一系列位元組讀取的。清單 9-21 展示瞭如何從你所連線的 URL 中讀取並列印前 100 個位元組。
print(my_web.read(100))
Listing 9-21.Reading Data from a URL
9-7.張貼到網頁
問題
您需要透過 GET 或 POST 與 web 表單進行通訊。
解決辦法
Python 標準庫中的urllib.request
模組支援使用 GET 或 POST 方法將表單資料傳送到 web 伺服器。
它是如何工作的
urllib.request
模組包括一個名為Request
的類,它可以處理與 web 伺服器更復雜的互動。清單 9-22 展示瞭如何建立一個新的連線。
import urllib.request
mydata = b'some form data'
my_req = urllib.request.Request('http://form.host.com', data=mydata, method='POST')
Listing 9-22.Connecting to a Web Form
然後您可以在urlopen()
方法中使用這個新的Request
物件,如清單 9-23 所示。
my_form = urllib.request.urlopen(my_req)
print(my_form.status)
print(my_form.reason)
Listing 9-23.Opening a Request Object
9-8.充當伺服器
問題
您希望建立一個偵聽傳入連線的網路應用。
解決辦法
Python 標準庫中的socket
類支援監聽傳入的連線。
它是如何工作的
清單 9-24 展示瞭如何建立一個socket
物件來監聽給定的埠號。
import socket
host = ''
port = 4242
my_server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
my_server.bind((host, port))
my_server.listen(1)
Listing 9-24.Listening on a Network Port
您應該知道,根據您的計算機上使用的設定,您可能會收到來自防火牆的警告。建立套接字後,您需要顯式地接受可以讀取的傳入連線,如清單 9-25 所示。
conn, addr = my_server.accept()
print('Connected from host ', addr)
data = conn.recv(1024)
Listing 9-25.Accepting Incoming Connections
就像任何套接字連線一樣,不要忘記使用close()
方法乾淨地關閉網路連線。
十、模組和包
使用 Python 的最大優勢之一是可用模組的大環境。幾乎每一種可以想象的用途都有一個模組。事實上,這甚至是 XKCD 網路漫畫的主題。在本章中,您將瞭解如何透過安裝和使用可用的模組來利用已經完成的所有工作。您還將瞭解如何將您自己的工作打包成可以與其他人共享的模組。
10-1.匯入模組
問題
您希望將給定模組中的功能匯入到當前的 Python 程式中。
解決辦法
Python 包含了import
關鍵字來處理整個模組的匯入,或者只匯入可用功能的選定部分。
它是如何工作的
清單 10-1 展示了一個匯入整個模組的基本例子。
>>> import math
Listing 10-1.Basic Importing of a Module
這段程式碼將標準模組math
匯入到當前 Python 直譯器的名稱空間中。您可以使用點符號來訪問這個模組中包含的任何功能,如清單 10-2 所示。
>>> math.sin(45)
0.8509035245341184
Listing 10-2.Dot Notation for Modules
為了減少鍵入的數量,您可以在程式中使用別名。清單 10-3 展示瞭如何用as
關鍵字為math
標準模組做這件事。
>>> import math as m
>>> m.cos(45)
0.5253219888177297
Listing 10-3.Using Aliases
如果您只需要某些功能,您可以用關鍵字from
匯入特定的專案,如清單 10-4 所示。
>>> from math import sin, pi
>>> sin(pi/2)
1.0
Listing 10-4.Importing Parts of a Module
如您所見,這將math
模組的匯入部分放入 Python 程式的名稱空間中。然後,您可以在不使用前面顯示的點符號的情況下使用它們。您可以用清單 10-5 中的語句新增給定模組中的所有內容。
>>> from math import *
Listing 10-5.Importing All of a Module
請注意,每次匯入都會增加名稱衝突的機率。您最終可能會得到一個給定函式的多個例項。上次匯入的版本是執行給定函式名時使用的版本。你需要意識到由此可能引發的複雜情況。
10-2.從原始碼安裝模組
問題
您需要將一個模組從原始碼安裝到一個標準庫目錄中。
解決辦法
當從原始碼安裝時,許多模組被編寫為使用 Python 中包含的一個名為distutils
的系統。
它是如何工作的
Distutils
是 Python 中包含的一個非常基本的系統。對於更簡單的模組,處理安裝過程已經足夠了。該模組包括一個名為setup.p
y 的特殊檔案,用於處理細節。當您使用distutils
安裝一個包時,首先要將原始檔解壓到一個臨時目錄中。然後,您需要執行清單 10-6 中給出的命令。
python setup.py install
Listing 10-6.Installation with Distutils
這將執行模組所需的所有步驟,以便將它安裝在 Python 庫目錄之一中。
10-3.從 Pypi 安裝模組
問題
您想要安裝 Pypi 儲存庫中的一個可用模組。
解決辦法
工具 pip 提供了直接從 Pypi 儲存庫中輕鬆安裝模組和包的能力。
它是如何工作的
在早期版本的 Python 中,pip 工具不可用。它從版本開始提供
-
- Python 2 >= 2.7.9
-
- Python 3 >= 3.4
對於早期版本,您首先需要安裝它。如果您使用的是 Linux,大多數發行版都包含一個 pip 包。對於其他作業系統,或者如果您想要安裝最新版本,您可以從 Pypi 網站( https://pip.pypa.io/en/latest/installing/
)下載所需的軟體包,並按照附帶的說明進行操作。然後,您可以使用清單 10-7 中的命令安裝感興趣的 Python 模組。
pip install numpy
Listing 10-7.Installing a Module with pip
使用 pip 真正強大的部分是它將處理依賴性檢查。這樣,您就可以專注於安裝您需要的部件。如果您試圖在一個您沒有許可權的系統上安裝模組,您總是可以使用清單 10-8 中所示的命令將其安裝在您的主目錄中。
pip install --user numpy
Listing 10-8.Installing a Module in Your Home Directory
10-4.使用 pip 升級模組
問題
您需要更新系統上已經安裝的軟體包。
解決辦法
pip 工具包括一個升級選項,該選項將檢查 Pypi 儲存庫以檢視是否有更新的版本。
它是如何工作的
要檢查新版本,使用清單 10-9 中給出的命令。
pip install --upgrade numpy
Listing 10-9.Upgrading Packages
這種形式的更新選項對所有依賴項進行積極的更新。相反,您可以使用清單 10-10 中的命令進行“必要時升級”更新。
pip install --upgrade --no-deps numpy
pip install numpy
Listing 10-10.Doing a Selective Upgrade
如您所見,這實際上是一個兩步過程,應該只更新那些需要更新的依賴項。
十一、數字和數值
Python 越來越多的應用領域之一是在科學界。一個問題,也一直是一個問題,就是 Python 在做數值計算的時候效率不是很高。幸運的是,Python 的設計旨在使擴充套件其功能變得相對容易。有助於科學計算的核心模組是Numpy
模組。Numpy 將處理數字計算的最低效部分外包給用 c 編寫的外部庫,它使用的標準開源庫與其他專門編寫的應用中使用的相同,用於處理大量的數字計算。
Numpy 功能的核心是由一個叫做 array 的新物件提供的。陣列是包含一種資料型別元素的多維物件。這意味著 Numpy 模組中的函式可以自由地假設可以對資料做什麼,而不必在訪問資料時檢查每個元素。
11-1.建立陣列
問題
您希望建立陣列用於其他 Numpy 函式。
解決辦法
建立陣列最簡單的方法是使用提供的creation
函式獲取列表中的現有資料,並將其轉換成一個新的陣列物件。也可以使用 empty 函式建立一個新的空陣列物件。
它是如何工作的
最簡單的形式的array
函式簡單地接受一個值列表並返回一個新的陣列物件,如清單 11-1 所示。
>>> import numpy as np
>>> list1 = [1, 2, 3.0, 4]
>>> array1 = np.array(list1)
>>> array1
array([1., 2., 3., 4.])
Listing 11-1.Basic Array Creation
這將返回一個一維陣列,其中每個元素都是一個實數。array
函式的預設行為是選擇儲存原始列表中每個元素的最小資料型別。您可以專門選擇想要在程式碼中使用的資料型別,比如清單 11-2 。
>>> complex1 = np.array(list1, dtype=complex)
>>> complex1
array([1.+0.j, 2.+0.j, 3.+0.j, 4.+0.j])
Listing 11-2.Creating an Array of Complex Numbers
如果您有一個需要處理的資料矩陣,您可以簡單地提交一個列表列表,其中每個列表都是您的矩陣的一行,如清單 11-3 所示。
>>> matrix1 = np.array([[1, 2], [3, 4]])
>>> matrix1
array([[1, 2],
[3, 4]])
Listing 11-3.Creating a Matrix
如果您還沒有準備好資料,但是想要一個儲存資料的地方,有一個函式可以建立一個固定大小和特定資料型別的空陣列。例如,清單 11-4 展示瞭如何建立一個空的二維整數陣列。
>>> empty1 = np.empty([2, 2], dtype=int)
>>> empty1
array([[-1073741821, -1067949133],
[ 496041986, 19249760]])
Listing 11-4.Creating an Empty Array of Integers
這個函式的問題是,它可能無法以任何方式初始化這些值,這取決於它所執行的作業系統。你最後得到的只是那些記憶體位置中存在的資料。雖然這稍微快了一點,但這確實意味著您需要意識到新陣列中的初始值是垃圾資料。如果需要從一些初始值開始,可以從 0 或 1 開始,如清單 11-5 所示。
>>> zero1 = np.zeros((2, 3), dtype=float)
>>> zero1
array([[0., 0., 0.],
[0., 0., 0.]])
>>> ones1 = np.ones((3, 2), dtype=int)
>>> ones1
array([[1, 1],
[1, 1],
[1, 1]])
Listing 11-5.Creating Arrays of Zeroes and Ones
請注意,對於新建立的陣列的維度,這兩個函式採用一系列值,而不是一個列表。
11-2.複製陣列
問題
您需要複製一個陣列以供進一步處理。
解決辦法
在程式的不同部分共享資料有三種方式:無複製訪問、淺層複製和深層複製。
它是如何工作的
透過一次使用多個變數,可以使程式的不同部分可以訪問陣列。在清單 11-6 中,您可以看到如何將同一個陣列賦給兩個不同的變數。
>>> a = np.ones((6,), dtype=int)
>>> a
array([1, 1, 1, 1, 1, 1])
>>> b = a
Listing 11-6.Using No-Copy Sharing
和 Python 的其他部分一樣,這兩個變數指向記憶體中的同一個實際物件。您可以使用其中任何一個來影響實際物件。
第二種型別的訪問共享是透過淺層複製,其中不複製資料本身,只複製關於資料的資訊。這是可能的,因為陣列物件由兩部分組成。第一個是儲存在陣列中的資料,而第二個包含關於陣列的後設資料,例如陣列的形狀。清單 11-7 展示瞭如何透過建立一個檢視來建立一個淺層副本。
>>> view1 = ones1.view()
>>> # Do these variables point to the same object?
>>> view1 is ones1
False
>>> view1.base is ones1
True
Listing 11-7.
Shallow Copies
您可以透過使用新檢視的base
屬性來訪問原始物件。您可以透過檢視更改後設資料,如清單 11-8 所示。
>>> view1.shape = 2,3
>>> ones1
array([[1, 1],
[1, 1],
[1, 1]])
>>> view1
array([[1, 1, 1],
[1, 1, 1]])
Listing 11-8.Changing the Shape of a View
這將改變儲存資料的矩陣的形狀(列數和行數)。第三種複製形式是深度複製,即複製陣列的所有部分。這由copy
方法處理,如清單 11-9 所示。
>>> copy1 = a.copy()
>>> a is copy1
False
>>> a is copy1.base
False
Listing 11-9.
Deep Copy
of an Array
11-3.訪問陣列資料
問題
您需要訪問陣列的單個元素或子部分。
解決辦法
您可以使用多維列表索引來訪問單個元素,並且可以使用切片來訪問子部分。
它是如何工作的
對於一維陣列,可以使用與列表相同的索引來訪問單個元素。清單 11-10 顯示了一個簡單的例子。
>>> a[1] = 2
>>> a
array([1, 2, 1, 1, 1])
Listing 11-10.Changing the Value of an Array Element
切片也以同樣的方式工作,如清單 11-11 所示。
>>> a[1:3]
array([2, 1])
Listing 11-11.Getting a Slice of an Array
需要注意的一點是,slice 實際上返回的是原始陣列的一個淺層副本,因此不會複製原始資料。
當處理多維陣列時,您只需透過為每個額外的維度新增一個額外的值來擴充套件索引。例如,清單 11-12 展示瞭如何從矩陣中獲取單個元素。
>>> ones1[1, 1] = 2
>>> ones1
array([[1, 1],
[1, 2],
[1, 1]])
Listing 11-12.Accessing One Element from a Matrix
如果您對獲取單個行感興趣,您可以使用清單 11-13 中的例子。
>>> ones1[1, : ]
array([1, 2])
Listing 11-13.Selecting a Row from a Matrix
11-4.操作矩陣
問題
你需要操縱一個給定的矩陣。這包括反演、轉置和計算範數。
解決辦法
包含一整套線性代數工具來處理矩陣操作。
它是如何工作的
如果你從一個簡單的 2 乘 2 矩陣開始,你可以用清單 11-14 中的程式碼轉置它。
>>> a = np.array([[1.0, 2.0], [3.0, 4.0]])
>>> np.linalg.inv(a)
array([[-2., 1.],
[1.5, -0.5]])
Listing 11-14.Inverting a Matrix
linalg
子模組也提供了一個計算定額的函式,如清單 11-15 所示。
>>> np.linalg.norm(a)
5.4772255750516612
Listing 11-15.Finding a Norm
如果你想尋找一個矩陣的軌跡,這實際上是一個陣列物件的方法,如清單 11-16
>>> a.trace()
5.0
Listing 11-16.Finding the Trace of a Matrix
矩陣的轉置也是陣列的一種方法,如清單 11-17 所示。
>>> a.transpose()
array([[1., 3.],
[2., 4.]])
Listing 11-17.Finding the Transpose of a Matrix
11-5.計算快速傅立葉變換
問題
你需要計算一個快速傅立葉變換來觀察一些資料集合的頻譜。
解決辦法
Numpy
提供了一套不同型別的 FFT(快速傅立葉變換)函式。
它是如何工作的
離散 FFT 可用於一維、二維或 n 維資料。然而,每一種情況的數學方法都非常不同。所以Numpy
為每種情況提供了獨立的函式,如清單 11-18 所示。
# a is a 1-dimensional array
np.fft.fft(a)
# b is a 2-dimensional array
np.fft.fft2(b)
# c is a 3-dimensional array
np.fft.fftn(c)
Listing 11-18.Discrete FFTs
如你所見,所有的 FFT 功能實際上都被安排在一個名為fft
的Numpy
子模組中。如果您使用的資料集大於所選 FFT 函式的適用範圍,則使用最後 x 個軸。例如,如果您在一維 FFT 中使用陣列c
,它將使用最後一個軸作為計算的輸入。如果你願意,你可以用axis
引數指定一個不同的軸,如清單 11-19 所示。
np.fft.fft(c, axis=1)
Listing 11-19.FFT Over Other Axes
11-6.將檔案資料載入到陣列中
問題
您希望將資料從檔案直接載入到陣列中。
解決辦法
Numpy
可以讀寫純文字檔案,以及自己特殊的二進位制格式。
它是如何工作的
要從純文字檔案中讀取資料,可以使用函式loadtxt()
,如清單 11-20 所示。
>>> txt1 = np.loadtxt('mydata.txt')
Listing 11-20.Reading in a Text File
此函式假設您的資料以列和行的形式排列,其中每行就是一行。定義列是透過用其他字元分隔各個值來完成的。預設情況下,這是透過空白來完成的。科學資料的常用格式是逗號分隔值(CSV)。如果是這種情況,您可以用清單 11-21 中給出的程式碼載入您的資料。
>>> txt2 = np.loadtxt('mydata.txt', delimiter=',')
Listing 11-21.Loading a CSV File
如果你有以Numpy
的特殊二進位制格式儲存的資料,你可以使用一個簡單的load
命令將其載入回記憶體,如清單 11-22 所示。
>>> data = np.load('mydata.npy')
Listing 11-22.Loading Binary Data
11-7.儲存陣列
問題
您希望將陣列中的資料儲存到磁碟。
解決辦法
與載入資料一樣,儲存資料時有幾個選項。您可以將其儲存為Numpy
的二進位制格式,也可以儲存為某種原始文字格式。
它是如何工作的
要使用Numpy
的二進位制格式儲存資料,您可以簡單地使用save
函式,如清單 11-23 所示。
>>> np.save('mydata.npy', data)
Listing 11-23.Saving Data Using Numpy’s Binary Format
如果你在上面的呼叫中給它的檔名沒有一個.npy
副檔名,一個將被新增到它裡面。相反,如果您想將資料儲存到一個純文字檔案中,以便其他程式可以使用,您可以使用savetxt
函式呼叫,如清單 11-24 所示。
>>> np.savetxt('mydata.csv', data, delimiter=',')
Listing 11-24.Saving a CSV File
在這種情況下,您顯式地將分隔符設定為逗號,得到一個 CSV 檔案。如果不設定分隔符,預設為單個空格字元。
11-8.生成隨機數
問題
你需要生成高質量的隨機數。
解決辦法
Numpy
提供一個 Mersenne Twister 偽隨機數發生器,提供非常優質的隨機數。它可以提供基於幾種分佈的隨機數,如二項式分佈、卡方分佈、伽瑪分佈和指數分佈。
它是如何工作的
如果您需要使用特定分佈的隨機數,您可以使用RandomState
提供的方法來生成它們。清單 11-25 展示瞭如何從幾何分佈中生成一個隨機值。
>>> rand1 = np.random.geometric(p=0.5)
Listing 11-25.Generating Random Numbers from a Geometric Distribution
大多數發生器都包含控制每個發行版細節的引數。它們通常還包含一個size
引數,您可以用它來請求一組隨機值,而不僅僅是一個。
如果您想要一個可重複的隨機數序列(例如,如果您正在測試程式碼),您可以用清單 11-26 中的程式碼顯式地設定一個種子。
>>> np.random.seed(42)
Listing 11-26.Setting a Seed for Random Number Generation
當RandomState
被建立時,這個種子也被初始化。如果你不提交一個,那麼RandomState
要麼嘗試從作業系統隨機數生成器(例如,Linux 上的/dev/urandom
)讀取一個值,要麼根據時鐘設定種子。
在大多數情況下,您可以獲得與清單 11-27 中的程式碼一起使用的隨機數型別。
>>> rand2 = np.random.random()
Listing 11-27.Generating Random Numbers
11-9.計算基本統計資料
問題
您需要對儲存在陣列中的資料進行基本的統計。
解決辦法
Numpy
提供了一系列統計函式,可以對不同維度的陣列進行操作。你可以做你可能需要的所有標準的簡單統計分析。
它是如何工作的
給定一組儲存在一維陣列中的資料,您可以用清單 11-28 中的程式碼找到平均值、中值、方差和標準差。
>>> a = np.array([1, 2, 3, 4, 5])
>>> np.mean(a)
3.0
>>> np.median(a)
3.0
>>> np.var(a)
2.0
>>> np.std(a)
1.4142135623730951
Listing 11-28.Basic Statistics
如果您有多維資料,您可以選擇沿著哪個軸計算這些統計資料。
11-10.計算直方圖
問題
您有一系列的資料,您需要將這些資料分組並計算直方圖。
解決辦法
Numpy
包含一些相關的函式來處理直方圖,包括一維直方圖和多維直方圖。
它是如何工作的
假設您已經將資料序列儲存在變數b
中,您可以使用清單 11-29 中的程式碼生成一個直方圖。
>>> b = np.array([1,2,1,2,3,1,2,3,3,2,1])
>>> np.histogram(b)
(array([4, 0, 0, 0, 0, 4, 0, 0, 0, 3], dtype=int64),
array([ 1\. , 1.2, 1.4, 1.6, 1.8, 2\. , 2.2, 2.4, 2.6, 2.8, 3\. ]))
Listing 11-29.Generating a Simple Histogram
預設情況下,Numpy
會嘗試將您的資料分組到 10 個箱中。第一個陣列告訴您每個容器中有多少個值,第二個陣列告訴您每個容器的邊界。您可以透過新增第二個引數來設定箱子的數量,如清單 11-30 所示。
>>> np.histogram(b, 3)
(array([4, 4, 3], dtype=int64),
array([ 1\. , 1.66666667, 2.33333333, 3\. ]))
Listing 11-30.Histograms with a Set Bin Count
十二、併發
幾十年來,計算機的速度越來越快,但我們開始遇到一些物理學的限制。這意味著為了完成更多的工作,我們需要並行使用多個過程。Python 中有幾種技術可以支援程式碼的併發執行。
第一種技術是使用執行緒來分解工作。這種方法的主要問題是,它受到由 GIL(全域性直譯器鎖)引起的瓶頸的影響。執行 I/O 或使用特定模組(如 numpy)的執行緒可以繞過這個瓶頸。如果你需要做更多的計算工作,你可以使用程序來代替。在本章中,你將看到 Python 中的幾個可用選項。
12-1.建立執行緒
問題
你想建立一個執行緒來完成一些任務。
解決辦法
Python 標準庫包含一個名為threading
的模組,該模組包含一個Thread
類。
它是如何工作的
主類Thread
支援並行執行多個函式。清單 12-1 展示瞭如何建立和執行一個基本執行緒。
import threading
def print_sum():
print('The sum of 1 and 2 is 3')
my_thread = threading.Thread(target=print_sum)
my_thread.start()
Listing 12-1.Creating a Thread
您應該注意到,您建立的執行緒在您呼叫start()
方法之前不會開始執行目標函式。如果這個函式是一個執行時間更長的函式,您可以透過使用is_alive()
方法來檢查它是否還在執行。它將返回一個布林值,告訴你它是否還在執行。如果沒有給定執行緒的結果就無法繼續,可以呼叫join()
方法強制等待,直到執行緒全部完成。
12-2.使用鎖
問題
您需要控制執行緒對特定資源的訪問。
解決辦法
threading
模組包括一個Lock
類來控制執行緒訪問。
它是如何工作的
當執行緒需要安全地訪問全域性資源時,使用鎖。清單 12-2 展示瞭如何建立和使用一個鎖物件。
import threading
sum = 0
my_lock = threading.Lock()
def adder():
global sum, my_lock
my_lock.acquire()
sum = sum + 1
my_lock.release()
my_thread = threading.thread(target=adder)
my_thread.start()
Listing 12-2.Creating a Lock Object
預設情況下,如果鎖已經被另一個執行緒獲取,那麼鎖物件的acquire()
方法就會阻塞。相反,如果你想在等待鎖的時候做些別的事情,你可以使用acquire()
方法中的引數blocking=False
。它將立即返回,給出一個布林值,表明獲取嘗試是否成功。
12-3.設定障礙
問題
您需要透過設定一個公共停止點來同步執行緒活動。
解決辦法
threading
模組包括一個可用於設定公共停止點的障礙物件。
它是如何工作的
在許多語言中,使用屏障涉及簡單的函式呼叫,而在 Python 中,屏障是用物件來管理的。清單 12-3 展示瞭如何為五個執行緒建立一個屏障。
import threading
b = threading.Barrier(5, timeout=10)
Listing 12-3.Creating a Barrier Object
正如您所看到的,您必須明確地告訴 barrier 物件有多少執行緒將使用它。您還可以設定一個超時值,該值是允許執行緒等待屏障得到滿足的最長時間。為了實際使用 barrier 物件,每個執行緒都需要呼叫wait()
方法。
12-4.建立流程
問題
您需要為多重處理建立多個程序。
解決辦法
Python 標準庫包括一個名為multiprocessing
的模組,該模組包含一個Process
類。
它是如何工作的
如果您的程式碼受到 GIL 的影響,一種解決方法是使用類Process
在主 Python 程序之外生成其他任務。該介面與執行緒的介面非常相似。例如,清單 12-4 展示瞭如何建立一個新流程並開始執行。
import multiprocessing
def adder(a, b):
return a+b
proc1 = multiprocessing.Process(target=adder, args=(2,2))
proc1.start()
proc1.join()
Listing 12-4.Creating a New Process
正如您所看到的,您新建立的流程物件是用start()
方法執行的,您可以用join()
方法強制程式碼的主要部分等待結果。
12-5.程序間的通訊
問題
您需要將資訊從一個流程物件傳送到另一個流程物件。
解決辦法
multiprocessing
模組有兩個可用於程序間通訊的類:pipe
和queue
類。
它是如何工作的
因為流程物件在 Python 直譯器的主要部分之外執行,所以與它們或它們之間的通訊需要更多的工作。最基本的通訊形式是管道。清單 12-5 展示瞭如何建立一個新的管道物件。
import multiprocessing
def func(pipe_end):
pipe_end.send(['hello', 'world'])
pipe_end.close()
parent_end, child_end = multiprocessing.Pipe()
proc1 = multiprocessing.Process(target=func, args=(child_end,))
proc1.start()
print(parent_end.recv())
proc1.join()
Listing 12-5.Creating a Pipe
如您所見,管道是一個簡單的通訊通道,有兩端,程序可以從中讀取和寫入。管道是全雙工的,所以訊息可以從兩端傳送。然而,管道的主要問題是末端一次只能被一個程序使用。如果兩個程序試圖同時從同一端讀取或寫入,資料可能會損壞。
另一種不同的技術是使用佇列物件與。佇列是一個 FIFO(先進先出)物件。它可以接受來自多個程序的資料,並且多個程序可以從佇列中取出資料。清單 12-6 展示瞭如何建立和使用佇列物件。
import multiprocessing
def func(queue1):
queue1.put(['hello', 'world'])
my_queue = multiprocessing.Queue()
proc1 = multiprocessing.Process(target=func, args=(my_queue,))
proc1.start()
print(my_queue.get())
proc1.join()
Listing 12-6.Creating a Queue
12-6.創造一批工人
問題
您需要啟動並執行一個程序池。
解決辦法
Python 標準庫模組multiprocessing
包含一個Pool
類來管理任務佇列。
它是如何工作的
當您有一系列需要處理的任務時,您可以建立一個程序池來完成這些任務。清單 12-7 展示瞭如何建立一個由四個工作程序組成的池,並讓它們處理一堆任務。
import multiprocessing
def adder(a):
return a+a
pool1 = multiprocessing.Pool(processes=4)
Listing 12-7.Creating a Pool of Processes
這個新建立的池物件有幾種不同的方法來在程序之間劃分任務。這些方法有阻塞版本和非阻塞版本。例如,清單 12-8 展示瞭如何使用map
方法。
# This method blocks untill all processes return
pool1.map(adder, range(10))
# This method returns a result object
results = pool1.map_async(adder, range(10))
results.wait()
Listing 12-8.Mapping a Function to a Pool of Processes
清單 12-8 中的最後一行用於阻塞和等待,直到返回所有結果。當您準備使用外包任務的所有結果時,可以使用它。
12-7.建立子流程
問題
您需要生成一個子流程來處理外部任務。
解決辦法
Python 標準庫包含一個名為subprocess
的模組,可以生成外部程序。
它是如何工作的
subprocess
模組旨在取代執行外部流程的舊的os.system
和os.spawn
方法。該模組包含一個名為run()
的方法,這是使用子流程的常用方法。例如,清單 12-9 展示瞭如何在 Linux 機器或 Mac OS 機器上獲得當前目錄下的檔案列表。
import subprocess
results = subprocess.run(['ls', '-l'], stdout=subprocess.PIPE)
print(results.stdout)
Listing 12-9.Spawning a Subprocess
返回的 results 物件包含許多關於外部程序如何執行的資訊,包括退出程式碼。
12-8.安排事件
問題
您需要為以後的某個時間安排任務。
解決辦法
Python 標準庫包含一個名為sched
的模組,其中有幾個物件和方法,在不同時間排程工作時非常有用。
它是如何工作的
為了安排未來的任務,您可以建立一個Scheduler
物件來獲取事件佇列並管理它們。您可以使用enter()
方法將事件新增到佇列中,如清單 12-10 所示。
import sched, time
def print_time():
print(time.time())
my_sched = sched.scheduler()
my_sched.enter(10, 1, print_time)
Listing 12-10.Creating a Scheduler
當事件觸發時,enter()
方法接受一個延遲、一個優先順序和一個要執行的函式。如果您想在特定時間觸發事件,可以使用enterabs()
方法。一旦您有了一個更大的事件列表,您就可以使用排程器的queue
屬性來檢查當前佇列。如果您到達了程式的末尾,您可以使用run()
方法強制程式碼停止並等待,直到所有事件都結束。
十三、工具
所有程式語言都使用外部工具,以使程式設計師的開發和執行更容易。Python 在這方面也沒有什麼不同。在這一章中,你將會看到一些為 Python 程式語言編寫程式碼時最常用的外部工具。這些包括設定環境的工具、更好的直譯器、完整的編碼環境,甚至是外殼替換。
13-1.建立虛擬環境
問題
您希望為特定的 Python 程式建立和使用虛擬環境。
解決辦法
建立和管理虛擬環境的常用方法是透過virtualenv
工具。
它是如何工作的
Python 的優勢之一,它對第三方模組非常健康的選擇,也是它的問題之一。你很快就會得到一個大規模的模組庫,這些模組只在某些時候使用。最小化這個問題的一個方法是為每個專案建立一個單獨的環境,這樣你就可以在這個環境中只安裝你所需要的模組。第一步,安裝virtualenv
。清單 13-1 展示瞭如何用 pip 來做這件事。
pip install virtualenv
Listing 13-1.Installing virtualenv
安裝後,您可以建立新的虛擬環境。清單 13-2 展示瞭如何為一個新專案建立一個新的空白環境。
virtualenv project1
Listing 13-2.Creating a New Virtual Environment
這個命令建立一個名為project1
的新目錄,並安裝執行 Python 程式所需的一切,以及安裝和管理模組。為了使用這個新環境,您將目錄更改為project1
子目錄,並獲取 shell 指令碼./bin/activate
。許多最常用的 Linux shells 都有不同的版本,還有 Windows cmd
shell 和 Windows powershell
。這個指令碼設定了幾個環境變數來使用包含的直譯器和模組庫。
當您使用完有問題的環境後,您可以執行指令碼deactivate
來撤銷環境更改。如果您決定不再需要某個特定的虛擬環境,只需刪除整個相關目錄及其所有內容。完整文件可在 https://virtualenv.pypa.io/en/stable/
獲得。
13-2.使用 Ipython Shell
問題
您希望使用更好的直譯器外殼,而不是 Python 直譯器使用的預設外殼。
解決辦法
IPython 直譯器外殼在預設的 Python 直譯器外殼上提供了許多增強。
它是如何工作的
您可以使用清單 13-3 中所示的命令安裝最新版本的 IPython。
pip install ipython
Listing 13-3.Installing IPython
要使用這個直譯器,您只需要在命令提示符下執行命令ipython
。它與許多 Linux shells 非常相似,因為它提供了製表符補全、命令歷史和更復雜的命令編輯等功能。
IPython 直譯器中更強大的功能之一是稱為 magics 的命令集。這些特殊的命令以一個%
符號開始,後面跟著一些關鍵字。例如,清單 13-4 展示瞭如何為一個給定的操作計時。
%timeit x = range(1000000)
Listing 13-4.Using the timeit Magic Function
有些魔術可以執行外部指令碼,載入或儲存命令,甚至影響 IPython 直譯器的顏色。清單 13-5 展示瞭如何將之前的一系列命令儲存到一個檔案中。
# Saving a series of commands to a file
%save myfile.py 2-5 10
Listing 13-5.Saving to a File with Ipython Magics
這樣,您可以從一次實驗中儲存有用的行。load
魔法函式將檔案的內容讀入 IPython 前端,就好像您剛剛輸入了它們一樣。清單 13-6 展示瞭如果你載入一個只有一條print
語句的檔案會發生什麼。
In [7]: %load myfile.py
In [8]: print("Hello world")
Listing 13-6.Loading a File with IPython Magic
您還可以建立自己的神奇函式,進一步擴充套件 IPython 的功能。主要文件可在 http://ipython.readthedocs.io/en/stable/index.html
找到。
13-3.使用 Jupyter 環境
問題
您希望使用更完整的環境,以便於互動式開發。
解決辦法
你可以使用 Jupyter,它是 IPython 的 web 介面的一個分支。它提供了一個類似於 Maple、Mathematica 或 Matlab 等商業應用的介面。
它是如何工作的
第一步是在您的系統上安裝 Jupyter。清單 13-7 展示瞭如何使用 pip 來完成這項工作。
pip install jupyter
Listing 13-7.Installing Jupyter
在命令提示符下,執行命令jupyter notebook
將啟動 web 伺服器和 Python 引擎,然後啟動連線到新啟動的伺服器的 web 瀏覽器。Jupyter 正迅速成為用 Python 進行科學研究的事實平臺。除了繼承自 IPython 的增強功能,Jupyter 還包括繪製內嵌 matplotlib 圖形等功能。作為 Jupyter 的一部分,實際上還有一套非常完整的其他工具,例如筆記本檢視器和筆記本評分工具,供您在課堂環境中使用 Jupyter 時使用。主文件位於 http://jupyter.readthedocs.io/en/latest/index.html
。
13-4.使用 xonsh 作為替換外殼
問題
Linux 或 Mac OS 上的命令 shell 是非常個人化的選擇。它是您用來與機器上的一切進行互動的環境。您可能希望使用 Python 作為在計算機上工作的命令 shell。
解決辦法
您可以使用名為xonsh
的新專案作為替換命令 shell。
它是如何工作的
可以用 pip 安裝xonsh
,如清單 13-8 所示。
pip install xonsh
Listing 13-8.Installing xonsh
一旦安裝完畢,xonsh
就可以像其他命令 shell 一樣使用。xonsh
至少部分相容 bash。這是指執行其他程式、擁有和使用命令歷史記錄以及處理後臺作業的能力。除了這種常見的功能,您還擁有 Python 引擎的所有功能。這意味著您可以使用 Python 模組在命令 shell 中新增功能。清單 13-9 展示瞭如何從命令列直接獲得隨機數。
jbernard@DESKTOP-QPKN2QC ∼ <branch-timeout> $ import numpy as np
jbernard@DESKTOP-QPKN2QC ∼ <branch-timeout> $ np.random.random()
0.48053753953641054
Listing 13-9.Using Python Modules within xonsh
主文件站點位於 http://xon.sh/index.html
。
十四、測試和除錯
這本書主要是給你一些如何編寫 Python 程式碼的技巧和訣竅。很少的篇幅花在使這段程式碼儘可能好上。在這一章中,你將會看到分析程式碼效能的技術。您還將看到在您編寫的程式碼中出現錯誤時除錯程式的方法。
14-1.為一段程式碼計時
問題
你想對一段程式碼計時,看看它執行了多長時間。
解決辦法
Python 標準庫包括一個名為timeit
的包,它可以多次執行程式碼並獲得平均執行時間。
它是如何工作的
如果您有 Python 語句,您可以從命令列透過timeit
包執行它們,如清單 14-1 所示。
python -m timeit 'print(42)'
10000000 loops, best of 3: 0.035 usec per loop
Listing 14-1.Using the timeit Command
清單 14-2 展示瞭如何在 Python 直譯器中完成類似的任務。
>>> import timeit
>>> timeit.timeit('42*42', number=1000)
0.0001980264466965309
Listing 14-2.Timing Python Code with timeit
如您所見,您需要明確地告訴timeit
執行 Python 語句的次數。
14-2.分析程式碼
問題
您希望分析您的程式碼,看看效能瓶頸在哪裡。
解決辦法
Python 標準庫包括兩個包,可以用來分析您的程式碼:profile
和cProfile
。
它是如何工作的
profile
和cProfile
都為分析工具提供了一個公共介面來檢視程式碼的效能。這兩個包的主要區別在於它們在分析程式碼時各自的效能。包cProfile
是用 C 寫的,所以它對你自己程式碼的執行時間影響很小。然而,為了獲得這樣的速度,無論您在哪個系統上分析程式碼,都需要對它進行編譯。package profile
是用純 Python 寫的,所以執行起來會比cProfile
慢一些。然而,優點是profile
將在您的程式碼執行的任何地方執行,並且很容易擴充套件profile
的功能以新增額外的特性。
有幾種方法可以將分析包用於您自己的程式碼。如果您的程式碼已經捆綁在一組函式中,您可以簡單地在分析器下執行它,如清單 14-3 所示。
>>> def my_func():
… return 42
>>> import profile
>>> profile.run('my_func')
4 function calls in 0.000 seconds
Ordered by: standard name
ncalls tottime percall cumtime percall filename:lineno(function)
1 0.000 0.000 0.000 0.000 :0(exec)
1 0.000 0.000 0.000 0.000 :0(setprofile)
1 0.000 0.000 0.000 0.000 <string>:1(<module>)
1 0.000 0.000 0.000 0.000 profile:0(my_func)
0 0.000 0.000 profile:0(profiler)
Listing 14-3.Running the Profiler
如您所見,您可以獲得相當多的關於程式碼中每個函式呼叫所花費時間的資訊。透過分析器執行您自己的程式碼還有其他幾種方法。如果您想要分析的程式已經打包成一個指令碼檔案,您可以選擇從命令列透過分析器執行它,如清單 14-4 所示。
python -m profile -o myscript.out myscript.py
Listing 14-4.Running the Profiler from the Command Line
該命令將把二進位制版本的分析結果轉儲到檔案myscript.out
中。如果您需要在遠端機器上執行概要分析步驟,但想在以後檢視結果,這是很方便的。使用pstats
包可以看到結果。清單 14-5 展示瞭如何從這個二進位制檔案中獲得基本的統計資料。
>>> import pstats
>>> p = pstats.Stats('myscript.out')
>>> p.print_stats()
Sun Sep 11 20:39:14 2016 myscript.out
9 function calls in 0.000 seconds
Random listing order was used
ncalls tottime percall cumtime percall filename:lineno(function)
2 0.000 0.000 0.000 0.000 C:\Users\berna_000\Anaconda3_4\lib\encodings\cp850.py:18(encode)
1 0.000 0.000 0.000 0.000 :0(print)
2 0.000 0.000 0.000 0.000 :0(charmap_encode)
1 0.000 0.000 0.000 0.000 :0(setprofile)
1 0.000 0.000 0.000 0.000 myscript.py:1(<module>)
1 0.000 0.000 0.000 0.000 profile:0(<code object <module> at 0x000001F1B82040C0, file "myscript.py", line 1>)
0 0.000 0.000 profile:0(profiler)
1 0.000 0.000 0.000 0.000 :0(exec)
<pstats.Stats object at 0x000001F1B8208550>
Listing 14-5.Reading a Profiling Run
在pstats
中有很多選項可以幫助你挖掘結果輸出檔案。
14-3.跟蹤子程式
問題
您需要跟蹤您的程式碼使用了哪些子例程,以檢視您的程式還使用了哪些其他函式。
解決辦法
Python 標準庫包括一個名為trace
的包,它可以為您提供覆蓋列表、呼叫者/被呼叫者關係以及所有被呼叫函式的列表。
它是如何工作的
可以從命令列使用trace
模組,如果您已經將程式碼捆綁在指令碼檔案中,這將非常有用。清單 14-6 展示了你如何做來追蹤你的指令碼。
python -m trace --trace myscript.py
--- modulename: myscript, funcname: <module>
myscript.py(1): print('42')
--- modulename: cp850, funcname: encode
cp850.py(19): return codecs.charmap_encode(input,self.errors,encoding_map)[0]
42 --- modulename: cp850, funcname: encode
cp850.py(19): return codecs.charmap_encode(input,self.errors,encoding_map)[0]
--- modulename: trace, funcname: _unsettrace
trace.py(77): sys.settrace(None)
Listing 14-6.Tracing a Program
您可以使用以下跟蹤選項來收集不同種類的資料:
| `--count` | 計算每條語句執行的次數 | | `--trace` | 執行時顯示行 | | `--listfuncs` | 執行時顯示功能 | | `--trackcalls` | 顯示呼叫關係 |您也可以在 Python 程式碼中使用trace
。它包括兩個主要類別:Trace
和CoverageResults
。清單 14-7 展示瞭如何透過一個命令進行跟蹤。
>>> import trace
>>> tracer = trace.Trace()
>>> tracer.run('print("Hello World")')
Hello World
--- modulename: trace, funcname: _unsettrace
trace.py(80): sys.settrace(None)
Listing 14-7.Tracing a Command in Python Code
14-4.跟蹤記憶體分配
問題
您需要跟蹤程式中的記憶體分配,以瞭解記憶體的使用情況。
解決辦法
Python 標準庫包括一個名為tracemalloc
的模組,它可以跟蹤記憶體分配和記憶體使用統計。
它是如何工作的
要使用tracemalloc
,您需要啟動它,以便它可以隨著時間的推移收集記憶體資訊。清單 14-8 展示瞭如何獲得記憶體使用中的前 10 名違規者。
import tracemalloc
tracemalloc.start()
# Run you code here
snapshot = tracemalloc.take_snapshot()
top_stats = snapshot.statistics('lineno')
for curr_stat in top_stats:
print(curr_stat)
Listing 14-8.Getting Memory Statistics
您還可以透過拍攝多個快照來檢視記憶體使用如何隨時間變化。有益的是,快照物件有一個名為compare_to()
的方法,允許您檢視它們之間的區別,如清單 14-9 所示。
import tracemalloc
tracemalloc.start()
snapshot1 = tracemalloc.take_snapshot()
# run your code
snapshot2 = tracemalloc.take_snapshot()
top_stats = snapshot2.compare_to(snapshot1, 'lineno')
Listing 14-9.Comparing Two Memory Snapshots
然後您可以檢視top_stats
物件,看看記憶體使用是如何隨時間變化的。
14-5.執行單元測試
問題
您希望對程式碼執行單元測試來驗證程式行為。
解決辦法
Python 標準庫包括一個名為unittest
的模組,可用於為您的程式碼構建測試用例。
它是如何工作的
為了建立單元測試,你需要子類化TestCase
類並新增測試用例來驗證你的程式碼。每個測試用例必須以 test 作為字首來命名。然後使用assertEqual()
、assertTrue()
、assertFalse()
和assertRaises()
來驗證條件。清單 14-10 中顯示了一個簡單的例子。
import unittest
class MyTestCase(unittest.TestCase):
def test_the_answer(self):
assertEqual(self.curr_val, 42)
if __name__ == '__main__':
unittest.main()
Listing 14-10.A Simple Test Case
這只是對在 Python 中使用測試用例的簡短介紹。有整本書致力於組織和設計測試驅動程式碼。
14-6.除錯程式碼
問題
您需要除錯程式碼中出現的問題。
解決辦法
Python 標準庫包括一個名為pdb
的包,它為程式碼的操作提供了一個除錯介面。
它是如何工作的
如果你想互動式地使用它,你可以在你的 Python 直譯器中匯入pdb
模組,並用它來執行你的程式碼,如清單 14-11 所示。
>>> import pdb
>>> pdb.run('my_func()')
Listing 14-11.Running Code Under the Debugger
這將進入偵錯程式的互動模式。這透過提示(pdb)
突出顯示。介面類似於其他基於文字的偵錯程式,比如gdb
。如果你想在偵錯程式中執行整個指令碼,你可以從命令列執行,如清單 14-12 所示。
python -m pdb myscript.py
Listing 14-12.Debugging a Script File
如果您的指令碼異常退出,這將使您進入偵錯程式的事後分析會話。如果您大致知道問題可能出在哪裡,您可以新增一行程式碼來中斷偵錯程式,如清單 14-13 所示。
import pdb; pdb.set_trace()
Listing 14-13.Dropping into the Debugger
然後,您可以逐句透過程式碼來定位程式碼中問題的根源。如果您處於互動式會話中,可以透過偵錯程式手動執行程式碼。清單 14-14 展示了一個單步執行函式的例子。
>>> import pdb
>>> def myfunc():
.... print("Hello World")
>>> pdb.run('myfunc()')
> <string>(1)<module>()->None
(Pdb) step
--Call--
> <ipython-input-11-b0e3e2c712c8>(1)myfunc()
-> def myfunc():
(Pdb) step
> <ipython-input-11-b0e3e2c712c8>(2)myfunc()
-> print("Hello World")
(Pdb) step
Hello World
--Return--
> <ipython-input-11-b0e3e2c712c8>(2)myfunc()->None
-> print("Hello World")
(Pdb) step
--Return--
> <string>(1)<module>()->None
(Pdb) step
> /usr/lib/python3.4/bdb.py(435)run()
-> self.quitting = True
(Pdb) step
Listing 14-14.Stepping Through a Function with pdb
十五、C 和其他擴充套件
Python 的一大優點是它不是每項工作的最佳工具,更重要的是,它知道它不是每項工作的最佳工具。由於這種自我意識,Python 從一開始就被設計成可以用 c 語言編寫的程式碼進行擴充套件。這種能力是由一個名為 Cython 的模組提供的,該模組可從 http://cython.org
獲得。在這一章中,你將看到一些不同的方法,你可以把 Cython 包含在你自己的 Python 程式中,以提高它的效能或者增加額外的功能。
15-1.編譯 Python 程式碼
問題
你想把你的 Python 程式碼編譯成 C 以獲得加速。
解決辦法
Cython 包提供了一種混合編譯 C 程式碼和 Python 的機制。
它是如何工作的
初始設定適用於本章中的以下所有示例。你的系統上需要有一個 C 編譯器。如果您執行的是 Linux 或 Mac OS,那麼您可以使用 gcc 作為所需的編譯器。要在 Mac OS 上獲得編譯器,您需要安裝 XCode 包。至於 Windows,涉及的步驟更多。Cython 文件中有一個完整的附錄,專門用於指導如何設定 Windows。同樣,您需要安裝 Cython。您可以從原始碼安裝它,也可以使用 pip 安裝它,如清單 15-1 所示。
pip install --user cython
Listing 15-1.Installing Cython with pip
一旦所有的東西都安裝好了,你就需要編寫將要編譯成 c 的 Python 程式碼。這些程式碼儲存在一個以.pyx
結尾的檔案中,而不是以.py
結尾。然後,這個 Cython 原始檔以幾種不同的方式之一進行編譯。對於較大的專案,處理編譯的最靈活和健壯的方法是編寫一個setup.py
檔案並使用distutils
。在這麼短的篇幅裡介紹這個方法有點太複雜了。令人高興的是,有一些其他更簡單的方法可以讓您立即在程式碼中使用 Cython。
清單 15-2 展示了一個例子。只有一個功能的檔案。
def print_msg():
print("Hello World")
Listing 15-2.HelloWorld.pyx File
Cython 包含一個名為pyximport
的模組,它將編譯。pyx
當您嘗試匯入檔案時,它們會在後臺執行。清單 15-3 展示瞭如何在互動式 Python 會話中使用它。
>>> import pyximport
>>> pyximport.install()
>>> import HelloWorld
>>> print_msg()
Hello World
Listing 15-3.Using pyximport
這適用於整個原始檔。如果您希望編譯的程式碼段更短,您可以讓 Cython 直接在 Python 原始碼的中間進行內聯編譯。清單 15-4 給出了一個內聯編譯程式碼的例子。
>>> import cython
>>> def my_adder(a, b):
... ret = cython.inline("return a+b")
...
Listing 15-4.Using Inlined Cython Code
為了提高效率,內聯程式碼的編譯版本被快取。
15-2.使用靜態型別
問題
你想透過給物件一個型別來加速對它們的訪問。
解決辦法
您可以安裝 Cython 模組以及支援的 C 編譯器,來定義新型別,這些新型別的訪問和處理速度比 Python 物件快得多。
它是如何工作的
為了使用靜態型別,Cython 引入了一個名為cdef
的新關鍵字。當使用這種方法時,您可以獲得比用 Cython 編譯 Python 程式碼更快的速度。清單 15-5 展示了一個整合問題的例子。
def f(x):
return x**2-42
def integrate_f(a, b, N):
s = 0
dx = (b-a)/N
for I in range(N):
s += f(a+i*dx)
return s*dx
Listing 15-5.Pure Python Integration Problem
在 Cython 下編譯它將提供一定程度的加速,但是仍然會發生型別檢查。這在迴圈中尤其昂貴,因為變數被多次訪問。清單 15-6 展示了相同的例子,除了使用了cdef
關鍵字。
def f(double x):
return x**2-42
def integrate_f(double a, double b, int N):
cdef int i
cdef double s, dx
s = 0
dx = (b-a)/N
for I in range(N):
s += f(a+i*dx)
return s*dx
Listing 15-6.Integration Problem Using Static Typing
這段程式碼去掉了所有那些代價高昂的型別檢查,在試圖最佳化程式碼時會是一個很大的幫助。為了編譯這些檔案,您可以使用 Cython 命令列工具來生成一個 C 原始檔,該檔案可以被編譯成一個共享物件,並匯入到 Python 中。假設上面的例子儲存在一個名為mycode.pyx
的檔案中,清單 15-7 展示瞭如何使用 GCC。
cython myfile.pyx
gcc -shared -o myfile.so myfile.c `python3-config --includes`
Listing 15-7.Compiling Cython Code Manually
然後,您可以從 Python 中匯入這個新編譯的共享物件。
15-3.從 C 中呼叫 Python
問題
您希望能夠從 C 程式中呼叫 Python 程式碼。
解決辦法
標準庫包括名為Python.h
的標頭檔案,這使得 Python 可以從 c 中呼叫。
它是如何工作的
當您想從 C 中呼叫 Python 程式碼時,有兩個主要的函式可用:Py_Initialize()
和Py_Finalize()
。第一個函式啟動 Python 直譯器,第二個函式再次關閉它。在兩次函式呼叫之間,您可以執行您的 Python 程式碼。清單 15-8 展示了一個可以執行一串 Python 程式碼的例子。
#include "Python.h"
void run_pycode(const char* code) {
Py_Initialize();
PyRun_SimpleString(code);
Py_Finalize();
}
Listing 15-8.Running Python Code from C
這對於較短的程式碼來說很好,但是如果你有一個完整的指令碼,你可以從你的 C 程式中執行它,如清單 15-9 所示。
#include "Python.h"
Int main() {
Py_Initialize();
FILE* file = fopen("./my_script.py", "r");
PyRun_SimpleFile(file, "./my_script.py");
Py_Finalize();
}
Listing 15-9.Running a Python Script from C
15-4.從 Python 呼叫 C
問題
您希望從 Python 程式中呼叫外部 C 程式碼。
解決辦法
標準的 Python API 包括幫助連線 Python 和 c 的程式碼,Cython 包使這種通訊更加容易。
它是如何工作的
關鍵字cdef extern from
告訴 Cython 匯入 C 函式的位置。清單 15-10 顯示了一個.pyx
檔案的例子。
cdef extern from "hello_world.c":
void print_msg()
Listing 15-10.Importing External C Code
清單 15-11 顯示了相關的 C 原始碼檔案。
static void print_msg() {
printf("Hello World");
}
Listing 15-11.Imported C Code
這是一個比 Python 中包含的標準 API 更簡單的匯入 C 程式碼的介面。
十六、Arduino 和 RPi 秘籍
對於業餘發明家來說,Raspberry PI 和 Arduino 是設計和構建自己技術的巨大資源。Raspberry Pi 已經成為事實上的單板計算機(或 SBC ), Python 已經成為 Pi 上使用的事實上的語言。在其他情況下,您可能實際上需要一個微控制器來提供與計算機的介面,或者作為一個更有限的控制單元。在這些情況下,Arduino 的所有變體都成為了標準。雖然在這一章中你將只探索 Arduino 和 Raspberry Pi,但是還有其他幾個選項可供你選擇。
16-1.向 Arduino 傳送資料
問題
您想要向 Arduino 傳送資料或指令。
解決辦法
您可以使用 Python 模組pyserial
透過序列連線與 Arduino 通訊。
它是如何工作的
可以用pip
安裝pyserial
,如清單 16-1 所示。
pip install --user pyserial
Listing 16-1.Installing pyserial
您需要在 Arduino 上預裝一個程式,該程式可以接受指令或資料。完成後,使用序列電纜將 Arduino 連線到執行 Python 的計算機。然後你可以讓你的 Python 程式碼向 Arduino 傳送資訊,比如清單 16-2 中的程式碼。
>>> import serial
>>> ser = serial.Serial('/dev/tty.usbserial', 9600)
>>> ser.write(b'5')
Listing 16-2.Sending Data to an Arduino
在第二行中,您需要將裝置條目/dev/tty.usbserial
替換為適合您的系統的位置。在write()
方法中,需要前面的b
將 Unicode 字串轉換成簡單的位元組字串。
16-2.從 Arduino 讀取資料
問題
你想從 Arduino 讀取資料。
解決辦法
同樣,您可以使用pyserial
模組從 Arduino 的序列連線中讀取資料。
它是如何工作的
假設預先載入在 Arduino 上的程式碼期望透過序列連線傳送資料,您可以使用 Python 模組pyserial
來讀取這些資料。清單 16-3 給出了一個例子。
>>> import serial
>>> ser = serial.Serial('/dev/tty.usbserial', 9600)
>>> data = ser.readline()
Listing 16-3.Reading Data from an Arduino
readline()
方法希望在傳送的資料末尾有一個新行,所以確保 Arduino 程式碼在每次輸出資料時都傳送這個新行。
16-3.寫入 Raspberry Pi 的 GPIO 匯流排
問題
你想在 Raspberry Pi 的 GPIO 匯流排上寫輸出。
解決辦法
RPi.GPIO
模組提供了 Python 程式碼和物理 GPIO 匯流排之間的介面。
它是如何工作的
模組RPi.GPIO
包含在 Raspbian 的包庫中。清單 16-4 展示瞭如何在你的樹莓 Pi 上安裝它。
sudo apt-get install python-dev python-rpi.gpio
Listing 16-4.Installing the RPi.GPIO Module
為了使用RPi.GPIO
模組,需要幾個步驟來設定 GPIO 匯流排和處理通訊。清單 16-5 顯示了一個將 GPIO 引腳設定為高電平的例子。
import RPi.GPIO as GPIO
GPIO.setmode(GPIO.BOARD)
GPIO.setup(1, GPIO.OUT, initial=GPIO.LOW)
GPIO.output(1, GPIO.HIGH)
Listing 16-5.Setting a GPIO Pin to High
setmode()
方法設定要使用的管腳編號方案。GPIO.BOARD
使用電路板上的引腳編號。setup()
方法將一個特定的管腳設定為輸入或輸出,並可選地設定管腳的初始值。然後,您可以使用output()
方法將輸出傳送到有問題的管腳。如您所見,GPIO 匯流排輸出二進位制資料,要麼為低電平,要麼為高電平。
16-4.從 Raspberry Pi 的 GPIO 匯流排讀取
問題
你想從 Raspberry Pi 的 GPIO 匯流排讀取輸入。
解決辦法
您可以使用RPi.GPIO
Python 模組從 GPIO 匯流排讀取資料。
它是如何工作的
清單 16-6 提供瞭如何從一個 GPIO 引腳讀入資料的基本示例。
import RPi.GPIO as GPIO
GPIO.setup(1, GPIO.IN)
if GPIO.input(1):
print('Input was HIGH')
else:
print('Input was LOW')
Listing 16-6.Reading Data from the GPIO Bus
如您所見,輸入以二進位制高電平或低電平接收。