標籤:

【重讀經典】《Python核心編程(第3版)》

今天星期五,很高興馬上將開啟愉快的周末時光,今天要介紹的是指引了無數讀者入門並提高的Python殿堂的神書《Python核心編程(第3版)》中文版累計銷售超20萬冊。他被譽為提高Python技能的必讀書,書中全面涵蓋當今應用開發中的眾多領域為中級Python開發人員提供實踐方法,通過目錄脈絡慢慢了解。另外非同步社區招募書評人,後台回復「書評」,加入我們。

作者與Python

大約10多年以前,我在一家名為Four11的公司接觸到Python。當時,該公司有一個主要的產品——Four11.com White Page目錄服務。它們使用Python來設計該產品的下一代:Rocketmail Web E-mail服務,該服務最終演變為今天的Yahoo!Mail。

學習Python並加入最初的Yahoo!Mail工程團隊是一件相當有趣的事情。我幫助重新設計了地址簿和拼寫檢查程序。在當時,Python也成為其他Yahoo!站點的一部分,其中包括People Search、Yellow Pages、Maps和Driving Directions等。事實上,我當時是People Search部門的首席工程師。

儘管在當時Python對我而言是全新的,但是它也很容易學習—比我過去學習的其他語言都要簡單。在當時,Python教程的缺乏迫使我使用Library Reference和Quick Reference Guide作為主要的學習工具,而這也是促使我寫作本書的一個驅動力。

從我在Yahoo!的日子開始,我能夠以各種有趣的方式在隨後的工作中使用Python。在任何情況下,我都能使用Python的強大功能來及時地解決遇到的問題。我也開發了多門Python課程,並使用本書來講授那些課程—完全使用自己的作品。

Core Python圖書不僅是卓越的Python學習資料,它們還是用來講解Python的最佳工具。作為一名工程師,我知道學習、理解和應用一種新技術所需要的東西。作為一名專業講師,我也知道為客戶提供最有效的會話(session)所需要的是什麼。這些圖書栩栩如生,同時包含你無法從「純粹的培訓師」或「純粹的圖書作者」那裡獲得的提示。

以講解技術為主,同時容易閱讀

不同於嚴格的「入門」圖書或者純粹的「重口味」計算機科學參考圖書,我過去的教學經驗告訴我,一本易於閱讀同時又面向技術的圖書應該服務於這樣的一個目的,即能夠讓人儘可能迅速地掌握Python,以便能將其應用到十萬火急的任務上來。我們在介紹概念時會輔之以合適的案例,以加速學習過程。每章最後都會給出大量練習,旨在夯實你對書中概念和理念的理解。

能夠與Bruce Eckel的寫作風格相提並論,我很激動也很謙卑(見本書第1版的評論,網址為corepython.com)。本書並非一本枯燥的大學教材,我們的目標是營造一個與你交談的環境,就像你是在參加我的一個廣受好評的Python培訓課程一樣。作為一名終身學習的學生,我不斷地因材施教,告訴你需要學習什麼才能快速、徹底地掌握Python的概念。你也將發現,可以快速、輕鬆地閱讀本書,而且不會錯失任何技術細節。

作為一名工程師,我知道應該怎樣做才能向你講授Python中的概念。作為一名教師,我可以將技術細節全部打散,然後轉換成一種易於理解和迅速掌握的語言。你將從我的寫作風格和教學風格中獲益,更重要的是,你會喜歡上用Python來編程。

因此,你也將注意到,儘管我是本書唯一的作者,但是我使用的是「第三人稱」的寫作風格,也就是說,我使用了諸如「我們」這樣的一些廢話,原因是在學習本書的過程中,我們是一起的,共同朝著擴展Python編程技能的目標而努力。

歷 史

在本書第1版剛問世時,Python剛發布了2.0版本。從那時起,Python語言發生了重大的改進,Python語言被越來越多的人接受,其使用率也大幅提升。Python編程語言大獲成功。Python語言的缺陷已被刪除,而且有新的特性不斷加入,這將全世界Python開發人員的能力和編程修養提升到了一個新的水平。本書第2版於2006年問世,當時也是Python的鼎盛時期,它的版本是迄今為止最為流行的2.5版本。

本書第2版問世之後好評如潮,其銷量超過了第1版。在那期間,Python本身也贏得了無數榮譽,包括下面這些。

  • Tiobe(tiobe.com

    ——年度編程語言(2007年、2010年)
  • LinuxJournal(linuxjournal.com

    ——最喜歡的編程語言(2009~2011年)

    ——最喜歡的腳本語言(2006~2008年、2010年、2011年)

  • LinuxQuestions.org會員選擇獎

    ——年度編程語言(2007~2010年)

這些獎項和榮譽推動著Python進一步發展。現在,Python已經進入了下一代:Python 3。同樣,本書也在向著其「第三代」前進。我非常高興Prentice Hall能夠讓我寫作本書第3版。由於Python 3.x版本不能夠後向兼容Python 1和Python 2,因此還需要一段時間,Python 3.x才能被業界全面採用和集成進來。我們很樂意引導你經歷這個過渡。本書第3版的代碼也適用於Python 2和Python 3(視情況而定——並非所有代碼都移植了過來)。在移植代碼時,我們還會討論各種工具和做法。

Python 3.x版本帶來的挑戰延續著對Python編程語言進行迭代和改進的趨勢,要移除Python語言最後的重大缺陷還有很長的路要走,而且在不斷演變的Python語言中移除重大缺陷也是一個相當大的飛躍。與之相似,本書的結構也做出了相當重大的轉變。限於篇幅和範圍,已出版的第2版無法處理第3版中引入的所有新內容。

因此,Prentice Hall和我想到了一個好方法來向前推進本書,即從邏輯上將其拆分為兩部分,其中一部分講述Python核心語言主題,另一部分講述高級應用主題,並由此將書拆分為兩卷。而你手頭上當前拿著的這本書是Core Python Programming(第3版)的第二部分。好消息是由於第二部分的內容已經相當完整齊備,因此第一部分的內容也就沒有存在的必要了。要閱讀本書,我們建議讀者能夠擁有Python中級編程經驗。如果你最近已經學過Python,而且能夠相當輕鬆地駕馭它,或者你已經具備Python技能,但是希望能進一步提升該技能,那麼你算是找對圖書了。

Core Python Programming的讀者都知道,我的主要目標是以一種全面的方式來講解Python語言的本質,而非僅僅是其語法(學習Python的語法貌似也不需要一本書)。在知道了Python的工作機制之後—包括數據對象和內存管理之間的關係—你將成為一名更高效的Python程序員。而這是第一部分(即Core Python Language Fundamentals)要做的工作。

與本書所有版本一樣,我會繼續更新圖書的Web站點以及博客,以確保無論你移植到哪個新發布的Python版本,都可以讓本書做到與時俱進。

對之前的讀者來說,本書第3版新增了下述主題:

  • 基於Web的E-mail示例(第3章);
  • 使用Tile/Ttk(第5章);
  • 使用MongoDB(第6章);
  • 更重要的Outlook和PowerPoint示例(第7章);
  • Web伺服器網關介面(WSGI)(第10章);
  • 使用Twitter(第13章);
  • 使用Google+(第15章)。

此外,我們還在當前版本中添加了全新的3章,分別是第11章、第12章和第14章。這幾章代表著經常使用Python進行應用開發的一些新領域或正在進行的領域。所有的現有章節已經煥然一新,並更新到Python的最新版本,同時還包含了一些新內容。通過隨後的「章節指南」部分,你可以了解到本書每部分要講解的內容。

關於本書

在本書中,你將會用到從其他地方學習到的所有Python知識,並培養新的技能,從而構建自己的工具箱。藉助於該工具箱,你能夠使用Python開發各種類型的應用程序。關於高級主題的章節旨在快速概述各種不同的主題。如果你開始轉向這些章節中涵蓋的特定應用開發領域,你將會發現它們不僅給出了正確的方向,還包含了更多的信息。但是不要期待有一個深入的解決方案,因為這有悖於本書的初衷—提供更為廣泛的解決方案。

與其他所有Core Python圖書一樣,本書同樣包含了許多示例,你可以在計算機上進行嘗試。為了牢固掌握概念,你也會在每章最後發現有趣、有挑戰性的練習。這些初級和中級難度的練習旨在測試你的知識掌握情況,提升你的Python技能。畢竟,沒有什麼可以替代實踐經驗。我們相信,你不僅能夠學到很多Python編程技能,同時還能在儘可能短的時間內迅速掌握它們。

對我們來講,擴展Python技能的最佳方式就是動手練習,因此你會發現這些練習是本書的一個最大優勢。它們可以測試你對每章主題和定義的掌握情況,並激勵你儘可能多地動手編程。除了自己編寫應用程序之外,沒有其他方法可以更有效地提升你的編程技能。你需要解決初級、中級和高級難度的編程問題。而且你應該需要編寫一個大型的應用程序(這也是很多讀者想要在本書中看到的),而不是採用一些腳本來實現。坦白說,你可能做得沒有那麼好,但是通過親自動手實踐,你的收穫會更大。附錄A給出了每章中某些練習的答案。附錄B包含了一些有用的參考表。

感謝所有讀者的反饋和鼓勵,你們是我寫作這些圖書的動力。希望你們能繼續給我發送反饋信息,並促使本書第4版儘快問世,而且其質量優於之前所有版本。

本書分為3部分。其中第1部分佔據了本書2/3的篇幅,它講解了應用開發工具箱中(當然,Python是關注重點)「核心」成員的解決方案。第2部分講解了與Web編程相關的各種主題。第3部分是補充部分,它提供了一些仍然在開發過程中的實驗章節,在本書後續版本中,這些章節有望成為獨立的章節。

本書提供了一些高級主題,以展示Python可以用來開發什麼應用程序。值得高興的是,本書起碼可以向你提供Python開發中許多關鍵領域的入門知識,其中包括之前版本中提到的一些主題。

下面是本書每章的內容簡介。

第1部分:通用應用主題

第1章——正則表達式

正則表達式是一種功能強大的工具,它可以用來進行模式匹配、提取、查找和替換。

第2章——網路編程

如今許多應用都是面向網路的。該章將介紹如何使用TCP/IP與UDP/IP來創建客戶端和伺服器,以及如何快速入門SocketServer和Twisted。

第3章——網際網路客戶端編程

如今在用的大多數Internet協議都是使用套接字開發的。該章將探究一些用來構建Internet協議客戶端的高級庫。該章重點討論的是FTP、Usenet消息協議(NNTP)以及各種E-mail協議(SMTP、POP3及IMAP4)。

第4章——多線程編程

多線程編程是一種通過引入並發來提升多種應用程序執行性能的方式。該章通過解釋概念並展示正確創建Python多線程應用程序的方法、什麼是最佳用例來講解如何在Python中實現線程。

第5章——GUI編程

Tkinter(在Python 3中重名為tkinter)以Tk圖形工具包為基礎,是Python中的默認GUI開發庫。該章通過演示如何創建簡單的GUI應用來介紹Tkinter。一種最佳的學習方式是複製,並在某些應用的頂層進行創建,這樣可以很快上手。該章最後簡要討論其他圖形庫,比如Tix、Pmw、wxPython、PyGTK和Ttk/Tile。

第6章——資料庫編程

Python也有助於簡化資料庫編程。該章首先回顧一些基本概念,然後介紹Python資料庫應用編程介面(DB-API)。隨後介紹如何使用Python連接到關係資料庫,並執行查詢和操作。如果你更喜歡使用結構化查詢語言(SQL)的放手管理方法(hands-off approach),而且只是想在無須考慮底層資料庫層的情況下處理對象,則可以使用對象-關係映射。最後,該章以MongoDB作為NoSQL示例介紹了非關係資料庫。

第7章——Microsoft Office編程

無論喜歡與否,我們都生活在一個不得不和Microsoft Windows PC打交道的世界。我們可能偶爾與它們打交道,也可能每天都要接觸到它們,但是無論處於哪種情況下,都可以使用Python的強大功能來讓生活更輕鬆一些。該章將探究使用Python來編寫COM客戶端,以控制Office應用程序(比如Word、Excel、PowerPoint和Outlook)並與它們進行通信。儘管該章在本書之前版本中是實驗章節,但是我們很高興能夠為其添加足夠的內容,使其單獨成章。

第8章——擴展Python

前面提到,能夠重用代碼並對語言進行擴展將具有相當強大的功能。在純Python中,這些擴展是模塊和包,但是你也可以使用C/C++、C#或Java來開發底層的代碼。這些擴展能夠以無縫方式與Python相接。用低級編程語言來編寫自己的擴展可以提升性能,並增強安全性(因為源代碼沒有必要泄露)。該章講解使用C語言來開發擴展的整個過程。

第2部分:Web開發

第9章——Web客戶端和伺服器

該章將擴展第2章討論的客戶端/伺服器架構,我們將這一概念應用到Web上。該章不僅探究客戶端,還介紹用來解析Web內容的各種Web客戶端工具。最後,該章介紹如何使用Python來定製自己的Web伺服器。

第10章——Web編程:CGI和WSGI

Web伺服器的主要工作是接受客戶端的請求,然後返回結果。但是伺服器如何獲得客戶端的請求數據呢?由於伺服器只擅長返回結果,因此它們通常沒有獲取數據的能力或邏輯,於是這個工作需要在他處完成。CGI給了伺服器生成另外一個程序的能力,讓這個程序來進行數據處理(長久以來一直也是這麼做的),但是該程序不具備擴展性,因此並不會在實踐中使用。但是,無論使用的是什麼框架,這一概念仍然適用,因此我們將用一章的篇幅來學習CGI。該章介紹WSGI如何通過通用編程介面來為應用開發人員提供幫助。此外,該章還將介紹當框架開發人員需要在一端連接Web伺服器而應用程序的代碼放在另外一端時,WSGI如何提供幫助,以便應用開發人員能夠在無須擔心執行平台的情況下編寫代碼。

第11章——Web框架:Django

Python有很多Web框架,Django是其中最為流行的一個。該章介紹這個框架,然後介紹如何編寫簡單的Web應用。在具備了這些知識後,你可以自行研究其他Web框架。

第12章——雲計算:Google App Engine

雲計算在IT業界引發了轟動。儘管像Amazon的AWS這樣的基礎設施服務和Gmail、Yahoo!Mail這樣的在線應用等在當今世界中更為常見,但是有很多平台憑藉其強大的功能,成為這些服務的替代者。這些平台充分利用了基礎設施,無須用戶介入,而且要比雲軟體具有更多的靈活性,原因是你可以自行控制應用及其代碼。該章全面介紹使用Python的第一個平台服務——Google App Egnine。在掌握了該章的內容後,你可以探討該章介紹的其他類似服務。

第13章——Web服務

該章介紹Web上的高級服務(使用HTTP)。該章先介紹一個較為古老的服務(Yahoo!Finance),然後再給出一個較新的服務(Twitter)。該章討論如何使用Python以及前面學到的知識來與這些服務進行交互。

第3部分:補充/實驗章節

第14章——文本處理

這是本書的第一個補充章節,它介紹使用Python來處理文本的方法。該章先介紹CSV,然後是JSON,最後是XML。在該章最後一節,我們將前面學到的客戶端/伺服器知識融合到XML中,以查看如何使用XML-RPC來創建在線的遠程過程調用(RPC)。

第15章——其他內容

該章包含一些附加材料,這些內容可能會在本書下一版中成為單獨的章節。該章討論的主題包含Java/Jython和Google+。

導 讀 第8章 擴展Python

C語言效率很高。但這種效率的代價是需要用戶親自進行許多低級資源管理工作。由於現在的機器性能非常強大,這種親歷親為是得不償失的。如果能使用一種在機器執行效率較低而用戶開發效率很高的語言,則是非常明智的。Python就是這樣的一種語言。

——Eric Raymond,1996年10月

本章內容:

  • 簡介和動機;
  • 編寫Python擴展;
  • 相關主題。

本章將介紹如何編寫擴展代碼,並將其功能集成到Python編程環境中。首先介紹這樣做的動機,接著逐步介紹如何編寫擴展。需要指出的是,雖然Python擴展主要用C語言編寫,且出於通用性的考慮,本節的所有示例代碼都是純C語言代碼。因為C++是C語言的超集,所以讀者也可以使用C++。如果讀者使用Microsoft Visual Studio構建擴展,需要用到Visual C++。

8.1 簡介和動機

本章第一節將介紹什麼是Python擴展,並嘗試說明什麼情況下需要(或不需要)考慮創建一個擴展。

8.1.1 Python擴展簡介

一般來說,任何可以集成或導入另一個Python腳本的代碼都是一個擴展。這些新代碼可以使用純Python編寫,也可以使用像C和C++這樣的編譯語言編寫(在Jython中用Java編寫擴展,在IronPython中用C#或VisualBasic.NET編寫擴展)。

核心提示:在不同的平台上分別安裝客戶端和伺服器來運行網路應用程序!

這裡需要提醒一下,一般來說,即使開發環境中使用了自行編譯的Python解釋器,Python擴展也是通用的。手動編譯和獲取二進位包之間存在著微妙的關係。儘管編譯比直接下載並安裝二進位包要複雜一些,但是前者可以靈活地定製所使用的Python版本。如果需要創建擴展,就應該在與擴展最終執行環境相似的環境中進行開發。

本章的示例都是在基於UNIX的系統上構建的(這些系統通常自帶編譯器),但這裡假定讀者有可用的C/C++(或Java)編譯器,以及針對C/C++(或Java)的Python開發環境。這兩者的唯一區別僅僅是編譯方法。而擴展中的實際代碼可通用於任何平台上的Python環境中。

如果是在Windows平台上開發,需要用到Visual C++開發環境。Python發行包中自帶了7.1版的項目文件,但也可以使用老版本的VC++。

關於構建Python擴展的更多信息請查看下面的網址。

  • 針對PC上的C++:docs.python.org/extendi
  • Java/Jython:wiki.python.org/jython
  • IronPython–http://ironpython.codeplex.com

警告:

儘管在相同架構下的不同計算機之間移動二進位擴展一般情況下不會出現問題,但是有時編譯器或CPU之間的細微差別可能導致代碼不能正常工作。

Python中一個非常好的特性是,無論是擴展還是普通Python模塊,解釋器與其交互方式完全相同。這樣設計的目的是對導入的模塊進行抽象,隱藏擴展中底層代碼的實現細節。除非模塊使用者搜索相應的模塊文件,否則他就不會知道某個模塊是使用Python編寫,還是使用編譯語言編寫的。

8.1.2 什麼情況下需要擴展Python

簡要縱觀軟體工程的歷史,編程語言過去一直都根據原始定義來使用。只能使用語言定義的功能,就無法向已有的語言添加新的功能。然而,在現今的編程環境中,可定製性編程是很吸引人的特性,它可以促進代碼重用。Tcl和Python就是第一批這樣可擴展的語言,這些語言能夠擴展其語言本身。那麼為什麼需要擴展像Python這樣已經很完善的語言呢?有下面幾點充分的理由。

  • 需要Python沒有的額外功能:擴展Python的原因之一是需要該語言核心部分沒有提供一些的新功能。使用純Python或編譯後的擴展都可以做到這一點,不過像創建新的數據類型或在已有應用中嵌入Python,就必須使用編譯後的模塊。
  • 改善瓶頸性能:眾所周知,由於解釋型語言的代碼在運行時即時轉換,因此執行起來比編譯語言慢。一般來說,將一段代碼移到擴展中可以提升總體性能。但問題在於,如果轉移到擴展中,有時代價會過高。

    從性價比的角度來看,先對代碼進行一些簡單的性能分析,找出瓶頸所在,然後將這些瓶頸處的代碼移到擴展中是個更聰明的方式。這樣既能更快地獲得效率提升,也不會花費太多的資源。
  • 隱藏專有代碼:創建擴展的另一個重要原因是腳本語言的缺陷。所有這樣易用的語言都沒有關注源碼的私密性,因為這些語言的源碼本身就是可執行程序。

    將代碼從Python中轉到編譯型語言中可以隱藏這些專有代碼,因為後者提供的是二進位文件。編譯過的文件相對來說不易進行逆向工程,這樣就將源碼隱藏起來了。在涉及特殊演算法、加密或軟體安全性時這,這就顯得十分重要。

    另一個保證代碼私有的方式是只提供預編譯的.pyc文件。在提供實際代碼(.py文件)和將代碼遷移到擴展這兩種方法之間,這是比較好的折中。

8.1.3 什麼情況下不應該擴展Python

在真正介紹如何編寫擴展之前,還要了解什麼情況下不應該編寫擴展。這一節相當於一個告誡,否則讀者會認為作者一直在為擴展Python做虛假宣傳。是的,編寫擴展有前面提到的那些優點,但也有一些缺點。

  • 必須編寫C/C++代碼。
  • 需要理解如何在Python和C/C++之間傳遞數據。
  • 需要手動管理引用。
  • 還有一些封裝工具可以完成相同的事情,這些工具可以生成高效的C/C++代碼,但用戶又無須手動編寫任何C/C++代碼就可以使用這些代碼。本章末尾將介紹其中一些工具。不要說我沒提醒過你!下面繼續……

8.2 編寫Python擴展

為Python編寫擴展主要涉及三個步驟。

1.創建應用代碼。

2.根據樣板編寫封裝代碼。

3.編譯並測試。

本節將深入了解這三個步驟。

8.2.1 創建應用代碼

首先,所有需要成為擴展的代碼應該組成一個獨立的「庫」。換句話說,要明白這些代碼將作為一個Python模塊存在。因此在設計函數和對象時需要考慮Python代碼與C代碼之間的交互和數據共享,反之亦然。

下一步,創建測試代碼來保證代碼的正確性。甚至可以使用Python風格的做法,即將main()函數放在C中作為測試程序。如果代碼編譯、鏈接並載入到一個可執行程序中(而不是共享庫文件),調用這樣的可執行程序能對軟體庫進行回歸測試。下面將要介紹的擴展示例都使用這種方法。

測試用例包含兩個需要引入Python環境中的C函數。一個是遞歸階乘函數fac()。另一個是簡單的字元串逆序函數reverse(),主要用於「原地」逆序字元串,即在不額外分配字元串空間的情況下,逆序排列字元串中的字元。由於這些函數需要用到指針,因此需要仔細設計並調試這些C代碼,以防將問題帶入Python。

第1版的文件名為Extest1.c,參見示例8-1。

示例8-1 純C版本的庫(Extest1.c)

這段代碼含有兩個函數:fac()和reverse(),用來實現前面所說的功能。fac()接受一個整型參數,然後遞歸計算結果,最後從遞歸的最外層返回給調用者。

最後一部分是必要的main()函數。它用來作為測試函數,將不同的參數傳入fac()和reverse()。通過這個函數可以判斷前兩個函數是否能正常工作。

現在編譯這段代碼。許多類UNIX系統都含有gcc編譯器,在這些系統上可以使用下面的命令。

要運行代碼,可以執行下面的命令並獲得輸出。

再次強調,必須儘可能先完善擴展程序的代碼。把針對Python程序的調試與針對擴展庫本身bug的調試混在一起是一件非常痛苦的事情。換句話說,將調試核心代碼與調試Python程序分開。與Python介面的代碼寫得越完善,就越容易把它集成進Python並正確工作。

這裡每個函數都接受一個參數,也只返回一個參數。這簡單明了,因此集成進Python應該不難。注意,到目前為止,還沒涉及任何與Python相關的內容。僅僅創建了一個標準的C或C++應用而已。

8.2.2 根據樣板編寫封裝代碼

完整地實現一個擴展都圍繞「封裝」相關的概念,讀者應該熟悉這些概念,如組合類、修飾函數、類委託等。開發者需要精心設計擴展代碼,無縫連接Python和相應的擴展實現語言。這種介面代碼通常稱為樣板(boilerplate)代碼,因為如果需要與Python解釋器交互,會用到一些格式固定的代碼。

樣板代碼主要含有四部分。

1.包含Python頭文件。

2.為每一個模塊函數添加形如PyObject*Module_func()的封裝函數。

3.為每一個模塊函數添加一個PyMethodDef ModuleMethods[]數組/表。

4.添加模塊初始化函數void initModule()。

包含Python頭文件

首先要做的是找到Python包含文件,並確保編譯器可以訪問這個文件的目錄。在大多數類UNIX系統上,Python包含文件一般位於/usr/local/include/python2.x或/usr/include/python2.x中,其中2.x是Python的版本。如果通過編譯安裝的Python解釋器,應該不會有問題,因為系統知道安裝文件的位置。

將Python.h這個頭文件包含在源碼中,如下所示。

這部分很簡單。下面需要添加樣板軟體中的其他部分。

為函數編寫形如PyObject* Module_func()的封裝函數

這一部分有點難度。對於每個需要在Python環境中訪問的函數,需要創建一個以static PyObject*標識,以模塊名開頭,緊接著是下劃線和函數名本身的函數。

例如,若要讓fac()函數可以在Python中導入,並將Extest作為最終的模塊名稱,需要創建一個名為Extest_fac()的封裝函數。在用到這個函數的Python腳本中,可以使用import Extest和Extest.fac()的形式在任意地方調用fac()函數(或者先from Extest import fac,然後直接   調用fac())。

封裝函數的任務是將Python中的值轉成成C形式,接著調用相應的函數。當C函數執行完畢時,需要返回Python的環境中。封裝函數需要將返回值轉換成Pytho形式,並進行真正的返回,傳回所有需要的值。

在fac()的示例中,當客戶程序調用Extest.fac()時,會調用封裝函數。這裡會接受一個Python整數,將其轉換成C整數,接著調用C函數fac(),獲取返回結果,同樣是一個整數。將這個返回值轉換成Python整數,返回給調用者(記住,編寫的封裝函數就是def fac(n)聲明的代理函數。當這個封裝函數返回時,就相當於Python fac()函數執行完畢了)。

現在讀者可能會問,怎樣才能完成這種轉換?答案是在從Python到C時,調用一系列的PyArg_Parse*()函數,從C返回Python時,調用Py_BuildValue()函數。

這些PyArg_Parse*()函數與C中的sscanf()函數類似。其接受一個位元組流,然後根據一些格式字元串進行解析,將結果放入到相應指針所指的變數中。若解析成功就返回1;否則返回0。

Py_BuildValue()的工作方式類似sprintf(),接受一個格式字元串,並將所有參數按照格式字元串指定的格式轉換為一個Python對象。

表8-1總結了這些函數。

表8-1 在Python和C/C++之間轉換數據

在Python和C之間使用一系列的轉換編碼來轉換數據對象。轉換編碼見表8-2。

表8-2 Python①和C/C++之間的「轉換編碼」

格式編碼

Python數據類型

C/C++數據類型

① Python 2和Python 3之間的格式編碼基本相同。

② 與「O」類似,但不遞增對象的引用計數。

這些轉換編碼用在格式字元串中,用於指出對應的值在兩種語言中應該如何轉換。注意,其轉換類型不可用於Java中,Java中所有數據類型都是類。可以閱讀Jython文檔來了解Java類型和Python對象之間的對應關係。對於C#和VB.NET同樣如此。

這裡列出完整的Extest_fac()封裝函數。

封裝函數中首先解析Python中傳遞進來的參數。這裡應該是一個普通的整型變數,所以使用「i」這個轉換編碼來告知轉換函數進行相應的操作。如果參數的值確實是一個整型變數,則將其存入num變數中。否則,PyArg_ParseTuple()會返回NULL,在這種情況下封裝函數也會返回NULL。此時,它會生成TypeError異常來通知客戶端用戶,所需的參數應該是一個整型變數。

接著使用num作為參數調用fac()函數,將結果放在res中,這裡重用了res變數。現在構建返回對象,即一個Python整數,依然通過「i」這個轉換編碼。Py_BuildValue()創建一個整型Python對象,並將其返回。這就是封裝函數的所有內容。

實際上,當封裝函數寫多了後,就會試圖簡化代碼來避免使用中間變數。盡量讓代碼保持可讀性。這裡將Extest_fac()函數精簡成下面這個更短的版本,它只用了一個變數num。

那reverse()怎麼實現?由於已經知道如何返回單個值,這裡將對reverse()的需求稍微修改下,返回兩個值。將以元組的形式返回一對字元串,第一個元素是傳遞進來的原始字元串,第二個是新逆序的字元串。

為了更靈活地調用函數,這裡將該函數命名為Extest.doppel(),來表示其行為與reverse()有所不同。將C代碼封裝進Extest_doppel()函數中,如下所示。

在Extest_fac()中,接受一個字元串值作為輸入,將其存入orig_str中。注意,選擇使用「s」這個轉換編碼。接著調用strdup()來創建該字元串的副本。(因為需要返回原始字元串,同時需要一個字元串來逆序,所以最好的選擇是直接複製原始字元串。)strdup()創建並返回一個副本,該副本立即傳遞給reverse()。這樣就獲得逆序後的字元串。

如你所見,Py_BuildValue()使用轉換字元串「ss」將這兩個字元串放到了一起。這裡創建了含有原始字元串和逆序字元串的元組。都結束了嗎?還沒有。

這裡遇到了C語言中一個危險的東西:內存泄露(分配了內存但沒有釋放)。內存泄露就相當於從圖書館借書,但是沒有歸還。在獲取了某些資源後,當不再需要時,一定要釋放這些資源。我們怎麼能在代碼中犯這樣的錯誤呢(雖然看上去很無辜)?

當Py_BuildValue()將值組合到一個Python對象並返回時,它會創建傳入數據的副本。在這裡的例子中,創建了一對字元串。問題在於分配了第二個字元串的內存,但在結束時沒有釋放這段內存,導致了內存泄露。而實際想做的是構建返回值,接著釋放在封裝函數中分配的內存。為此,必須像下面這樣修改代碼。

這裡引入了dupe_str變數來指向新分配的字元串並構建返回對象。接著使用free()來釋放分配的內容,並最終返回給調用者。現在才算真正完成。

為模塊編寫PyMethodDef ModuleMethods[]數組

既然兩個封裝函數都已完成,下一步就需要在某個地方將函數列出來,以便讓Python解釋器知道如何導入並訪問這些函數。這就是ModuleMethods[]數組的任務。

這個數組由多個子數組組成,每個子數組含有一個函數的相關信息,母數組以NULL數組結尾,表示在此結束。對Extest模塊來說,創建下面這個ExtestMethods[]數組。

首先給出了在Python中訪問所用到的名稱,接著是對應的封裝函數。常量METH_VARARGS表示參數以元組的形式給定。如果使用PyArg_ParseTupleAndKeywords()來處理包含關鍵字的參數,需要將這個標記與METH_KEYWORDS常量進行邏輯OR操作。最後,使用一對NULL來表示結束函數信息列表,還表示只含有兩個函數。

添加模塊初始化函數void initModule()

最後一部分是模塊初始化函數。當解釋器導入模塊時會調用這段代碼。這段代碼中只調用了Py_InitModule()函數,其第一個參數是模塊名稱,第二個是ModuleMethods[]數組,這樣解釋器就可以訪問模塊函數。對於Extest模塊,其initExtest()過程如下所示。

現在已經完成了所有封裝任務。將Extest1.c中原先的代碼與所有這些代碼合併到一個新文件Extest2.c中。至此,就完成了示例中的所有開發步驟。

另一種創建擴展的方式是先編寫封裝代碼,使用存根(stub)函數、測試函數或假函數,在開發的過程中將其替換成具有完整功能的實現代碼。通過這種方式,可以保證Python和C之間介面的正確性,並使用Python來測試相應的C代碼。

8.2.3 編譯

現在進入了編譯階段。為了構建新的Python封裝擴展,需要將其與Python庫一同編譯。(從2.0版開始)擴展的編譯步驟已經跨平台標準化了,簡化了擴展編寫者的工作。現在使用distutils包來構建、安裝和發布模塊、擴展和軟體包。從Python 2.0開始,這種方式替換了老版本1.x中使用makefile構建擴展的方式。使用distutils,可以通過下面這些簡單的步驟構建擴展。

1.創建setup.py。

2.運行setup.py來編譯並鏈接代碼。

3.在Python中導入模塊。

4.測試函數。

創建setup.py

第一步就是創建setup.py文件。大部分編譯工作由setup()函數完成。在該函數之前的所有代碼都只是預備步驟。為了構建擴展模塊,需要為每個擴展創建一個Extension實例。因為這裡只有一個擴展,所以只需一個Extension實例。

第一個參數是擴展的完整名稱,以及該擴展中擁有的所有高階包。該名稱應該使用完整的點分割表示方式。由於這裡是個獨立的包,因此名稱為「Extest」。sources參數是所有源碼文件的列表。同樣,只有一個文件Extest2.c。

現在就可以調用setup()。其接受一個命名參數來表示構建結果的名稱,以及一個列表來表示需要構建的內容。由於這裡是創建一個擴展,因此設置一個含有擴展模塊的列表,傳遞給ext_modules。語法如下所示。

由於這裡只有一個模塊,因此將擴展模塊的實例化代碼集成到setup()的調用中,在預備步驟中將模塊名稱設置為「常量」MOD。

setup()中含有許多其他選項,這裡就不一一列舉了。讀者可以在官方的Python文檔中找到關於創建setup.py和調用setup()的更多信息,在本章末尾可以找到這些鏈接。示例8-2顯示了示例擴展中用到的完整腳本。

示例8-2 構建腳本(setup.py)

運行setup.py來編譯並鏈接代碼

既然有了setup.py文件,就運行python setup.py build命令構建擴展。這裡在Mac上完成構建(根據操作系統和Python版本的不同,對應的輸出與下面的內容會有些差別)。

8.2.4 導入並測試

最後一步是回到Python中使用擴展包,就像這個擴展就是用純Python編寫的那樣。

在Python中導入模塊

擴展模塊會創建在build/lib.*目錄下,即運行setup.py腳本的位置。要麼切換到這個目錄中,要麼用下面的方式將其安裝到Python中。

如果安裝該擴展,會得到下面的輸出。

現在可以在解釋器中測試模塊了。

添加測試函數

需要完成的最後一件事是添加測試函數。實際上,我們已經有測試函數了,就是那個main()函數。但要小心,在擴展代碼中含有main()函數有潛在的風險,因為系統中應該只有一個main()函數。將main()的名稱改成test()並對其封裝可以消除這個風險,添加Extest_test()並更新ExtestMethods數組,如下所示。

Extest_test()模塊函數僅僅運行test()並返回一個空字元串,在Python中是一個None值返回給調用者。

現在可以在Python中進行相同的測試。

示例8-3中列出了Extest2.c的最終版本,上述輸出都是用這個版本來完成的。

示例8-3 C函數庫的Python封裝版本(Extest2.c)

在這個示例中,僅在同一個文件中將原始的C代碼與Python相關的封裝代碼進行了隔離。這樣方便閱讀,在這個短小的例子中也沒有什麼問題。但在實際應用中,源碼文件會越寫越大,可以將其分割到不同的源碼文件中,使用如ExtestWrappers.c這樣好記的名字。

8.2.5 引用計數

也許讀者還記得Python使用引用計數來追蹤對象,並釋放不再引用的對象。這是Python垃圾回收機制的一部分。當創建擴展時,必須額外注意如何處理Python對象,必須留心是否需要修改此類對象的引用計數。

一個對象有兩種類型的引用,一種是擁有引用(owned reference),對該對象的引用計數遞增1表示擁有該對象的所有權。當從零創建一個Python對象時,就一定會含有一個擁有引用。

當使用完一個Python對象後,必須對所有權進行處理,要麼遞減其引用計數,通過傳遞它轉移其所有權,要麼將該對象存儲到其他容器。如果沒有處理引用計數,則會導致內存泄漏。

對象還有一個借用引用(borrowered reference)。相對來說,這種方式的責任就小一些。一般用於傳遞對象的引用,但不對數據進行任何處理。只要在其引用計數遞減至零後不繼續使用這個引用,就無須擔心其引用計數。可以通過遞增對象的引用計數來將借用引用轉成擁有引用。

Python提供了一對C宏來改變Python對象的引用計數。如表8-3所示。

{-:-}表8-3 用於執行Python對象引用計數的宏

函  數

說  明

Py_INCREF(obj)

遞增對象obj的引用計數

Py_DECREF(obj)

遞減對象obj的引用計數

在上面的Extest_test()函數中,在構建PyObject對象時使用空字元串來返回None。但可以通過擁有一個None對象來完成這個任務。即遞增一個PyNone的引用計數並顯式返回這個對象,如下所示。

Py_INCREF()和 Py_DECREF()還有一個先檢測對象是否為 NULL 的版本,分別為Py_XINCREF()和Py_XDECREF()。

這裡強烈建議讀者閱讀相關Python文檔中關於擴展和嵌入Python裡面所有關於引用計數的細節(詳見附錄C中的參考文獻部分)。

8.2.6 線程和全局解釋器鎖

擴展的編寫者必須要注意,他們的代碼可能在多線程Python環境中執行。4.3.1節介紹了Python虛擬機(Python Virtual Machine,PVM)和全局解釋器鎖(Global Interpreter Lock,GIL),描述了在PVM中,任意時間只有一個線程在執行,GIL就負責阻止其他線程的執行。除此之外,還指出了調用外部函數的代碼,如擴展代碼,將會鎖住GIL,直至外部函數返回。

但也提到了一種折衷方法,即讓擴展開發者釋放GIL。例如,在執行系統調用前就可以實現。這是通過將代碼和線程隔離實現的,這些線程使用了另外的兩個C宏:Py_BEGIN_ALLOW_THREADS和Py_END_ALLOW_THREADS,保證了運行和非運行時的安全性。用這些宏圍起來的代碼塊會允許其他線程在其執行時同步執行。

與引用計數宏相同,這裡也建議讀者閱讀Python文檔中關於擴展和嵌入Python的內容,以及Python/C API參考手冊。

延伸推薦

點擊關鍵詞閱讀更多新書:

Python|機器學習|Kotlin|Java|移動開發|機器人|有獎活動|Web前端|書單

在「非同步圖書」後台回復「關注」,即可免費獲得2000門在線視頻課程;推薦朋友關注根據提示獲取贈書鏈接,免費得非同步圖書一本。趕緊來參加哦!

點擊閱讀原文,查看本書更多信息

掃一掃上方二維碼,回復「關注」參與活動!

點擊閱讀原文,購買《Python核心編程(第3版)》


推薦閱讀:

如何找到適合需求的 Python 庫?
從零開始寫Python爬蟲 --- 2.2 Scrapy 選擇器和基本使用
Python 中 「is」 和 「==」 的問題?
喵哥的Django學習筆記1:安裝
如何回答同學知道我在學 Python 時問我「會盜 QQ 號嗎」?

TAG:Python |