Python快速學習第八天

本文內容全部出自《Python基礎教程》第二版

10.1 模塊

現在你已經知道如何創建和執行自己的程序(或腳本)了,也學會了怎麼用import從外部模塊獲取函數並且爲自己的程序所用:

>>> import math
>>> math.sin(0)
0.0

讓我們來看看怎樣編寫自己的模塊。

 

10.1.1 模塊是程序

任何Python程序都可以作爲模塊導入。假設你寫了一個代碼清單10-1所示的程序,並且將它保存爲hello.py文件(名字很重要)。

代碼清單10-1  一個簡單的模塊

# hello.py
print "Hello, world!"

程序保存的位置也很重要。下一節中你會了解更多這方面的知識,現在假設將它保存在C:\python(Windows)或者~/python(UNIX/Mac OS X)目錄中,接着就可以執行下面的代碼,告訴解釋器在哪裏尋找模塊了(以Windows目錄爲例):

>>> import sys
>>> sys.path.append("c:/python")

注:在UNIX系統中,不能只是簡單地將字符串"~/python"添加到sys.path中,必須使用完整的路徑(例如/home/yourusername/python)。如果你希望將這個操作自動完成,可以使用sys.path.expanduser("~/python")。

我這裏所做的知識告訴解釋器:除了從默認的目錄中尋找之外,還需要從目錄c:\python中尋找模塊。完成這個步驟之後,就能導入自己的模塊了(存儲在c:\python\hello.py文件中):

>>> import hello
Hello, world!

注:在導入模塊的時候,你可能會看到有新文件出現——在本例中是c:\python\hello.pyc。這個以.pyc爲擴展名的文件是(平臺無關的)經過處理(編譯)的,已經轉換成Python能夠更加有效地處理的文件。如果稍後導入同一個模塊,Python會導入.pyc文件而不是.py文件,除非.py文件已改變,在這種情況下,會生成新的.pyc文件。刪除.pyc文件不會損害程序(只要等效的.py文件存在即可)——必要的時候系統還會創建新的.pyc文件。

如你所見,在導入模塊的時候,其中的代碼被執行了。不過,如果再次導入該模塊,就什麼都不會發生了:

>>> import hello
>>>

爲什麼這次沒用了?因爲導入模塊並不意味着在導入時執行某些操作(比如打印文本)。它們主要用於定義,比如變量、函數和類等。此外,因爲只需要定義這些東西一次,導入模塊多次和導入一次的效果是一樣的。

爲什麼只是一次

 這種“只導入一次”(import-only-once)的行爲在大多數情況下是一種實質性優化,對於一下情況尤其重要:兩個模塊互相導入。

在大多數情況下,你可能會編寫兩個互相訪問函數和類的模塊以便實現正確的功能。舉例來說,假設創建了兩個模塊——clientdb和billing——分別包含了用於客戶端數據庫和計費系統的代碼。客戶端數據庫可能需要調用計費系統的功能(比如每月自動將賬單發送給客戶),而計費系統可能也需要訪問客戶端數據庫的功能,以保證計費準確。

如果每個模塊都可以導入數次,那麼就出問題了。模塊clientdb會導入billing,而billing又導入clientdb,然後clientdb又······你應該能想象到這種情況。這個時候導入就成了無限循環。(無限遞歸,記得嗎?)但是,因爲在第二次導入模塊的時候什麼都不會發生,所以循環會終止。

如果堅持重新載入模塊,那麼可以使用內建的reload函數。它帶有一個參數(需要重新載入的模塊),並且返回重新載入的模塊。如果你在程序運行的時候更改了模塊並且希望將這些更改反應出來,那麼這個功能會比較有用。要重新載入hello模塊(只包含一個print語句),可以像下面這樣做:

>>> hello = reload(hello)
Hello, world!

這裏假設hello已經被導入過(一次)。那麼,通過將reload函數的返回值賦給hello,我們使用重新載入的版本替換了原先的版本。如你所見,問候語已經打印出來了,在此我完成了模塊的導入。

如果你已經通過實例化bar模塊中的Foo類創建了一個對象x,然後重新載入bar模塊,那麼不管通過什麼方式都無法重新創建引用bar的對象x,x仍然是舊版本Foo類的實例(源自舊版本的bar)。如果需要x基於重新載入的模塊bar中的新Foo類進行創建,那麼你就得重新創建它了。

注意,Python3.0已經去掉了reload函數。儘管使用exec能夠實現同樣的功能,但是應該儘可能避免重新載入模塊。

 

10.1.2 模塊用於定義

綜上所述,模塊在第一次導入到程序中時被執行。這看起來有點用——但並不算很有用。真正的用處在於它們(像類一樣)可以保持自己的作用域。這就意味着定義的所有類和函數以及賦值後的變量都成爲了模塊的特性。這看起來挺複雜的,用起來卻很簡單。

1.在模塊中定義函數

假設我們編寫了一個類似代碼清單10-2的模塊,並且將它存儲爲hello2.py文件。同時,假設我們將它放置到Python解釋器能夠找到的地方——可以使用前一節中的sys.path方法,也可以用10.1.3節中的常規方法。

注:如果希望模塊能夠像程序一樣被執行(這裏的程序是用於執行的,而不是真正作爲模塊使用的),可以對Python解釋器使用-m切換開關來執行程序。如果progname.py(注意後綴)文件和其他模塊都已被安裝(也就是導入了progname),那麼運行python -m progname args命令就會運行帶命令行參數args的progname程序。

代碼清單10-2  包含函數的簡單模塊

# hello2.py
def hello():
    print "Hello, world!"

可以像下面這樣導入:

>>> import hello2

模塊就會被執行,這意味着hello函數在模塊的作用域被定義了。因此可以通過以下方式來訪問函數:

>>> hello2.hello()
Hello, world!

我們可以通過同樣的方法來使用如何在模塊的全局作用域中定義的名稱。

我們爲什麼要這樣做呢?爲什麼不在主程序中定義好一切呢?主要原因是代碼重用(code reuse)。如果把代碼放在模塊中,就可以在多個程序中使用這些代碼了。這意味着如果編寫了一個非常棒的客戶端數據庫,並且將它放在叫做clientdb的模塊中,那麼你就可以在計費的時候、發送垃圾郵件的時候(當然我可不希望你這麼做)以及任何需要訪問客戶數據的程序中使用這個模塊了。如果沒有將這段代碼放在單獨的模塊中,那麼就需要在每個程序中重寫這些代碼了。因此請記住:爲了讓代碼可重用,請將它模塊化!(是的,這當然也關乎抽象)

2.在模塊中增加測試代碼

模塊被用來定義函數、類和其他一些內容,但是有些時候(事實上是經常),在模塊中添加一些檢查模塊本身是否能正常工作的測試代碼是很有用的。舉例來說,假如想要確保hello函數正常工作,你可能會將hello2模塊重寫爲新的模塊——代碼清單10-3中定義的hello3。

# hello3.py
def hello():
    print "Hello, world!"

# A test
hello()

這看起來是合理的,如果將它作爲普通程序運行,會發現它能夠正常工作。但如果將它作爲模塊導入,然後在其他程序中使用hello函數,測試代碼就會被執行,就像本章實驗開頭的第一個hello模塊一樣:

>>> import hello3
Hello, world!
>>> hello3.hello.()
Hello, world!

這個可不是你想要的。避免這種情況關鍵在於:“告知”模塊本身是作爲程序運行還是導入到其他程序。爲了實現這一點,需要使用__name__變量:

>>> __name__
'__main__'
>>> hello3.__name__
'hello3'

如你所見,在“主程序”(包括解釋器的交互式提示符在內)中,變量__name__的值是'__main__'。而在導入的模塊中,這個值就被設定爲模塊的名字。因此,爲了讓模塊的測試代碼更加好用,可以將其放置在if語句中,如代碼清單10-4所示。

複製代碼
代碼清單10-4  使用條件測試代碼的模塊
# hello4.py

def hello():
    print "Hello, world!"

def test():
    hello()

if __name__ == '__main__':
    test()
複製代碼

如果將它作爲程序運行,hello函數會被執行。而作爲模塊導入時,它的行爲就會像普通模塊一樣:

>>> import hello4
>>> hello4.hello()
Hello, world!

如你所見,我將測試代碼放在了test函數中,也可以直接將它們放入if語句。但是,將測試代碼放入獨立的test函數會更靈活,這樣做即使在把模塊導入其他程序之後,仍然可以對其進行測試:

>>> hello4.test()
Hello, world!

注:如果需要編寫更完整的測試代碼,將其放置在單獨的程序中會更好。關於編寫測試代碼的更多內容,參見第16章。

 

10.1.3 讓你的模塊可用

前面的例子中,我改變了sys.path,其中包含了(字符串組成的)一個目錄列表,解釋器在該列表中查找模塊。然而一般來說,你可能不想這麼做。在理想情況下,一開始sys.path本身就應該包含正確的目錄(包括模塊的目錄)。有兩種方法可以做到這一點:一是將模塊放置在合適的位置,另外則是告訴解釋器去哪裏查找需要的模塊。下面幾節將探討這兩種方法。

1.將模塊放置在正確位置

將模塊放置在正確位置(或者說某個正確位置,因爲會有多種可能性)是很容易的。只需要找出Python解釋器從哪裏查找模塊,然後將自己的文件放置在那裏即可。

注:如果機器上面的Python解釋器是由管理員安裝的,而你又沒有管理員權限,可能無法將模塊存儲在Python使用的目錄中。這種情況下,你需要使用另外一個解決方案:告訴解釋器去那裏查找。

你可能記得,那些(成爲搜索路徑的)目錄的列表可以在sys模塊中的path變量中找到:

複製代碼
>>> import sys, pprint
>>> pprint.pprint(sys.path)
['',
 '/usr/lib/python2.7',
 '/usr/lib/python2.7/plat-x86_64-linux-gnu',
 '/usr/lib/python2.7/lib-tk',
 '/usr/lib/python2.7/lib-old',
 '/usr/lib/python2.7/lib-dynload',
 '/usr/local/lib/python2.7/dist-packages',
 '/usr/lib/python2.7/dist-packages',
 '/usr/lib/python2.7/dist-packages/PILcompat',
 '/usr/lib/python2.7/dist-packages/gtk-2.0',
 '/usr/lib/python2.7/dist-packages/ubuntu-sso-client']
複製代碼

注:如果你的數據結構過大,不能在一行打印完,可以使用pprint模塊中的pprint函數替代普通的print語句。pprint是個相當好的打印函數,能夠提供更加智能的打印輸出。

這是安裝在elementary OS上的Python2.7的標準路徑,不同的系統會有不同的結果。關鍵在於每個字符串都提供了一個放置模塊的目錄,解釋器可以從這些目錄中找到所需的模塊。儘管這些目錄都可以使用,但site-packages目錄是最佳的選擇,因爲它就是用來做這些事情的。查看你自己的sys.path,找到site-packages目錄,將代碼清單10-4的模塊存儲在其中,要記得改名,比如改成another_hello.py,然後測試:

>>> import another_hello
>>> another_hello.hello()
Hello, world!

只要將模塊放入類似site-packages這樣的目錄中,所有程序就都能將其導入了。

2.告訴編譯器去那裏找

“將模塊放置在正確的位置”這個解決方案對於以下幾種情況可能並不適用:

 不希望將自己的模塊填滿Python解釋器的目錄;

 沒有在Python解釋器目錄中存儲文件的權限;

 想將模塊放在其他地方。

最後一點是“想將模塊放在其他地方”,那麼就要告訴解釋器去哪裏找。你之前已經看到了一種方法,就是編輯sys.path,但這不是通用的方法。標準的實現方法是在PYTHONPATH環境變量中包含模塊所在的目錄。

PYTHONPATH環境變量的內容會因爲使用的操作系統不同而有所差異(參見下面的“環境變量”),但基本上來說,它與sys.path很類似——一個目錄列表。

環境變量

環境變量並不是Python解釋器的一部分——它們是操作系統的一部分。基本上,它相當於Python變量,不過是在Python解釋器外設置的。有關設置的方法,你應該參考操作系統文檔,這裏只給出一些相關提示。

在UNIX和Mac OS X中,你可以在一些每次登陸都要執行的shell文件內設置環境變量。如果你使用類似bash的shell文件,那麼要設置的就是.bashrc,你可以在主目錄中找到它。將下面的命令添加到這個文件中,從而將~/python加入到PYTHONPATH:

export PYTHON=$PYTHONPATH:~/python

注意,多個路徑以冒號分隔。其他的shell可能會有不同的語法,所以你應該參考相關的文檔。

對於Windows系統,你可以使用控制面板編輯變量(適用於高級版本的Windows,比如Windows XP、2000、NT和Vista,舊版本的,比如Windows 98就不適用了,而需要修改autoexec.bat文件,下段會講到)。依次點擊開始菜單→設置→控制面板。進入控制面板後,雙擊“系統”圖標。在打開的對話框中選擇“高級”選項卡,點擊“環境變量”按鈕。這時會彈出一個分爲上下兩欄的對話框:其中一欄是用戶變量,另外一欄就是系統變量,需要修改的是用戶變量。如果你看到其中已經有PYTHONPATH項,那麼選中它,單擊“編輯”按鈕進行編輯。如果沒有,單擊“新建”按鈕,然後使用PYTHONPATH作爲“變量名”,輸入目錄作爲“變量值”。注意,多個目錄以分號分分隔。

如果上面的方法不行,你可以編輯autoexec.bat文件,該文件可以在C盤的根目錄下找到(假設是以標準模式安裝的Windows)。用記事本(或者IDLE編輯器)打開它,增加一行設置PYTHONPATH的內容。如果想要增加目錄C:\pyrhon。可以像下面這樣做:

set PYTHONPATH=%PYTHONPATH%;C:\python

注意,你所使用的IDE可能會有自身的機制,用於設置環境變量和Python路徑。

注:你不需要使用PYTHONPATH來更改sys.path。路徑配置文件提供了一個有用的捷徑,可以讓Python替你完成這些工作。路徑配置文件是以.pth爲擴展名的文件,包括應該添加到sys.path中的目錄信息。空行和以#開頭的行都會被忽略。以import開頭的文件會被執行。爲了執行路徑配置文件,需要將其放置在可以找到的地方。對於Windows來說,使用sys.prefix定義的目錄名(可能類似於C:\Python22);在UNIX和Mac OS X中則使用site-packages目錄(更多信息可以參見Python庫參考中site模塊的內容,這個模板在Python解釋器初始化時會自動導入)。

3.命名模塊

你可能注意到了,包含模塊代碼的文件的名字要和模塊名一樣,再加上.py擴展名。在Windows系統中,你也可以使用.pyw擴展名。有關文件擴展名含義的更多信息請參見第12章。

 

10.1.4 包

爲了組織好模塊,你可以將它們分組爲(package)。包基本上就是另外一個類模塊,有趣的地方就是它們能包含其他模塊。當模塊存儲在文件中時(擴展名.py),包就是模塊所在的目錄。爲了讓Python將其作爲包對待,它必須包含一個命名爲__init__.py的文件(模塊)。如果將它作爲普通模塊導入的話,文件的內容就是包的內容。比如有個名爲constants的包,文件constants/__init__.py包括語句PI=3.14,那麼你可以像下面這麼做:

import constants
print constants.PI

爲了將模塊放置在包內,直接把模塊放在包目錄內即可。

比如,如果要建立一個叫做drawing的包,其中包括名爲shapes和colors的模塊,你就需要創建表10-1中所示的文件和目錄(UNIX路徑名)。

表10-1 簡單的包佈局

~/python/                      PYTHONPATH中的目錄

~/python/drawing/                   包目錄(drawing包)

~/python/drawing/__init__.py            包代碼(drawing模塊)

~/python/drawing/colors.py             colors模塊

~/python/drawing/shapes.py              shapes模塊

對於表10-1中的內容,假定你已經將目錄~/python放置在PYTHONPATH。在Windows系統中,只要用C:\python替換~/python,並且將正斜線爲反斜線即可。

依照這個設置,下面的語句都是合法的:

import drawing                    # (1) Imports the drawing package
import drawing.colors             # (2) Imports the colors module
from drawing import shapes        # (3) Imports the shapes module

在第1條語句drawing中__init__模塊的內容是可用的,但drawing和colors模塊則不可用。在執行第2條語句之後,colors模塊可用了,可以通過短名(也就是僅使用shapes)來使用。注意,這些語句只是例子,執行順序並不是必需的。例如,不用像我一樣,在導入包的模塊前導入包本身,第2條語句可以獨立使用,第3條語句也一樣。我們還可以在包之間進行嵌套。

 

包是一個分層次的文件目錄結構,它定義了一個由模塊及子包,和子包下的子包等組成的Python的應用環境。

考慮一個在Phone目錄下的pots.py文件。這個文件有如下源代碼:

#!/usr/bin/python
# -*- coding: UTF-8 -*-
 
def Pots():
   print "I'm Pots Phone"
   

同樣地,我們有另外兩個保存了不同函數的文件:

  • Phone/Isdn.py 含有函數Isdn()
  • Phone/G3.py 含有函數G3()

現在,在Phone目錄下創建file __init__.py:

  • Phone/__init__.py

當你導入Phone時,爲了能夠使用所有函數,你需要在__init__.py裏使用顯式的導入語句,如下:

from Pots import Pots
from Isdn import Isdn
from G3 import G3

當你把這些代碼添加到__init__.py之後,導入Phone包的時候這些類就全都是可用的了。

#!/usr/bin/python
# -*- coding: UTF-8 -*-
 
# 導入 Phone 包
import Phone
 
Phone.Pots()
Phone.Isdn()
Phone.G3()

以上實例輸出結果:

I'm Pots Phone
I'm 3G Phone
I'm ISDN Phone

如上,爲了舉例,我們只在每個文件裏放置了一個函數,但其實你可以放置許多函數。你也可以在這些文件裏定義Python的類,然後爲這些類建一個包。

10.2 探究模塊

在講述標準庫模塊前,先教你如何獨立地探究模塊。這種技能極有價值,因爲作爲Python程序員,在職業生涯中可能會遇到很多有用的模塊,我又不能在這裏一一介紹。目前的標準庫已經大到可以出本書了(事實上已經有這類書了),而且它還在增長。每次新的模塊發佈後,都會添加到標準庫,一些模塊經常發生一些細微的變化和改進。同時,你還能在網上找到些有用的模塊並且可以很快理解(grok)它們,從而讓編程輕而易舉地稱爲一種享受。

 

10.2.1 模塊中有什麼

探究模塊最直接的方式就是在Python解釋器中研究它們。當然,要做的第一件事就是導入它。假設你聽說有個叫做copy的標準模塊:

>>> import copy

沒有引發異常,所以它是存在的。但是它能做什麼?它又有什麼?

1.使用dir

查看模塊包含的內容可以使用dir函數,它會將對象的所有特性(以及模塊的所有函數、類、變量等)列出。如果想要打印出dir(copy)的內容,你會看到一長串的名字(試試看)。一些名字以下劃線開始,暗示(約定俗成)它們並不是爲在模塊外部使用而準備的。所以讓我們用列表推導式(如果不記得如何使用了,請參見5.6節)過濾掉它們:

>>> [n for n in dir(copy) if not n.startswith("_")]
['Error', 'PyStringMap', 'copy', 'deepcopy', 'dispatch_table', 'error', 'name', 't', 'weakref']

這個列表推導式是個包含dir(copy)中所有不以下劃線開頭的名字的列表。它比完整列表要清楚些。(如果喜歡用tab實現,那麼應該看看庫參考中的readline和rlcompleter模塊。它們在探究模塊時很有用)

2.__all__變量

在上一節中,通過列表推導式所做的事情是推測我可能會在copy模塊章看到什麼。但是我們可以直接從列表本身獲得正確答案。在完整的dir(copy)列表中,你可能注意到了__all__這個名字。這個變量包含一個列表,該列表與我之前通過列表推導式創建的列表很類似——除了這個列表在模塊本身中已被默認設置。我們來看看它都包含哪些內容:

>>> copy.__all__
['Error', 'copy', 'deepcopy']

我的猜測還不算太離譜吧。列表推導式得到的列表只是多出了幾個我用不到的名字。但是__all__列表從哪來,它爲什麼會在那兒?第一個問題很容易回答。它是在copy模塊內部被設置的,像下面這樣(從copy.py直接複製而來的代碼):

__all__ = ["Error", "copy", "deepcopy"]

那麼它爲什麼在那呢?它定義了模塊的公有接口(public interface)。更準確地說,它告訴解釋器:從模塊導入所有名字代表什麼含義。因此,如果你使用如下代碼:

from copy import *

那麼,你就能使用__all__變量中的4個函數。要導入PyStringMap的話,你就得顯示地實現,或者導入copy然後使用copy.PyStringMap,或者使用from copy import PyStringMap。

在編寫模塊的時候,像設置__all__這樣的技術是相當有用的。因爲模塊中可能會有一大堆其他程序不需要或不想要的變量、函數和類,__all__會“客氣地”將它們過濾了出去。如果沒有設定__all__,用import *語句默認將會導入模塊中所有不以下劃線開頭的全局名稱。

 

10.2.2 用help獲取幫助

目前爲止,你已經通過自己的創造力和Python的多個函數和特殊特性的知識探究了copy模塊。對於這樣的探究工作,交互式解釋器是個非常強大的工具,而對該語言的精通程度決定了對模塊探究的深度。不過,還有個標準函數能夠爲你提供日常所需的信息,這個函數叫做help。讓我們先用copy函數試試:

複製代碼
>>> help(copy.copy)
Help on function copy in module copy:

copy(x)
    Shallow copy operation on arbitrary Python objects.

    See the module's __doc__ string for more info.
複製代碼

這些內容告訴你:copy帶有一個參數x,並且是“淺複製操作”。但是它還提到了模塊的__doc__字符串。這是什麼呢?你可能記得第六章提到的文檔字符串,它就是寫在函數開頭並且簡述函數功能的字符串,這個字符串可以通過函數的__doc__特性引用。就像從上面的幫助文本中所理解到的一樣,模塊也可以有文檔字符串(寫在模塊開頭),類也一樣(寫在類開頭)。

事實上,前面的幫助文本是從copy函數的文檔字符串中取出的。

>>> print copy.copy.__doc__
Shallow copy operation on arbitrary Python objects.

    See the module's __doc__ string for more info.

使用help與直接檢查文檔字符串相比,它的好處在於會獲得更多信息,比如函數簽名(也就是所帶的參數)。試着調用help(copy)(對模塊本身)看看得到什麼。它會打印出很多信息,包括copy和deepcopy之間區別的透徹的討論(從本質來說,deepcopy(x)會將存儲在x中的值作爲屬性進行復制,而copy(x)只是複製x,將x中的值綁定到副本的屬性上)。

 

10.2.3 文檔

模塊信息的自然來源當然是文件。我把對文檔的討論推後在這裏,是因爲自己先檢查模塊總是快一些。舉例來說,你可能會問“range的參數是什麼”。不用在Python數據或者標準Python文檔中尋找有關range的描述,而是可以直接查看:

複製代碼
>>> print range.__doc__
range(stop) -> list of integers
range(start, stop[, step]) -> list of integers

Return a list containing an arithmetic progression of integers.
range(i, j) returns [i, i+1, i+2, ..., j-1]; start (!) defaults to 0.
When step is given, it specifies the increment (or decrement).
For example, range(4) returns [0, 1, 2, 3].  The end point is omitted!
These are exactly the valid indices for a list of 4 elements.
複製代碼

這樣就獲得了關於range函數的精確描述,因爲Python解釋器可能已經運行了(在編程的時候,經常會像這樣懷疑函數的功能),訪問這些信息花不了幾秒鐘。

但是,並非每個模塊和函數都有不錯的文檔字符串(儘管都應該有),有些時候可能需要十分透徹地描述這些模塊和函數是如何工作的。大多數從網上下載的模塊都有相關的文檔。在我看來,學習Python編程最有用的文檔莫過於Python庫參考,它對所有標準庫中的模塊都有描述。如果想要查看Python的知識。十有八九我都會去查閱它。庫參考可以在線瀏覽(http://python.org/doc/lib),並且提供下載,其他一些標準文檔(比如Python指南或者Python語言參考)也是如此。所有這些文檔都可以在Python網站上(http://python.org/doc)找到。

 

10.2.4 使用源代碼

到目前爲止,所討論的探究技術在大多數情況下都已經夠用了。但是,對於希望真正理解Python語言的人來說,要了解模塊,是不能脫離源代碼的。閱讀源代碼,事實上是學習Python最好的方式,除了自己編寫代碼外。

真正的閱讀不是問題,但是問題在於源代碼在哪裏。假設我們希望閱讀標準模塊copy的源代碼,去哪裏找呢?一種方案是檢查sys.path,然後自己找,就像解釋器做的一樣。另外一種快捷的方法是檢查模塊的__file__屬性:

>>> print copy.__file__
C:\Python27\lib\copy.pyc

注:如果文件名以.pyc結尾,只要查看對應的以/py結尾的文件即可。

就在那!你可以使用代碼編輯器打開copy.py(比如IDLE),然後查看它是如何工作的。

注:在文本編輯器中打開標準庫文件的時候,你也承擔着意外修改它的風險。這樣做可能會破壞它,所以在關閉文件的時候,你必須確保沒有保存任何可能做出的修改。

注意,一些模塊並不包含任何可以閱讀的Python源代碼。它們可能已經融入到解釋器內了(比如sys模塊),或者可能是使用C程序語言寫成的(如果模塊是使用C語言編寫的,你也可以查看它的C源代碼)。(請查看第17章以獲得更多使用C語言擴展Python的信息)

 

10.3 標準庫:一些最愛

有的讀者會覺得本章的標題不知所云。“充電時刻”(batteries included)這個短語最開始由Frank Stajano創造,用於描述Python豐富的標準庫。安裝Python後,你就“免費”獲得了很多有用的模塊(充電電池)。因爲獲得這些模塊的更多信息的方式很多(在本章的第一部分已經解釋過了),我不會在這裏列出完整的參考資料(因爲要佔去很大篇幅),但是我會對一些我最喜歡的標準模塊進行說明,從而激發你對模塊進行探究的興趣。你會在“項目章節”(第20章~第29章)碰到更多的標準模塊。模塊的描述並不完全,但是會強調每個模塊比較有趣的特徵。

 

10.3.1 sys

sys這個模塊讓你能夠訪問與Python解釋器聯繫緊密的變量和函數,其中一些在表10-2中列出。

表10-2 sys模塊中一些重要的函數和變量

argv                命令行參數,包括腳本名稱

exit([arg])             退出當前的程序,可選參數爲給定的返回值或者錯誤信息

modules              映射模塊名字到載入模塊的字典

path                查找模塊所在目錄的目錄名列表

platform              類似sunos5或者win32的平臺標識符

stdin                 標準輸入流——一個類文件(file-like)對象

stdout               標準輸出流——一個類文件對象

stderr                  標準錯誤流——一個類文件對象

變量sys.argv包含傳遞到Python解釋器的參數,包括腳本名稱。

函數sys.exit可以退出當前程序(如果在try/finally塊中調用,finally子句的內容仍然會被執行,第八章對此進行了探討)。你可以提供一個整數作爲參數,用來標識程序是否成功運行,這是UNIX的一個慣例。大多數情況下使用該整數的默認值就可以了(也就是0,表示成功)。或者你也可以提供字符串參數,用作錯誤信息,這對於用戶找出程序停止運行的原因會很有用。這樣,程序就會在退出的時候提供錯誤信息和標識程序運行失敗的代碼。

映射sys.modules將模塊名映射到實際存在的模塊上,它只應用於目前導入的模塊。

sys.path模塊變量在本章前面討論過,它是一個字符串列表,其中的每個字符串都是一個目錄名,在import語句執行時,解釋器就會從這些目錄中查找模塊。

sys.platform模塊變量(它是個字符串)是解釋器正在其上運行的“平臺”名稱。它可能是標識操作系統的名字(比如sunos5或win32),也可能標識其他種類的平臺,如果運行Jython的話,就是Java的虛擬機(比如java1.4.0)。

sys.stdin、sys.stdout和sys.stderr模塊變量是類文件流對象。它們表示標準UNIX概念中的標準輸入、標準輸出和標準錯誤。簡單來說,Python利用sys.stdin獲得輸入(比如用於函數input和raw_input中的輸入),利用sys.stdout輸出。第十一章會介紹更多有關於文件(以及這三個流)的知識。

舉例來說,我們思考一下反序打印參數的問題。當你通過命令行調用Python腳本時,可能會在後面加上一些參數——這就是命令行參數(command-line argument)。這些參數會放置在sys.argv列表中,腳本的名字爲sys.argv[0]。反序打印這些參數很簡單,如代碼清單10-5所示。

複製代碼
# 代碼清單10-5 反序打印命令行參數

import sys

args = sys.argv[1:]
args.reverse()
print " ".join(args)
複製代碼

正如你看到的,我對sys.argv進行了複製。你可以修改原始的列表,但是這樣做通常是不安全的,因爲程序的其他部分可能也需要包含原始參數的sys.argv。注意,我跳過了sys.argv的第一個元素,這是腳本的名字。我使用args.reverse()方法對列表進行反向排序,但是不能打印出這個操作結果的,這是個返回None的原地修改操作。下面是另外一種做法:

print " ".join(reversed(sys.argv[1:]))

最後,爲了保證輸出得更好,我使用了字符串方法join。讓我們試試看結果如何(我使用的是MS-DOS,在UNIX Shell下它也會工作的同樣好):

D:\Workspace\Basic tutorial>python Code10-5.py this is a test
test a is this

 

10.3.2 os

os模塊提供了訪問多個操作系統服務的功能。os模塊包括的內容很多,表10-3中只是其中一些最有用的函數和變量。另外,os和它的子模塊os.path還包括一些用於檢查、構造、刪除目錄和文件的函數,以及一些處理路徑的函數(例如,os.path.split和os.path.join讓你在大部分情況下都可以忽略os.pathsep)。關於它的更多信息,請參見標準庫文檔。

表10-3 os模塊中一些重要函數和變量

environ                    對環境變量進行映射

system(command)               在子shell中執行操作系統命令

sep                      路徑中的分隔符

pathsep                     分隔路徑的分隔符

linesep                     行分隔符("\n", "\r", or "\r\n")

urandom(n)                  返回n字節的加密強隨機數據

os.environ映射包含本章前面講述過的環境變量。比如要訪問系統變量PYTHONPATH,可以使用表達式os.environ["PYTHONPATH"]。這個映射也可以用來更改系統環境變量,不過並非所有系統都支持。

os.system函數用於運行外部程序。也有一些函數可以執行外部程序。還有open,它可以創建與程序連接的類文件。

關於這些函數的更多信息,請參見標準庫文檔。

注:當前版本的Python中,包括subprocess模塊,它包括了os.system、execv和open函數的功能。

os.sep模塊變量是用於路徑名字中的分隔符。UNIX(以及Mac OS X中命令行版本的Python)中的標準分隔符是"/",Windows中的是"\\"(即Python針對單個反斜線的語法),而Mac OS中的是":"(有些平臺上,os.altsep包含可選的路徑分隔符,比如Windows中的"/")。

你可以在組織路徑的時候使用os.pathsep,就像在PYTHONPATH中一樣。pathsep用於分割路徑名:UNIX(以及Mac OS X中的命令行版本的Python)使用":",Windows使用";",Mac OS使用"::"。

模塊變量os.linesep用於文本文件的字符串分隔符。UNIX中(以及Mac OS X中命令行版本的Python)爲一個換行符(\n),Mac OS中爲單個回車符(\r),而在Windows中則是兩者的組合(\r\n)。

urandom函數使用一個依賴於系統的"真"(至少是足夠強度加密的)隨機數的源。如果正在使用的平臺不支持它,你會得到NotImplementedError異常。

例如,有關啓動網絡瀏覽器的問題。system這個命令可以用來執行外部程序,這在可以通過命令行執行程序(或命令)的環境中很有用。例如在UNIX系統中,你可以用它來列出某個目錄的內容以及發送Email,等等。同時,它對在圖形用戶界面中啓動程序也很有用,比如網絡瀏覽器。在UNIX中,你可以使用下面的代碼(假設/usr/bin/firefox路徑下有一個瀏覽器):

os.system("/usr/bin/firefox")

以下是Windows版本的調用代碼(也同樣假設使用瀏覽器的安裝路徑):

os.system(r"C:\'Program Files'\'Mozilla Firefox'\firefox.exe")

注意,我很仔細地將Program Files和Mozilla Firefox放入引號中,不然DOS(它負責處理這個命令)就會在空格處停不下來(對於在PYTHONPATH中設定的目錄來說,這點也同樣重要)。同時,注意必須使用反斜線,因爲DOS會被正斜線弄糊塗。如果運行程序,你會注意到瀏覽器會試圖打開叫做Files'\Mozilla...的網站——也就是在空格後面的命令部分。另一方面,如果試圖在IDLE中運行該代碼,你會看到DOS窗口出現了,但是沒有啓動瀏覽器並沒有出現,除非關閉DOS窗口。總之,使用以上代碼並不是完美的解決方法。

另外一個可以更好地解決問題的函數是Windows特有的函數——os.startfile:

os.startfile(r"C:\Program Files\Mozilla Firefox\firefox.exe")

可以看到,os.startfile接受一般路徑,就算包含空格也沒問題(也就是不用像在os.system例子中那樣將Program Files放在引號中)。

注意,在Windows中,由os.system(或者os.startfile)啓動了外部程序之後,Python程序仍然會繼續運行,而在UNIX中,程序則會中止,等待os.system命令完成。

更好的解決方案:WEBBROWSER

在大多數情況下,os.system函數很有用,但是對於啓動瀏覽器這樣特定的任務來說,還有更好的解決方案:webbrowser模塊。它包括open函數,它可以自動啓動Web瀏覽器訪問給定的URL。例如,如果希望程序使用Web瀏覽器打開Python的網站(啓動新瀏覽器或者使用已經運行的瀏覽器),那麼可以使用以下代碼:

import webbrowser
webbrowser.open("http://www.python.org")

 

10.3.3 fileinput

第十一章將會介紹很多讀寫文件的知識,現在先做個簡短的介紹。fileinput模塊讓你能夠輕鬆地遍歷文本文件的所有行。如果通過以下方式調用腳本(假設在UNIX命令行下):

$ python some_script.py file1.txt file2.txt file3.txt

這樣就可以以此對file1.txt到file3.txt文件中的所有行進行遍歷了。你還能對提供給標準輸入(sys.stdin,記得嗎)的文本進行遍歷。比如在UNIX的管道中,使用標準的UNIX命令cat:

$ cat file.txt | python some_script.py

如果使用fileinput模塊,在UNIX管道中使用cat來調用腳本的效果和將文件名作爲命令行參數提供給腳本是一樣的。fileinput模塊最重要的函數如表10-4所示。

fileinput.input是其中最重要的函數。它會返回能夠於for循環遍歷的對象。如果不想使用默認行爲(fileinput查找需要循環遍歷的文件),那麼可以給函數提供(序列形式的)一個或多個文件名。你還能將inplace參數設爲其真值(inplace=True)以進行原地處理。對於要訪問的每一行,需要打印出替代的內容,以返回到當前的輸入文件中。在進行原地處理的時候,可選的backup參數將文件名擴展備份到通過原始文件創建的備份文件中。

表10-4 fileinput模塊中重要的函數

input(files[, inplace[, backup]])                    便於遍歷多個輸入流中的行

filename()                                返回當前文件的名稱

lineno()                                 返回當前(累計)的行數

filelineno()                               返回當前文件的行數

isfirstline()                               檢查當前行是否是文件的第一行

isstdin()                                檢查最後一行是否來自sys.stdin

nextfile()                                 關閉當前文件,移動到下一個文件

close()                                 關閉序列

fileinput.filename函數返回當前正在處理的文件名(也就是包含了當前正在處理的文本行的文件)。

fileinput.lineno返回當前行的行數。這個數值是累計的,所以在完成一個文件的處理並且開始處理下一個文件的時候,行數並不會重置。而是將上一個文件的最後行數加1作爲計數的起始。

fileinput.filelineno函數返回當前處理文件的當前行數。每次處理完一個文件並且開始處理下一個文件時,行數都會重置爲1,然後重新開始計數。

fileinput.isfirstline函數在當前行是當前文件的第一行時返回真值,反之返回假值。

fileinput.isstdin函數在當前文件爲sys.stdin時返回真值,否則返回假值。

fileinput.nextfile函數會關閉當前文件,跳到下一個文件,跳過的行並不計。在你知道當前文件已經處理完的情況下,這個函數就比較有用了——比如每個文件都包含經過排序的單詞,而你需要查找某個詞。如果已經在排序中找到了這個詞的位置,那麼你就能放心地跳到下一個文件了。

fileinput.close函數關閉整個文件鏈,結束迭代。

爲了演示fileinput的使用,我們假設已經編寫了一個Python腳本,現在想要爲其代碼進行編號。爲了讓程序在完成代碼行編號之後仍然能夠正常運行,我們必須通過在每一行的右側加上作爲註釋的行號來完成編號工作。我們可以使用字符串格式化來將代碼行和註釋排成一行。假設每個程序行最多有45個字符,然後把行號註釋加在後面。代碼清單10-6展示了使用fileinput以及inplace參數來完成這項工作的簡單方法:

複製代碼
# 代碼清單10-6 爲Python腳本添加行號

#!/usr/bin/env python
# coding=utf-8

# numberlines.py

import fileinput

for line in fileinput.input(inplace=True):
    line = line.rstrip()
    num = fileinput.lineno()

    print "%-40s # %2i" % (line, num)
複製代碼

 

如果你像下面這樣在程序本身上運行這個程序:

$ python numberline.py numberline.py

程序會變成類似於代碼清單10-7那樣。注意,程序本身已經被更改了,如果這樣運行多次,最終會在每一行中添加多個行號。我們可以回憶一下之前的內容:rstrip是可以返回字符串副本的字符串方法,右側的空格都被刪除(請參見3.4節,以及附錄B中的表B-6)。

複製代碼
# 代碼清單10-7 爲已編號的行進行編號

#!/usr/bin/env python                         #  1
# coding=utf-8                                #  2
                                              #  3
# numberline.py                               #  4
                                              #  5
import fileinput                              #  6
                                              #  7
for line in fileinput.input(inplace=True):    #  8
    line = line.rstrip()                      #  9
    num = fileinput.lineno()                  # 10
                                              # 11
    print "%-45s # %2i" % (line, num)         # 12
複製代碼

注:要小心使用inplace參數,它很容易破壞文件。你應該在不使用inplace設置的情況下仔細測試自己的程序(這樣只會打印出錯誤),在確保程序工作正常後再修改文件。

另外一個使用fileinput的例子,請參見本章後面的random模塊部分。

 

10.3.4 集合、堆和雙端隊列

在程序設計中,我們會遇到很多有用的數據結構,而Python支持其中一些相對通用的類型,例如字典(或者說散列表)、列表(或者說動態數組)是語言必不可少的一部分。其他一些數據結構儘管不是那麼重要,但有些時候也能派上用場。

1.集合

集合(set)在Python2.3才引入。Set類位於sets模塊中。儘管可以在現在的代碼中創建Set實例。但是除非想要兼容以前的程序,否則沒有什麼必要這樣做。在Python2.3中,集合通過set類型的實例成爲了語言的一部分,這意味着不需要導入sets模塊——直接創建集合即可:

>>> set(range(10))
set([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])

集合是由序列(或者其他可迭代的對象)構建的。它們主要用於檢查成員資格,因此副本是被忽略的:

>>> set(["fee", "fie", "foe"])
set(['foe', 'fee', 'fie'])

除了檢查成員資格外,還可以使用標準的集合操作(可能你是通過數學瞭解到的),比如求並集和交集,可以使用方法,也可以使用對整數進行位操作時使用的操作(參見附錄B)。比如想要找出兩個集合的並集,可以使用其中一個集合的union方法或者使用按位與(OR)運算符"|":

>>> a = set([1, 2, 3])
>>> b = set([2, 3, 4])
>>> a.union(b)
set([1, 2, 3, 4])
>>> a | b
set([1, 2, 3, 4])

以下列出了一些其他方法和對應的運算符,方法的名稱已經清楚地表明瞭其用途:

複製代碼
>>> c = a & b
>>> c.issubset(a)
True
>>> c <= a
True
>>> c.issuperset(a)
False
>>> c >= a
False
>>> a.intersection(b)
set([2, 3])
>>> a & b
set([2, 3])
>>> a.difference(b)
set([1])
>>> a - b
set([1])
>>> a.symmetric_difference(b)
set([1, 4])
>>> a ^ b
set([1, 4])
>>> a.copy()
set([1, 2, 3])
>>> a.copy() is a
False
複製代碼

還有一些原地運算符和對應的方法,以及基本方法add和remove。關於這方面更多的信息,請參看Python庫參考的3.7節(http://python.org/doc/lib/types-set.html)。

注:如果需要一個函數,用於查找並且打印兩個集合的並集,可以使用來自set類型的union方法的未綁定版本。這種做法很有用,比如結合reduce來使用:

>>> mySets = []
>>> for i in range(10):
...     mySets.append(set(range(i, i + 5)))
... 
>>> reduce(set.union, mySets)
set([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13])

集合是可變的,所以不能用做字典中的鍵。另外一個問題就是集合本身只能包含不可變(可散列的)值,所以也就不能包含其他集合。在實際當中,集合的集合是很常用的,所以這個就是個問題了。幸好還有個frozenset類型,用於代表不可變(可散列)的集合:

複製代碼
>>> a = set()
>>> b = set()
>>> a.add(b)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: unhashable type: 'set'
>>> a.add(frozenset(b))
>>> a
set([frozenset([])])
複製代碼

frozenset構造函數創建給定集合的副本,不管是將集合作爲其他集合成員還是字典的鍵,frozenset都很有用。

2.堆

另外一個衆所周知的數據結構是(heap),它是優先隊列的一種。使用優先隊列能夠以任意順序增加對象,並且能在任何時間(可能增加對象的同時)找到(也可能是移除)最小的元素,也就是說它比用於列表的min方法要有效率得多。

事實上,Python中並沒有獨立的堆類型,只有一個包含一些堆操作函數的模塊,這個模塊叫做heapq(q是queue的縮寫,即隊列),包括6個函數(參見表10-5),其中前4個直接和堆操作相關。你必須將列表作爲堆對象本身。

表10-5 heapq模塊中重要的函數

heappush(heap, x)                    將x入堆

heappop(heap)                      將堆中最小的元素彈出

heapify(heap)                        將heap屬性強制應用到任意一個列表

heapreplace(heap, x)                   將堆中最小的元素彈出,同時將x入堆

nlargest(n, iter)                       返回iter中第n大的元素

nsmallset(n, iter)                     返回iter中第n小的元素

heappush函數用於增加堆的項。注意,不能將它用於任何之前講述的列表中,它只能用於通過各種堆函數建立的列表中。原因是元素的順序很重要(儘管看起來是隨意排列,元素並不是進行嚴格排序的)。

複製代碼
>>> from heapq import *
>>> from random import shuffle
>>> data = range(10)
>>> shuffle(data)
>>> heap = []
>>> for n in data:
...     heappush(heap, n)
... 
>>> heap
[0, 2, 1, 6, 5, 3, 4, 9, 7, 8]
>>> heappush(heap, 0.5)
>>> heap
[0, 0.5, 1, 6, 2, 3, 4, 9, 7, 8, 5]
複製代碼

元素的順序並不像看起來那麼隨意。它們雖然不是嚴格排序的,但是也有規則的:位於i位置上的元素總比i//2位置處的元素大(反過來說就是i位置處的元素總比2*i以及2*i+1位置處的元素小)。這是底層堆算法的基礎,而這個特性稱爲堆屬性(heap property)。

heappop函數彈出最小的元素,一般來說都是在索引0處的元素,並且會確保剩餘元素中最小的那個佔據這個位置(保持剛纔提到的堆屬性)。一般來說,儘管彈出列表的第一個元素並不是很有效率,但是在這裏不是問題,因爲heappop在“幕後”會做一些精巧的移位操作:

複製代碼
>>> heappop(heap)
0
>>> heappop(heap)
0.5
>>> heappop(heap)
1
>>> heap
[2, 5, 3, 6, 7, 8, 4, 9]
複製代碼

heapify函數使用任意列表作爲參數,並且通過儘可能少的移位操作,將其轉換爲合法的堆(事實上是應用了剛纔提到的堆屬性)。如果沒有用heappush建立堆,那麼在使用heappush和heappop前應該使用這個函數。

>>> heap = [5, 8, 0, 3, 6, 7, 9, 1, 4, 2]
>>> heapify(heap)
>>> heap
[0, 1, 5, 3, 2, 7, 9, 8, 4, 6]

heapreplace函數不像其他函數那麼常用。它彈出堆的最小元素,並且將新元素推入。這樣做比調用heappop之後再調用heappush更高效。

複製代碼
>>> heapreplace(heap, 0.5)
0
>>> heap
[0.5, 1, 5, 3, 2, 7, 9, 8, 4, 6]
>>> heapreplace(heap, 10)
0.5
>>> heap
[1, 2, 5, 3, 6, 7, 9, 8, 4, 10]
複製代碼

heapq模塊中剩下的兩個函數nlargest(n, iter)和nsmallest(n, iter)分別用來尋找任何可迭代對象iter中第n大或第n小的元素。你可以使用排序(比如使用sorted函數)和分片來完成這個工作,但是堆算法更快而且更有效第使用內存(還有一個沒有提及的有點:更易用)。

3.雙端隊列

雙端隊列(double-ended queue,或稱deque)在需要按照元素增加的順序來移除元素時非常有用,Python2.4增加了collection模塊,它包括deque類型。

注:Python2.5中的collections模塊只包括deque類型和defaultdict類型,爲不存在的鍵提供默認值的字典,未來可能會加入二叉樹(B-Tree)和斐波那契堆(Fibonacci heap)。

雙端隊列通過可迭代對象(比如集合)創建,而且有些非常有用的方法,如下例所示:

複製代碼
>>> from collections import deque
>>> q = deque(range(5))
>>> q.append(5)
>>> q.appendleft(6)
>>> q
deque([6, 0, 1, 2, 3, 4, 5])
>>> q.pop()
5
>>> q.popleft()
6
>>> q.rotate(3)
>>> q
deque([2, 3, 4, 0, 1])
>>> q.rotate(-1)
>>> q
deque([3, 4, 0, 1, 2])
複製代碼

雙端隊列好用的原因是它能夠有效的在開頭(左側)增加和彈出元素,這是在列表中無法實現的。除此之外,使用雙端隊列的好處還有:能夠有效地旋轉(rotate)元素(也就是將它們左移或者右移,使頭尾相連)。雙端隊列對象還有extend和extendleft方法,extend和列表的extend方法差不多,extendleft則類似於appendleft。注意,extendleft使用的可迭代對象中的元素會反序出現在雙端隊列中。

 

10.3.5 time

time模塊所包括的函數能夠實現以下功能:獲得當前時間、操作時間和日期、從字符串讀取時間以及格式化時間爲字符串。日期可以用實數(從“新紀元”的1月1日0點開始計算到現在的秒數,新紀元是一個與平臺相關的年份,對UNIX來說是1970年),或者是包含有9個整數的元組。這些整數的意義如表10-6所示,比如,元組:

(2008, 1, 21, 12, 2, 56, 0, 21, 0)

表示2008年1月21日12時2分56秒,星期一,並且是當年的第21天(無夏令時)。

表10-6 Python日期元組的字段含義

0          年          比如2000,2001等等

1          月          範圍1~12

2          日          範圍1~31

3          時          範圍0~23

4          分          範圍0~59

5          秒          範圍0~61

6          周          當週一爲0時,範圍0~6

7          儒歷日        範圍1~366

8          夏令時        0、1或-1

秒的範圍是0~61是爲了應付閏秒和雙閏秒。夏令時的數字是布爾值(真或假),但是如果使用了-1,mktime(該函數將這樣的元組轉換爲時間戳,它包含從新紀元開始以來的秒數)就會工作正常。time模塊中最重要的函數如表10-7所示。

函數time.asctime將當前時間格式化爲字符串,如下例所示:

>>> time.asctime()
'Fri May 13 17:35:56 2016'

表10-7 time模塊中重要的函數

asctime([tuple])                    將時間元組轉換爲字符串

localtime([secs])                     將秒數轉換爲日期元組,以本地時間爲準

mktime(tuple)                     將時間元組轉換爲本地時間

sleep(secs)                       休眠(不做任何事情)secs秒

strptime(string[, format])                 將字符串解析爲時間元組

time()                         當前時間(新紀元開始後的描述,以UTC爲準)

如果不需要使用當前時間,還可以提供一個日期元組(比如通過localtime創建的)。(爲了實現更精細的格式化,你可以使用strftime函數,標準文檔對此有相應的介紹)

函數time.localtime將實數(從新紀元開始計算的秒數)轉換爲本地時間的日期元組。如果想獲得全球統一時間(有關全球統一時間的更多內容,請參見http://en/wikipedia.org/wiki/Universal_time),則可以使用gtime。

函數time.mktime將日期元組轉換爲從新紀元開始計算的秒數,它與localtime的功能相反。

函數time.sleep讓解釋器等待給定的秒數。

函數time.strptime將asctime格式化過的字符串轉換爲日期元組(可選的格式化參數所遵循的規則與strftime的一樣,詳情請參見標準文檔)。

函數time.time使用自新紀元開始計算的秒數返回當前(全球統一)時間,儘管每個平臺的新紀元可能不同,但是你仍然可以通過記錄某事件(比如函數調用)發生前後time的結果來對該事件計時,然後計算差值。有關這些函數的實例,請參見下一節的random模塊部分。

表10-7列出的函數只是從time模塊選出的一部分。該模塊的大多數函數所執行的操作與本小節介紹的內容相類似或者相關。如果需要這裏沒有介紹到的函數,請參見Python庫參考的14.2節(http://python.org/doc/lib/module-time.html),以獲得更多詳細信息。

此外,Python還提供了兩個和時間密切相關的模塊:datetime(支持日期和時間的算法)和timeit(幫助開發人員對代碼段的執行時間進行計時)。你可以從Python庫參考中找到更多有關它們的信息,第16章也會對timeit進行簡短的介紹。

 

10.3.6 random

random模塊包括返回隨機數的函數,可以用於模擬或者用於任何產生隨機輸出的程序。

注:事實上,所產生的數字都是僞隨機數,也就是說它們看起來是完全隨機的,但實際上,它們以一個可預測的系統作爲基礎。不過,由於這個系統模塊在僞裝隨機方面十分優秀,所以也就不必對此過多擔心了(除非爲了實現強加密的目標,因爲在這種情況下,這些數字就顯得不夠“強”了,無法抵抗某些特定的攻擊,但是如果你已經深入到強加密的話,也就不用我來解釋這些基礎的問題了)。如果需要真的隨機數,應該使用os模塊的urandom函數。random模塊內的SystemRandom類也是基於同種功能,可以讓數據接近真正的隨機性。

這個模塊中的一些重要函數如表10-8所示。

表10-8 random模塊中的一些重要的函數

random()                          返回0<n≤1之間的隨機實數n

getrandbits(n)                        以長整型形式返回n個隨機位

uniform(a, b)                          返回隨機實數n,其中a≤n<b

randrange([start, ]stop[, step])                 返回range(start, stop, step)中的隨機數

choice(seq)                          從序列seq中返回隨意元素

shuffle(seq[, random])                      原地指定序列seq

sample(seq, n)                        從序列seq中選擇n個隨機且獨立的元素

函數random.random是最基本的隨機函數之一,它只是返回0~1的僞隨機數n。除非這就是你想要的,否則你應該使用其他提供了額外功能的的函數。random.getrandbits以長整型形式返回給定的位數(二進制數)。如果處理的是真正的隨機事務(比如加密),這個函數尤爲有用。

爲函數random.uniform提供兩個數值參數a和b,它會返回在a~b的隨機(平均分佈的)實數n。所以,比如需要隨機數的角度值,可以使用uniform(0, 360)。

調用函數range可以獲得一個範圍,而使用與之相同的參數來調用標準函數random.randrange則能夠產生該範圍內的隨機整數。比如想要獲得1~10(包括10)的隨機數,可以使用randrange(1, 11)(或者使用randrange(10)+1),如果想要獲得小於20的隨機正奇數,可以使用randrange(1, 20, 2)。

函數random.choice從給定序列中(均一地)選擇隨機元素。

函數random.shuffle將給定(可變)序列的元素進行隨機移位,每種排列的可能性都是近似相等的。

函數random.sample從給定序列中(均一地)選擇給定數目的元素,同時確保元素互不相同。

注:從統計學的角度來說,還有些與uniform類似的函數,它們會根據其他各種不同的分佈規則進行抽取,從而返回隨機數。這些分佈包括貝塔分佈、指數分佈、高斯分佈等等。

下面介紹一些使用random模塊的例子。這些例子將使用一些前文介紹的time模塊中的函數。首先獲得代表時間間隔(2008年)限制的實數,這可以通過時間元組的方式來表示日期(使用-1表示一週中的某天,一年中的某天和夏令時,以便讓Python自己計算),並且對這些元組調用mktime:

>>> from random import *
>>> from time import *
>>> date1 = (2008, 1, 1, 0, 0, 0, -1, -1, -1)
>>> time1 = mktime(date1)
>>> date2 = (2009, 1, 1, 0, 0, 0, -1, -1, -1)
>>> time2 = mktime(date2)

然後就能在這個範圍內均一地生成隨機數(不包括上限):

>>> random_time = uniform(time1, time2)

然後,可以將數字轉換爲易讀的日期形式:

>>> print asctime(localtime(random_time))
Tue Oct 14 04:33:21 2008

在接下來的例子中,我們要求用戶選擇投擲的骰子數以及每個骰子具有的面數。投骰子機制可以由randrange和for循環實現:

複製代碼
#!/usr/bin/env python
# coding=utf-8


from random import randrange

num = input("How many dice? ")
sides = input("How many sides per die? ")
result = 0

for i in range(num):
    result += randrange(sides) + 1

print "The result is", result
複製代碼

如果將代碼存爲腳本文件並且執行,那麼會看到下面的交互操作:

How many dice? 3
How many sides per die? 6
The result is 11

接下來假設有一個新建的文本文件,它的每一行文本都代表一種運勢,那麼我們就可以使用前面介紹的fileinput模塊將“運勢”都存入列表中,再進行隨機選擇:

# fortunu.py

import fileinput, random

fortunes = list(fileinput.input())
print random.choice(fortunes)

在UNIX中,可以對標準字典文件/usr/dict/words進行測試,以獲得一個隨機單詞:

$ python Code.py /usr/dict/words 
Greyson

最後一個例子,假設你希望程序能夠在每次敲擊回車的時候都爲自己發一張牌,同時還要確保不會獲得相同的牌。首先要創建“一副牌”——字符串列表:

>>> values = range(1, 11) + "Jack Queen King".split()
>>> suits = "diamonds clubs hearts spades".split()
>>> deck = ["%s of %s" % (v, s) for v in values for s in suits]

現在創建的牌還不太適合進行遊戲,讓我們來看看現在的牌:

複製代碼
>>> from pprint import pprint
>>> pprint(deck[:12])
['1 of diamonds',
 '1 of clubs',
 '1 of hearts',
 '1 of spades',
 '2 of diamonds',
 '2 of clubs',
 '2 of hearts',
 '2 of spades',
 '3 of diamonds',
 '3 of clubs',
 '3 of hearts',
 '3 of spades']
複製代碼

太整齊了,對吧?不過,這個問題很容易解決:

複製代碼
>>> from random import shuffle
>>> shuffle(deck)
>>> pprint(deck[:12])
['7 of hearts',
 'Queen of hearts',
 'Jack of diamonds',
 '9 of hearts',
 '2 of diamonds',
 '7 of spades',
 '10 of diamonds',
 '8 of diamonds',
 'Jack of spades',
 '4 of spades',
 '2 of clubs',
 'King of spades']
複製代碼

注意,爲了節省空間,這裏只打印了前12張牌。你可以自己看看整副牌。

最後,爲了讓Python在每次按回車的時候都給你發一張牌,知道發完爲止,那麼只需要創建一個小的while循環即可。假設將建立牌的代碼放在程序文件中,那麼只需要在程序的結尾處加入下面這行代碼:

while deck:
    raw_input(deck.pop())

注:如果在交互式解釋器中嘗試上面找到的while循環,那麼你會注意到每次按下回車的時候都會打印出一個空字符串。因爲raw_input返回了輸入的內容(什麼都沒有),並且將其打印出來。在一般的程序中,從raw_input返回的值都會被忽略掉。爲了能夠在交互環節“忽略”它,只需要把raw_input的值賦給一些你不想再用到的變量即可。同時將這些變量命名爲ignore這類名字。

 

10.3.7 shelve

下一章將會介紹如何在文件中存儲數據,但如果只需要一個簡單的存儲方案,那麼shelve模塊可以滿足你大部分的需要,你所要做的只是爲它提供文件名。shelve中唯一的有趣的函數是open。在調用它的時候(使用文件名作爲參數),它會返回一個shelf對象,你10.3.7 shalve可以用它來存儲內容。只需要把它當做普通的字典(但是鍵一定要作爲字符串)來操作即可,在完成工作(並且將內容存儲到磁盤中)之後,調用它的close方法。

1.潛在的陷阱

shelve.open函數返回的對象並不是普通的映射,這一點尤其要注意,如下面的例子所示:

>>> import shelve
>>> s = shelve.open("/home/marlowes/workspace/pycharm_Python/Basic_tutorial/test.dat")
>>> s["x"] = ["a", "b", "c"]
>>> s["x"].append("d")
>>> s["x"]
['a', 'b', 'c']

"d"去哪了?

很容易解釋:當你在shelf對象中查找元素的時候,這個對象都會根據已經存儲的版本進行重新構建,當你將元素賦給某個鍵的時候,它就被存儲了。上述例子中執行的操作如下:

 列表["a", "b", "c"]存儲在鍵x下。

☑ 獲得存儲的表示,並且根據它來創建新的列表,而"d"被添加到這個副本中。修改的版本還沒有被保存!

☑ 最終,再次獲得原始版本——沒有"d"。

爲了正確地使用shelve模塊修改存儲的對象。必須將臨時變量綁定到獲得的副本上,並且在它被修改後重新存儲這個副本(感謝Luther Blissett指出這個問題):

>>> temp = s["x"]
>>> temp.append("d")
>>> s["x"] = temp
>>> s["x"]
['a', 'b', 'c', 'd']

 

Python2.4之後的版本還有個解決方法:將open函數的writeback參數設爲true。如果這樣做,所有從shelf讀取或者賦值到shelf的數據結構都會保存在內存(緩存)中,並且只有在關閉shelf的時候才寫回到磁盤中。如果處理的數據不大,並且不想考慮這些問題,那麼將writeback設爲true(確保在最後關閉了shelf)的方法還是不錯的。

2.簡單的數據庫示例

代碼清單10-8給出了一個簡單的使用shelve模塊的數據庫應用程序。

 Database.py

代碼清單10-8中的程序有一些很有意思的特徵。

☑ 將所有內容都放到函數中會讓程序更加結構化(可能的改進是將函數組織爲類的方法)。

 主程序放在main函數中,只有在if __name__ == '__main__'條件成立的時候才被調用。這意味着可以在其他程序中將這個程序作爲模塊導入,然後調用main函數。

 我在main函數中打開數據庫(shelf),然後將其作爲參數傳給另外需要它的函數。當然,我也可以使用全局變量,畢竟這個程序很小。不過,在大多數情況下最好避免使用全局變量,除非有充足的理由要使用它。

 在一些值中進行讀取之後,對讀取的內容調用strip和lower函數以生成了一個修改後的版本。這麼做的原因在於:如果提供的鍵與數據庫存儲的鍵相匹配,那麼它們應該完全一樣。如果總是對用戶的輸入使用strip和lower函數,那麼就可以讓用戶隨意輸入大小寫字母和添加空格了。同時需要注意的是:在打印字段名稱的時候,我使用了capitalize函數。

 我使用try/finally確保數據庫能夠正確關閉。我們永遠不知道什麼時候會出錯(同時程序會拋出異常)。如果程序在沒有正確關閉數據庫的情況下終止,那麼,數據庫文件就有可能被損壞了,這樣的數據文件是毫無用處的。使用try/finally就可以避免這種情況了。

接下來,我們測試一下這個數據庫。下面是一個簡單的交互過程:

複製代碼
Enter command(? for help): ?
The available commands are:
store   : Store information about a persoon
lookup  : Looks up a person from ID number
quit    : Save changes and exit
?       : Prints this message
Enter command(? for help): store
Enter unique ID number: 001
Enter name: Greyson
Enter age: 19
Enter phone number: 001-160309
Enter command(? for help): lookup
Enter ID number: 001
What would you like to know? (name, age, phone) phone
Phone: 001-160309
Enter command(? for help): quit
複製代碼

 

交互的過程並不是十分有趣,使用普通的字典也能獲得和shelf對象一樣的效果。但是,我們現在退出程序,然後再重新啓動它,看看發生了什麼?也許第二天才重新啓動它:

Enter command(? for help): lookup
Enter ID number: 001
What would you like to know? (name, age, phone) name
Name: Greyson
Enter command(? for help): quit

我們可以看到,程序讀出了第一次創建的文件,而Greyson的資料還在!

你可以隨意試驗這個程序,看看是否還能擴展它的功能並且提高用戶友好度。你是不是想創建一個供自己使用的版本?創建一個唱片集的數據庫怎樣?或者創建一個數據庫,幫助自己記錄借書朋友的名單(我想我會用這個版本)。

 

10.3.8 re

有些人面臨一個問題時回想:“我知道,可以使用正則表達式來解決這個問題。”於是現在他們就有兩個問題了。    ——Jamie Zawinski(Lisp黑客,Netscape早期開發者。關於他的更詳細編程生涯,可見人民郵電出版社出版的《編程人生》一書)

re模塊包含對正則表達式(regular expression)的支持。如果你之前聽說過正則表達式,那麼你可能知道它有多強大了,如果沒有,請做好心裏準備吧,它一定會令你很驚訝。

但是應該注意,在學習正則表達式之初會有點困難(好吧,其實是很難)。學習它們的關鍵是一次只學習一點——(在文檔中)查找滿足特定任務需要的那部分內容,預先將它們全部記住是沒必要的。本章將會對re模塊主要特徵和正則表達式進行介紹,以便讓你上手。

注:除了標準文檔外,Andrew Kuchling的"Regular Expression HOWTO"(正則表達式HOWTO)(http://amk.ca/python/howto/regex/)也是學習在Python中使用正則表達式的有用資源。

1.什麼是正則表達式

正則表達式是可以匹配文本片段的模式。最簡單的正則表達式就是普通字符串,可以匹配其自身。換句話說,正則表達式"python"可以匹配字符串"python"。你可以用這種匹配行爲搜索文本中的模式,並且用計算後的值替換特定模式,或者將文本進行分段。

 通配符

正則表達式可以可以匹配多於一個的字符串,你可以使用一些特殊字符串創建這類模式。比如點號(.)可以匹配任何字符(除了換行符),所以正則表達式".ython"可以匹配字符串"python"和"jython"。它還能匹配"qython"、"+ython"或者" ython"(第一個字母是空格),但是不會匹配"cpython"或者"ython"這樣的字符,因爲點號只能匹配一個字母,而不是兩個或者零個。

因爲它可以匹配“任何字符串”(除換行符外的任何單個字符),點號就稱爲通配符(wildcard)。

 對特殊字符進行轉義

你需要知道:在正則表達式中如果將特殊字符作爲普通字符使用會遇到問題,這很重要。比如,假設需要匹配字符串"python.org",直接調用"python.org"可以麼?這麼做是可以的,但是這樣也會匹配"pythonzorg",這可不是所期望的結果(點號可以匹配除換行符外的任何字符,還記得吧)。爲了讓特殊字符表現得像普通字符一樣,需要對它進行轉義(escape),就像我在第1章中對引號進行轉義所做的一樣——可以在它前面加上反斜線。因此,在本例中可以使用"python\\.org",這樣就只會匹配"python.org"了。

注:爲了獲得re模塊所需的單個反斜線,我們要在字符串中使用兩個反斜線——爲了通過解釋器進行轉義。這樣就需要兩個級別的轉義了:(1)通過解釋器轉義;(2)通過re模塊轉義(事實上,有些情況下可以使用單個反斜線,讓解釋器自動進行轉義,但是別依賴這種功能)。如果厭煩了使用雙斜線,那麼可以使用原始字符串,比如r"python\.org"。

 字符集

匹配任意字符可能很有用,但有些時候你需要更多的控制權。你可以使用中括號括住字符串來創建字符集(character set)。字符集可以匹配它所包括的任意字符,所以"[pj]ython"能夠匹配"python"和"jython",而非其他內容。你可以使用範圍,比如"[a-z]"能夠(按字母順序)匹配a到z的任意一個字符,還可以通過一個接一個的方式將範圍聯合起來使用,比如"[a-zA-Z0-9]"能夠匹配任意大小寫字母和數字(注意字符集只能匹配一個這樣的字符)。

爲了反轉字符集,可以在開頭使用^字符,比如"[^abc]"可以匹配任何除了a、b和c之外的字符。

字符集中的特殊字符

一般來說,如果希望點號、星號和問號等特殊字符在模式中用作文本字符而不是正則表達式運算符,那麼需要用反斜線進行轉義。在字符集中,對這些字符進行轉義通常是沒必要的(儘管是完全合法的)。不過,你應該記住下面的規則:

 如果脫字符(^)出現在字符集的開頭,那麼你需要對其進行轉義了,除非希望將它用做否定運算符(換句話說,不要將它放在開頭,除非你希望那樣用);

 同樣,右中括號(])和橫線(-)應該放在字符集的開頭或者用反斜線轉義(事實上,如果需要的話,橫線也能放在末尾)。

 選擇符和子模式

在字符串的每個字符都有各不相同的情況下,字符集是很好用的,但如果只想匹配字符串"python"和"perl"呢?你就不能使用字符集或者通配符來指定某個特定的模式了。取而代之的是用於選擇項的特殊字符:管道符號(|)。因此,所需的模式可以寫成"python|perl"。

但是,有些時候不需要對整個模式使用選擇運算符,只是模式的一部分。這時可以使用圓括號括起需要的部分,或稱子模式(subparttern)。前例可以寫成"p(ython|erl)"。(注意,術語子模式也是適用於單個字符)

 可選項和可重複子模式

在子模式後面加上問號,它就變成了可選項。它可能出現在匹配字符串中,但並非必需的。例如,下面這個(稍微有點難懂)模式:

r"(http://)?(www\.)?python\.org"

只能匹配下列字符串(而不會匹配其他的):

"http://www.python.org"
"http://python.org"
"www.python.org"
"python.org"

對於上述例子,下面這些內容是值得注意的:

 對點號進行了轉義,防止它被作爲通配符使用;

 使用原始字符串減少所需反斜線的數量;

 每個可選子模式都用圓括號括起;

 可選子模式出現與否均可,而且互相獨立。

問號表示子模式可以出現一次或根本不出現,下面這些運算符允許子模式重複多次:

 (pattern)*:允許模式重複0次或多次;

 (pattern)+:允許模式重複1次或多次;

 (patten){m,n}:允許模式重複m~n次。

例如,r"w*\.python\.org"會匹配"www.python.org",也會匹配".python.org"、"ww.python.org"和"wwwwww.python.org"。類似地,r"w+\.python\.org"匹配"w.python.org"但不匹配".python.org",而r"w{3,4}\.python\.org"只匹配"www.python.org"和"wwww.python.org"。

注:這裏使用術語匹配(match)表示模式匹配整個字符串。而接下來要說到的match函數(參見表10-9)只要求模式匹配字符串的開始。

 字符串的開始和結尾

目前爲止,所出現的模式匹配都是針對整個字符串的,但是也能尋找匹配模式的子字符串,比如字符串"www.python.org"中的子字符串"www"能夠匹配模式"w+"。在尋找這樣的子字符串時,確定子字符串位於整個字符串的開始還是結尾是很有用的。比如,只想在字符串的開頭而不是其他位置匹配"ht+p",那麼就可以使用脫字符(^)標記開始:"^ht+p"會匹配"http://python.org"(以及"httttp://python.org"),但是不匹配"www.python.org"。類似的,字符串結尾用美元符號($)標識。

注:有關正則表達式運算符的完整列表,請參見Python類參考的4.2.1節的內容(http://python.org/doc/lib/re-syntax.html)。

2.re模塊的內容

如果不知道如何應用,只知道如何書寫正則表達式還是不夠的。re模塊包含一些有用的操作正則表達式的函數。其中最重要的一些函數如表10-9所示。

表10-9 re模塊中一些重要的函數

compile(pattern[, flags])                        根據包含正則表達式的字符串創建模式對象

search(pattern, string[, flags])                        在字符串中尋找模式

match(pattern, string[, flags])                       在字符串的開始處匹配模式

split(pattern string[, maxsplit=0])                     根據模式的匹配項來分割字符串

findall(pattern, string)                          列出字符串中模式的所有匹配項

sub(pat, repl, string[, count=0])                     將字符串中所有pat的匹配項用repl替換

escape(string)                              將字符串中所有特性正則表達式字符轉義

函數re.compile將正則表達式(以字符串書寫的)轉換成模式對象,可以實現更有效率的匹配。如果在調用search或者match函數的時候使用字符串表示的正則表達式,它們也會在內部將字符串轉換爲正則表達式對象。使用compile完成一次轉換之後,在每次使用模式的時候就不用進行轉換。模式對象本身也沒有查找/匹配的函數,就像方法一樣,所以re.search(pat, string)(pat是用字符串表示的正則表達式)等價於pat.search(string)(pat是用compile創建的模式對象)。經過compile轉換的正則表達式對象也能用於普通的re函數。

函數re.search會在給定字符串中尋找第一個匹配給定正則表達式的子字符串。一旦找到子字符串,函數就會返回MatchObject(值爲True),否則返回None(值爲False)。因爲返回值的性質,所以該函數可以用在條件語句中,如下例所示:

if re.search(pat, string):
    print "Found it!"

同時,如果需要更多有關匹配的子字符串的信息,那麼可以檢查返回的MatchObject對象(有關MatchObject更多的內容,請參見下一節)。

函數re.match會在給定字符串的開頭匹配正則表達式。因此,match("p", "python")返回真(即匹配對象MatchObject),而re.match("p", "www.python.org")則返回假(None)。

注:如果模式與字符串的開始部分相匹配,那麼match函數會給出匹配的結果,而模式並不需要匹配整個字符串。如果要求模式匹配整個字符串,那麼可以在模式的結尾加上美元符號。美元符號會對字符串的末尾進行匹配,從而“順延”了整個匹配。

函數re.split會根據模式的匹配項來分割字符串。它類似於字符串方法split,不過是用完整的正則表達式替代了固定的分隔符字符串。比如字符串方法split允許用字符串","的匹配項來分割字符串,而re.split則允許用任意長度的逗號和空格序列來分割字符串:

>>> import re
>>> some_text = "alpha, beta,,,,gamma delta"
>>> re.split("[, ]+", some_text)
['alpha', 'beta', 'gamma', 'delta']

注:如果模式包含小括號,那麼括起來的字符組合會散佈在分割後的子字符串之間。例如,re.split("o(o)", "foobar")回生成["f", "o", "bar"]。

從上述例子可以看到,返回值是子字符串的列表。maxsplit參數表示字符串最多可以分割的次數:

>>> re.split("[, ]+", some_text, maxsplit=2)
['alpha', 'beta', 'gamma delta']
>>> re.split("[, ]+", some_text, maxsplit=1)
['alpha', 'beta,,,,gamma delta']

函數re.findall以列表形式返回給定模式的所有匹配項。比如,要在字符串中查找所有的單詞,可以像下面這麼做:

>>> pat = "[a-zA-Z]+"
>>> text = '"Hm... Err -- are you sure?" he said, sounding insecure.'
>>> re.findall(pat, text)
['Hm', 'Err', 'are', 'you', 'sure', 'he', 'said', 'sounding', 'insecure']

或者查找標點符號:

>>> pat = r'[.?\-",]+'
>>> re.findall(pat, text)
['"', '...', '--', '?"', ',', '.']

注意,橫線(-)被轉義了,所以Python不會將其解釋爲字符範圍的一部分(比如a~z)。

函數re.sub的作用在於:使用給定的替換內容將匹配模式的子字符串(最左端並且非重疊的子字符串)替換掉。請思考下面的例子:

>>> pat = '{name}'
>>> text = 'Dear {name}...'
>>> re.sub(pat, "Mr. Greyson", text)
'Dear Mr. Greyson...'

請參見本章後面“作爲替換的組號和函數”部分,該部分會向你介紹如何更有效地使用這個函數。

re.escape是一個很實用的函數,它可以對字符串中所有可能被解釋爲正則運算符的字符進行轉義的應用函數。如果字符串很長且包含很多特殊字符,而你又不想輸入一大堆反斜線,或者字符串來自於用戶(比如通過raw_input函數獲取的輸入內容),且要用作正則表達式的一部分的時候,可以使用這個函數。下面的例子向你演示了該函數是如何工作的:

>>> re.escape("www.python.org")
'www\\.python\\.org'
>>> re.escape("But where is the ambiguity?")
'But\\ where\\ is\\ the\\ ambiguity\\?'

注:你可能會注意到,表10-9中有些函數包含了一個名爲flags的可選參數。這個參數用於改變解釋正則表達式的方法。有關它的更多信息,請參見Python庫參考的4.2節:http://python.org/doc/lib/module-re.html。這個標誌在4.2.3節中有介紹。

3.匹配對象和組

對於re模塊中那些能夠對字符串進行模式匹配的函數而言,當能找到匹配項的時候,它們都會返回MatchObject對象。這些對象包括匹配模式的子字符串的信息。它們還包含了那個模式匹配了子字符串哪部分的信息——這些“部分”叫做組(group)。

簡而言之,組就是放置在圓括號內的子模式。組的序號取決於它左側的括號數。組0就是整個模式,所以在下面的模式中:

"There (was a (wee) (cooper)) who (lived in Fyfe)"

包含下面這些組:

0 There was a wee cooper who lived in Fyfe
1 was a wee cooper
2 wee
3 cooper
4 lived in Fyfe

一般來說,如果組中包含諸如通配符或者重複運算符之類的特殊字符,那麼你可能會對是什麼與給定組實現了匹配感興趣,比如在下面的模式中:

r"www\.(.+)\.com$"

組0包含整個字符串,而組1則包含位於"www."和".com"之間的所有內容。像這樣創建模式的話,就可以取出字符串中感興趣的部分了。

re匹配對象的一些重要方法如表10-10所示。

表10-10 re匹配對象的重要方法

group([group1, ...])                        獲取給定子模式(組)的匹配項

start([group])                            返回給定組的匹配項的開始位置

end([group])                           返回給定組的匹配項的結束位置(和分片不一樣,不包括組的結束位置)

span([group])                            返回一個組的開始和結束位置

group方法返回模式中與給定組匹配的(子)字符串。如果沒有給出組號,默認爲組0。如果給定一個組號(或者只用默認的0),會返回單個字符串。否則會將對應給定組數的字符串作爲元組返回。

注:除了整體匹配外(組0),我們只能使用99個組,範圍1~99。

start方法返回給定組匹配項的開始索引(默認爲0,即整個模式)。

方法end類似於start,但是返回結果是結束索引加1。

方法span以元組(start,end)的形式返回給定組的開始和結束位置的索引(默認爲0,即整個模式)。

請思考以下例子:

複製代碼
>>> m = re.match(r"www\.(.*)\..{3}", "www.python.org")
>>> m.group(1)
'python'
>>> m.start(1)
4
>>> m.end(1)
10
>>> m.span(1)
(4, 10)
複製代碼

4.作爲替換的組號和函數

在使用re.sub的第一個例子中,我只是把一個字符串用其他的內容替換掉了。我用replace這個字符串方法(3.4節對此進行了介紹)能輕鬆達到同樣的效果。當然,正則表達式很有用,因爲它們允許以更靈活的方式搜索,同時它們也允許進行功能更強大的替換。

見證re.sub強大功能的最簡單方式就是在替換字符串中使用組號。在替換內容中以"\\n"形式出現的任何轉義序列都會被模式中與組n匹配的字符串替換掉。例如,假設要把"*something*"用"<em>something</em>"替換掉,前者是在普通文本文檔(比如Emaill)中進行強調的常見方法,而後者則是相應的HTML代碼(用於網頁)。我們首先建立正則表達式:

>>> emphasis_pattern = r"\*([^\*]+)\*"

注意,正則表達式很容易變得難以理解,所以爲了讓其他人(包括自己在內)在以後能夠讀懂代碼,使用有意義的變量名(或者加上一兩句註釋)是很重要的:

注:讓正則表達式變得更加易讀的方式是在re函數中使用VERBOSE標誌。它允許在模式中添加空白(空白字符、tab、換行符,等等),re則會忽略它們,除非將其放在字符類或者用反斜線轉義。也可以在冗長的正則式中添加註釋。下面的模式對象等價於剛纔寫的模式,但是使用了VERBOSE標誌:

複製代碼
>>> emphasis_pattern = re.compile(r'''
...     \*        # Beginning emphasis tag -- an asterisk
...     (         # Begin group for capturing phrase
...     [^\*]+    # Capture anything except asterisks
...     )         # End group
...     \*        # Ending emphasis tag
...     ''', re.VERBOSE)
複製代碼

現在模式已經搞定,接下來就可以使用re.sub進行替換了:

>>> re.sub(emphasis_pattern, r"<em>\1</em>", "Hello, *world*!")
'Hello, <em>world</em>!'

從上述例子可以看到,普通文本已經成功地轉換爲HTML。

將函數作爲替換內容可以讓替換功能變得更加強大。MatchObject將作爲函數的唯一參數,返回的字符串將會用做替換內容。換句話說,可以對匹配的子字符串做任何事,並且可以細化處理過程,以生成替換內容。你可能會問,這個功能用在什麼地方呢?開始使用正則表達式以後,你肯定會發現這個功能的無數應用。本章後面的“模板系統示例”部分會向你介紹它的一個應用。

貪婪和非貪婪模式

重複運算符默認是貪婪(greedy)的,這意味着它會進行儘可能多的匹配。比如,假設我重寫了剛纔用到的程序,以使用下面的模式:

>>> emphasis_pattern = r"\*(.+)\*"

它會匹配星號加上一個或多個字符,再加上一個星號的字符串。聽起來很完美吧?但實際上不是:

>>> re.sub(emphasis_pattern, r"<em>\1</em>", "*This* is *it*!")
'<em>This* is *it</em>!'

模式匹配了從開始星號到結束星號之間的所有內容——包括中間的兩個星號!也就意味着它是貪婪的:將儘可能多的東西都據爲己有。

在本例中,你當然不希望出現這種貪婪行爲。當你知道某個特定字母不合法的時候,前面的解決方案(使用字符集匹配任何不是星號的內容)纔是可行的。但是假設另外一種情況:如果使用"**something**"表示強調呢?現在在所強調的部分包括單個星號已經不是問題了,但是如何避免過於貪婪?

事實上非常簡單,只要使用重複運算符的非貪婪版本即可。所有的重複運算符都可以通過在其後面加上一個問號變成非貪婪版本:

>>> emphasis_pattern = r"\*\*(.+?)\*\*"
>>> re.sub(emphasis_pattern, r"<em>\1</em>", "**This** is **it**!")
'<em>This</em> is <em>it</em>!'

這裏用+?運算符代替了+,意味着模式也會像之前那樣隊一個或者多個通配符進行匹配,但是它會進行儘可能少的匹配,因爲它是非貪婪的。它僅會在到達"\*\*"的下一個匹配項之前匹配最少的內容——也就是在模式的結尾進行匹配。我們可以看到,代碼工作得很好。

 

5.找出Email的發信人

有沒有嘗試過將Email存爲文本文件?如果有的話,你會看到文件的頭部包含了一大堆與郵件內容無關的信息,如代碼清單10-9所示。

複製代碼
#代碼清單10-9 一組(虛構的)Email頭部信息

From [email protected] Thu Dec 20 01:22:50 2008
Return-Path: <[email protected]>
Received: from xyzzy42.bar.com (xyzzy.bar.baz [123.456.789.42])
        by frozz.bozz.floop (8.9.3/8.9.3) with ESMTP id BAA25436
        for <[email protected]>: Thu 20 Dec 2004 01:22:50 +0100 (MET)
Received: from [43.253.124.23] by bar.baz
          [InterMail vM.4.01.03.27 201-229-121-20010626] with ESMTP
          id <20041220002242.ADASD123.bar.baz@[43.253.124.23]>:
          Thu, 20 Dec 2004 00:22:42 +0000
User-Agent: Microsot-Outlook-Express-Macintosh-Edition/5.02.2022
Date: Wed, 19 Dec 2008 17:22:42 -0700
Subject: Re: Spam
From: Foo Fie <[email protected]>
To: Magnus Lie Hetland <[email protected]>
CC: <[email protected]>
Message-ID: <B8467D62.84F%[email protected]>
In-Reply-To: <[email protected]>
Mime-version: 1.0
Content-type: text/plain: charset="US-ASCII"
Content-transfer-encoding: 7bit
Status: RO
Content-Length: 55
Lines: 6

So long, and thanks for all the spam!

Yours.

Foo Fie
複製代碼

我們試着找出這封Email是誰發的。如果直接看文本,你肯定可以指出本例中的發信人(特別是查看郵件結尾簽名的話,那就更直接了)。但是能找出通用的模式嗎?怎麼能把發信人的名字取出而不帶着Email地址呢?或者如何將頭部信息中包含的Email地址列示出來呢?我們先處理第一個任務。

包含發信人的文本行以字符串"From:"作爲開始,以放置在尖括號(<和>)中的Email地址作爲結束。我們需要的文本就夾在中間。如果使用fileinput模塊,那麼這個需求就很容易實現了。代碼清單10-10給出瞭解決這個問題的程序。

注:這個問題也可以不使用正則表達式解決,可以使用email模塊。

複製代碼
# 代碼清單10-10 尋找Email發信人的程序

# RegularExpression.py
import fileinput
import re

pat = re.compile(r"From: (.*) <.*?>$")

for line in fileinput.input():
    m = pat.match(line)
    if m:
        print m.group(1)
複製代碼

可以像下面這樣運行程序(假設郵件內容存儲在文本文件message.eml中):

$ python RegularExpression.py message.eml
Foo Fie

對於這個程序,應該注意以下幾點:

 我用compile函數處理了正則表達式,讓處理過程更有效率;

 我將需要取出的子模式放在圓括號中作爲組;

 我使用非貪婪模式對郵件地址進行匹配,那麼只有最後一對尖括號符合要求(當名字包含了尖括號的情況下);

 我使用了美元符號表明我要匹配正行;

 我使用if語句確保在我試圖從特定組中取出匹配內容之前,的確進行了匹配。

爲了列出頭部信息中所有的Email地址,需要建立只匹配Email地址的正則表達式。然後可以使用findall方法尋找每行出現的匹配項。爲了避免重複,可以將地址保存在集合中(本章前面介紹過)。最後,取出所有的鍵,排序,並且打印出來:

複製代碼
import re
import fileinput

pat = re.compile(r"[a-z\-\.]+@[a-z\-\.]+", re.IGNORECASE)
addresses = set()

for line in fileinput.input():
    for address in pat.findall(line):
        addresses.add(address)

for address in sorted(addresses):
    print address
複製代碼

運行程序的時候會輸出如下結果(以代碼清單10-9的郵件信息作爲輸入):

注:在這裏,我並沒有嚴格照着問題規範去做。問題的要求是在頭部找出Email地址,但是這個程序找出了整個文件中的地址。爲了避免這種情況,如果遇到空行就可以調用fileinput.close(),因爲頭部不包含空行,遇到空行就證明工作完成了。此外,你還可以使用fileinput.nextfile()開始處理下一個文件——如果文件多於一個的話。

6.模板系統示例

模板是一種通過放入具體值從而得到某種已完成文本的文件。比如,你可能會有只需要插入收件人姓名的郵件模板。Python有一種高級的模板機制:字符串格式化。但是使用正則表達式可以讓系統更加高級。假設需要把所有"[somethings]"(字段)的匹配項替換爲通過Python表達式計算出來的something結果,所以下面的字符串:

"The sum of 7 and 9 is [7 + 9]."

應該被翻譯爲如下形式:

"The sum of 7 and 9 is 16."

同時,還可以在字段內進行賦值,所以下面的字符串:

"[name='Mr. Gumby']Hello, [name]"

應該被翻譯爲如下形式:

"Hello, Mr. Gumby"

看起來像是複雜的工作,但是我們再看一下可用的工具。

 可以使用正則表達式匹配字段,提取內容。

 可以用eval計算字符值,提供包含作用域的字典。可以在try/except語句內進行這項工作。如果引發了SyntaxError異常,可能是某些語句出現了問題(比如賦值),應該使用exec來代替。

 可以用exce執行字符串(和其他語句)的賦值操作,在字典中保存模板的作用域。

 可以使用re.sub將求值的結果替換爲處理後的字符串。

這樣看來,這項工作又不再讓人寸步難行了,對吧?

注:如果某項任務令人望而卻步,將其分解爲小一些的部分總是有用的。同時,要對解決問題所使用的工具進行評估。

代碼清單10-11是一個簡單的實現。

 Templates.py

簡單來說,程序做了下面的事情。

 定義了用於匹配字段的模式。

 創建充當模板作用域的字典。

 定義具有下列功能的替換函數。

  * 將組1從匹配中取出,放入code中;

  * 通過將作用域字典作爲命名空間來對code進行求值,將結果轉換爲字符串返回,如果成功的話。字段就是個表達式,一切正常。否則(也就是引發了SyntaxError異常),跳到下一步;

  * 執行在相同命名空間(作用域字典)內的字段來對表達式求值,返回空字符串(因爲賦值語句沒有任何內容進行求值)。

 使用fileinput讀取所有可用的行,將其放入列表,組合成一個大字符串。

 將所有field_pat的匹配項用re.sub中的替換函數進行替換,並且打印結果。

注:在之前的Python中,將所有行放入列表,最後再聯合要比下面這種方法更有效率:

text = ""
for line in fileinput.input():
    text += line

儘管看起來很優雅,但是每個賦值語句都要創建新的字符串,由舊的字符串和新增加字符串聯結在一起組成,這樣就會造成嚴重的資源浪費,使程序運行緩慢。在舊版本的Python中,使用join方法和上述做法之間的差異是巨大的。但是在最近的版本中,使用+=運算符事實上會更快。如果覺得性能很重要,那麼你可以嘗試這兩種方式。同時,如果需要一種更優雅的方式來讀取文件的所有文本,那麼請參見第十一章。

好了,我只用15行代碼(不包括空行和註釋)就創建了一個強大的模板系統。希望讀者已經認識到:使用標準庫的時候,Python有多麼強大。下面,我們通過測試這個模板系統來結束本例。試着對代碼清單10-12中的示例文本運行該系統。

# 代碼清單10-12 簡單的模板示例

[x = 2]
[y = 3]
The sum of [x] and [y] is [x + y].

應該會看到如下結果:

The sum of 2 and 3 is 5.

注:雖然看起來不明顯,但是上面的輸出包含了3個空行——兩個在文本上方,一個在下方。儘管前兩個字段已經被替換爲空字符串,但是隨後的空行還留在那裏。同時,print語句增加了新行,也就是末尾的空行。

但是等等,它還能更好!因爲使用了fileinput,我可以輪流處理幾個文件。這意味着可以使用一個文件爲變量定義值,而另一個文件作爲插入這些值的模板。比如,代碼清單10-13包含了定義文件,名爲magnus.txt,而代碼清單10-14則是模板文件,名爲template.txt。

複製代碼
# 代碼清單 10-13 一些模板定義
[name     = "Magnus Lie Hetland"]
[email    = "[email protected]"]
[language = "python"]

# 代碼清單 10-14 一個模板
[import time]
Dear [name].

I would like to learn how to program. I hear you use
the [language] language a lot -- is it something I should consider?

And, by the way, is [email] your correct email address?

Fooville, [time.asctime()]

Oscar Frozzbozz
複製代碼

import time並不是賦值語句(而是準備處理的語句類型),但是因爲我不是過分挑剔的人,所以只用了try/except語句,使得程序支持任何可以配合eval或exec使用的語句和表達式。可以像下面這樣運行程序(在UNIX命令行下):

$ python templates.py magnus.txt template.txt

你將會看到類似以下內容的輸出:

複製代碼
Dear Magnus Lie Hetland.

I would like to learn how to program. I hear you use
the python language a lot -- is it something I should consider?

And, by the way, is [email protected] your correct email address?

Fooville, Wed May 18 20:58:58 2016

Oscar Frozzbozz
複製代碼

儘管這個模板系統可以進行功能非常強大的替換,但它還是有些瑕疵的。比如,如果能夠使用更靈活的方式來編寫定義文件就更好了。如果使用execfile來執行文件,就可以使用正常的Python語法了。這樣也會解決輸出內容中頂部出現空行的問題。

還能想到其他改進的方法嗎?對於程序中使用的概念,還能想到其他用途嗎?精通任何程序設計語言的最佳方法是實踐——測試它的限制,探索它的威力。看看你能不能重寫這個程序,讓它工作得更好並且更能滿足需求。

注:事實上,在標準庫的string模塊中已經有一個非常完美的模板系統了。例如,你可以瞭解一下Template類。

 

10.3.9 其他有趣的標準模塊

儘管本章內容已經涵蓋了很多模塊,但是對於整個標準庫來說這只是冰山一角。爲了引導你進行深入探索,下面會快速介紹一些很酷的庫。

☑ functools:你可以從這個庫找到一些功能,讓你能夠通過部分參數來使用某個參數(部分求值),稍後再爲剩下的參數提供數值。在Python3.0中,filter和reduce包含在該模塊中。

 difflib:這個庫讓你可以計算兩個序列的相似度。還能讓你從一些序列中(可供選擇的序列列表)找出提供的原始序列“最像”的那個。difflib可以用於創建簡單的搜索程序。

 hashlib:通過這個模塊,你可以通過字符串計算小“簽名”(數字)。如果爲兩個不同的字符串計算出了簽名,幾乎可以確保這兩個簽名完全不同。該模塊可以應用與大文本文件,同時在加密和安全性(另見md5和sha模塊)方面有很多用途。

 csv:CSV是逗號分隔值(Comma-Separated Values)的簡寫,這是一種很多程序(比如很多電子表格和數據庫程序)都可以用來存儲表格式數據的簡單格式。它主要用於在不同程序間交換數據。使用csv模塊可以輕鬆讀寫CSV文件,同時以顯而易見的方式來處理這種格式的某些很難處理的地方。

 timeit、profile和trace:time模塊(以及它的命令行腳本)是衡量代碼片段運行時間的工具。它有很多神祕的功能,你應該用它來代替time模塊進行性能測試。profile模塊(和伴隨模塊pstats)可用於代碼片段效率的全面分析。trace模塊(和程序)可以提供總的分析(也是代碼哪部分執行了,哪部分沒執行)。這在寫測試代碼的時候很有用。

 datetime:如果time模塊不能滿足時間追蹤方面的需求,那麼datetime可能就有用武之地了。它支持特殊的日期和時間對象,讓你能夠以多種方式對它們進行構建和聯合。它的接口在很多方面比time的接口要更加直觀。

 itertools:它有很多工具用來創建和聯合迭代器(或者其他可迭代對象),還包括實現以下功能的函數:將可迭代的對象鏈接起來、創建返回無限連續整數的迭代器(和range類似,但是沒有上限),從而通過重複訪問可迭代對象進行循環等等。

 logging:通過簡單的print語句打印出程序的哪些方面很有用。如果希望對程序進行跟蹤但又不想打印出太多調試內容,那麼就需要將這些信息寫入日誌文件中了。這個模塊提供了一組標準的工具,以便讓開發人員管理一個或多個核心的日誌文件,同時還對日誌信息提供了多層次的優先級。

 getopt和optparse:在UNIX中,命令行程序經常使用不同的選項(option)或者開關(switches)運行(Python解釋器就是個典型的例子)。這些信息都可以在sys.argv中找到,但是自己要正確處理它們就沒有這麼簡單了。針對這個問題,getopt庫是個切實可行的解決方案,而optparse則更新、更強大並且更易用。

 cmd:使用這個模塊可以編寫命令行解釋器,就像Python的交互式解釋器一樣。你可以自定義命令,以便讓用戶能夠通過提示符來執行。也許你還能將它作爲程序的用戶界面。

 

10.4 小結

本章講述了模塊的知識:如何創建、如何探究以及如何使用標準Python庫中的模塊。

 模塊:從基本上來說,模塊就是子程序,它的主函數則用於定義,包括定義函數、類和變量。如果模塊包含測試代碼,那麼應該將這部分代碼放置在檢查 __name__ == '__main__'是否爲真的if語句中。能夠在PYTHONPATH中找到的模塊都可以導入。語句import foo可以導入存儲在foo.py文件中的模塊。

 包:包是包含有其他模塊的模塊。包是作爲包含__init__.py文件的目錄來實現的。

 探究模塊:將模塊導入交互式編輯器後,可以用很多方法對其進行探究。比如使用dir檢查__all__變量以及使用help函數。文檔和源碼是獲取信息和內部機制的極好來源。

 標準庫:Python包括了一些模塊,總稱爲標準庫。本章講到了其中的很多模塊,以下對其中一部分進行回顧。

    ○ sys:通過該模塊可以訪問到多個和Python解釋器聯繫緊密的變量和函數。

    ○ os:通過該模塊可以訪問到多個和操作系統聯繫緊密的變量和函數。

    ○ fileinput:通過該模塊可以輕鬆遍歷多個文件和流中所有的行。

    ○ sets、heapq和deque:這3個模塊提供了3個有用的數據結構。集合也以內建的類型set存在。

    ○ time:通過該模塊可以獲取當前時間,並可進行時間日期操作和格式化。

    ○ random:通過該模塊中的函數可以產生隨機數,從序列中選取隨機元素以及打亂列表元素。

    ○ shelve:通過該模塊可以創建持續性映射,同時將映射的內容保存在給定文件名的數據庫中。

    ○ re:支持正則表達式的模塊。

如果想要了解更多模塊,再次建議你瀏覽Python類庫參考(http://python.org/doc/lib),讀起來真的很有意思。

 

10.4.1 本章的新函數

本章涉及的新函數如表10-11所示。

表10-11 本章的新函數

dir(obj)                    返回按字母順序排序的屬性名稱列表

help([obj])                    提供交互式幫助或關於特定對象的交互式幫助信息

reload(module)                 返回已經導入模塊的重新載入版本,該函數在Python3.0將要被廢除

發佈了160 篇原創文章 · 獲贊 584 · 訪問量 90萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章