【重读经典】《Python核心编程(第3版)》

异步社区 2018-01-27

python 函数 c++ 模块 测试 编程 数组


今天星期五,很高兴马上将开启愉快的周末时光,今天要介绍的是指引了无数读者入门并提高的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版的评论,网址为http://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(www.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扩展的更多信息请查看下面的网址。

警告:
尽管在相同架构下的不同计算机之间移动二进制扩展一般情况下不会出现问题,但是有时编译器或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)


下面显示的是C函数库,需要对其进行封装以便在Python解释器中使用。main()是测试函数。

upload-ueditor-image-20180127-1517027852

这段代码含有两个函数:fac()和reverse(),用来实现前面所说的功能。fac()接受一个整型参数,然后递归计算结果,最后从递归的最外层返回给调用者。

最后一部分是必要的main()函数。它用来作为测试函数,将不同的参数传入fac()和reverse()。通过这个函数可以判断前两个函数是否能正常工作。

现在编译这段代码。许多类UNIX系统都含有gcc编译器,在这些系统上可以使用下面的命令。


$ gcc Extest1.c -o Extest
$

要运行代码,可以执行下面的命令并获得输出。


$ Extest
4! == 24
8! == 40320
12! == 479001600
reversing 'abcdef', we get 'fedcba'
reversing 'madam', we get 'madam'
$

再次强调,必须尽可能先完善扩展程序的代码。把针对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这个头文件包含在源码中,如下所示。


#include "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++之间转换数据

函  数 说  明

PythonC


int PyArg_ParseTuple()

将位于元组中的一系列参数从Python转化为C

int PyArg_ParseTupleAndKeywords()

与上一个类似,但还会解析关键字参数

CPython


PyObject*Py_BuildValue()

将C数据值转化为Python返回对象,要么是单个对象,要么是一个含有多个对象的元组

在Python和C之间使用一系列的转换编码来转换数据对象。转换编码见表8-2。

表8-2 Python和C/C++之间的“转换编码”

格式编码

Python数据类型

C/C++数据类型




s、s#

str/unicode, len()

char*(, int)

z、z#

str/unicode/None,len()

char*/NULL(, int)

u、u#

unicode, len()

(Py_UNICODE*, int)

i

int

int

b

int

char

h

int

short

l

int

long

k

int或long

unsigned long

I

int或long

unsigned int

B

int

unsigned char

H

int

unsigned short

L

long

long long

K

long

unsigned long long

c

str

char

d

float

double

f

float

float

D

complex

Py_Complex*

O

(任意类型)

PyObject*

S

str

PyStringObject

N

(任意类型)

PyObject*

O&

(任意类型)

(任意类型)

① Python 2和Python 3之间的格式编码基本相同。

② 与“O”类似,但不递增对象的引用计数。

这些转换编码用在格式字符串中,用于指出对应的值在两种语言中应该如何转换。注意,其转换类型不可用于Java中,Java中所有数据类型都是类。可以阅读Jython文档来了解Java类型和Python对象之间的对应关系。对于C#和VB.NET同样如此。

这里列出完整的Extest_fac()封装函数。


static  PyObject *
Extest_fac(PyObject *self, PyObject *args) {
 int res;                 // parse result
 int  num;                 // arg for fac()
   PyObject* retval;         // return value
res = PyArg_ParseTuple(args, "i", &num);
 if(!res) {             // TypeError
   return  NULL;
}
res = fac(num);
retval = (PyObject*)Py_BuildValue("i", res);
 returnretval;
}

封装函数中首先解析Python中传递进来的参数。这里应该是一个普通的整型变量,所以使用“i”这个转换编码来告知转换函数进行相应的操作。如果参数的值确实是一个整型变量,则将其存入num变量中。否则,PyArg_ParseTuple()会返回NULL,在这种情况下封装函数也会返回NULL。此时,它会生成TypeError异常来通知客户端用户,所需的参数应该是一个整型变量。

接着使用num作为参数调用fac()函数,将结果放在res中,这里重用了res变量。现在构建返回对象,即一个Python整数,依然通过“i”这个转换编码。Py_BuildValue()创建一个整型Python对象,并将其返回。这就是封装函数的所有内容。

实际上,当封装函数写多了后,就会试图简化代码来避免使用中间变量。尽量让代码保持可读性。这里将Extest_fac()函数精简成下面这个更短的版本,它只用了一个变量num。


<strong>static </strong>PyObject *
Extest_fac(PyObject *self, PyObject *args) {
<strong>    int</strong> num;
<strong>    if </strong>(!PyArg_ParseTuple(args, "i", &num))
<strong>          return</strong> NULL;
<strong>    return </strong>(PyObject*)Py_BuildValue("i", fac(num));
}

那reverse()怎么实现?由于已经知道如何返回单个值,这里将对reverse()的需求稍微修改下,返回两个值。将以元组的形式返回一对字符串,第一个元素是传递进来的原始字符串,第二个是新逆序的字符串。

为了更灵活地调用函数,这里将该函数命名为Extest.doppel(),来表示其行为与reverse()有所不同。将C代码封装进Extest_doppel()函数中,如下所示。


<strong>static</strong> PyObject *
Extest_doppel(PyObject *self, PyObject *args) {
<strong>    char</strong> *orig_str;
<strong>    if </strong>(!PyArg_ParseTuple(args, "s", &orig_str)) <strong>return</strong> NULL;
<strong>    return </strong>(PyObject*)Py_BuildValue("ss", orig_str, \
<strong>        </strong>reverse(strdup(orig_str)));
}

在Extest_fac()中,接受一个字符串值作为输入,将其存入orig_str中。注意,选择使用“s”这个转换编码。接着调用strdup()来创建该字符串的副本。(因为需要返回原始字符串,同时需要一个字符串来逆序,所以最好的选择是直接复制原始字符串。)strdup()创建并返回一个副本,该副本立即传递给reverse()。这样就获得逆序后的字符串。

如你所见,Py_BuildValue()使用转换字符串“ss”将这两个字符串放到了一起。这里创建了含有原始字符串和逆序字符串的元组。都结束了吗?还没有。

这里遇到了C语言中一个危险的东西:内存泄露(分配了内存但没有释放)。内存泄露就相当于从图书馆借书,但是没有归还。在获取了某些资源后,当不再需要时,一定要释放这些资源。我们怎么能在代码中犯这样的错误呢(虽然看上去很无辜)?

当Py_BuildValue()将值组合到一个Python对象并返回时,它会创建传入数据的副本。在这里的例子中,创建了一对字符串。问题在于分配了第二个字符串的内存,但在结束时没有释放这段内存,导致了内存泄露。而实际想做的是构建返回值,接着释放在封装函数中分配的内存。为此,必须像下面这样修改代码。


<strong>static</strong> PyObject *
Extest_doppel(PyObject *self, PyObject *args) {
<strong>    char</strong> *orig_str;                 // original string
<strong>    char</strong> *dupe_str;                 // reversed string
<strong>    </strong>PyObject* retval;
<strong>    if </strong>(!PyArg_ParseTuple(args, "s", &orig_str)) <strong>return </strong>NULL;
<strong>    </strong>retval = (PyObject*)Py_BuildValue("ss", orig_str, \
<strong>        </strong>dupe_str=reverse(strdup(orig_str)));
<strong>    </strong>free(dupe_str);
<strong>    return</strong> retval;
}

这里引入了dupe_str变量来指向新分配的字符串并构建返回对象。接着使用free()来释放分配的内容,并最终返回给调用者。现在才算真正完成。

为模块编写PyMethodDef ModuleMethods[]数组

既然两个封装函数都已完成,下一步就需要在某个地方将函数列出来,以便让Python解释器知道如何导入并访问这些函数。这就是ModuleMethods[]数组的任务。

这个数组由多个子数组组成,每个子数组含有一个函数的相关信息,母数组以NULL数组结尾,表示在此结束。对Extest模块来说,创建下面这个ExtestMethods[]数组。


static  PyMethodDef
ExtestMethods[] = {
   { "fac", Extest_fac, METH_VARARGS },
   { "doppel", Extest_doppel, METH_VARARGS },
   { NULL, NULL },
};

首先给出了在Python中访问所用到的名称,接着是对应的封装函数。常量METH_VARARGS表示参数以元组的形式给定。如果使用PyArg_ParseTupleAndKeywords()来处理包含关键字的参数,需要将这个标记与METH_KEYWORDS常量进行逻辑OR操作。最后,使用一对NULL来表示结束函数信息列表,还表示只含有两个函数。

添加模块初始化函数void initModule()

最后一部分是模块初始化函数。当解释器导入模块时会调用这段代码。这段代码中只调用了Py_InitModule()函数,其第一个参数是模块名称,第二个是ModuleMethods[]数组,这样解释器就可以访问模块函数。对于Extest模块,其initExtest()过程如下所示。


<strong>void </strong>initExtest() {
        Py_InitModule("Extest", ExtestMethods);
   }

现在已经完成了所有封装任务。将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实例。


Extension('Extest', sources=['Extest2.c'])

第一个参数是扩展的完整名称,以及该扩展中拥有的所有高阶包。该名称应该使用完整的点分割表示方式。由于这里是个独立的包,因此名称为“Extest”。sources参数是所有源码文件的列表。同样,只有一个文件Extest2.c。

现在就可以调用setup()。其接受一个命名参数来表示构建结果的名称,以及一个列表来表示需要构建的内容。由于这里是创建一个扩展,因此设置一个含有扩展模块的列表,传递给ext_modules。语法如下所示。


setup('Extest', ext_modules=[...])

由于这里只有一个模块,因此将扩展模块的实例化代码集成到setup()的调用中,在预备步骤中将模块名称设置为“常量”MOD。


MOD = 'Extest'
setup(name=MOD, ext_modules=[
   Extension(MOD, sources=['Extest2.c'])])

setup()中含有许多其他选项,这里就不一一列举了。读者可以在官方的Python文档中找到关于创建setup.py和调用setup()的更多信息,在本章末尾可以找到这些链接。示例8-2显示了示例扩展中用到的完整脚本。

示例8-2 构建脚本(setup.py)


这段脚本将扩展编译到build/lib.*子目录中。

运行setup.py来编译并链接代码

既然有了setup.py文件,就运行python setup.py build命令构建扩展。这里在Mac上完成构建(根据操作系统和Python版本的不同,对应的输出与下面的内容会有些差别)。


$ python setup.py build
running build
running build_ext
building 'Extest' extension
creating build
creating build/temp.macosx-10.x-fat-2.x
gcc -fno-strict-aliasing -Wno-long-double -no-cpp-
precomp-mno-fused-madd -fno-common -dynamic -DNDEBUG –g
-I/usr/include -I/usr/local/include -I/sw/include -I/
usr/local/include/python2.x -c Extest2.c -o build/temp.macosx-10.x-
fat2.x/Extest2.o
creating build/lib.macosx-10.x-fat-2.x
gcc -g -bundle -undefined dynamic_lookup -L/usr/lib -L/
usr/local/lib -L/sw/lib -I/usr/include -I/usr/local/
include -I/sw/include build/temp.macosx-10.x-fat-2.x/Extest2.o -o
build/lib.macosx-10.x-fat-2.x/Extest.so

8.2.4 导入并测试

最后一步是回到Python中使用扩展包,就像这个扩展就是用纯Python编写的那样。

在Python中导入模块

扩展模块会创建在build/lib.*目录下,即运行setup.py脚本的位置。要么切换到这个目录中,要么用下面的方式将其安装到Python中。


$ python setup.py install

如果安装该扩展,会得到下面的输出。


running install
running build
running build_ext
running install_lib
copying build/lib.macosx-10.x-fat-2.x/Extest.so ->
/usr/local/lib/python2.x/site-packages

现在可以在解释器中测试模块了。


>>> <strong>import</strong> Extest
>>> Extest.fac(5)
120
>>> Extest.fac(9)
362880
>>> Extest.doppel('abcdefgh')
('abcdefgh', 'hgfedcba')
>>> Extest.doppel("Madam, I'm Adam.")
("Madam, I'm Adam.", ".madA m'I ,madaM")

添加测试函数

需要完成的最后一件事是添加测试函数。实际上,我们已经有测试函数了,就是那个main()函数。但要小心,在扩展代码中含有main()函数有潜在的风险,因为系统中应该只有一个main()函数。将main()的名称改成test()并对其封装可以消除这个风险,添加Extest_test()并更新ExtestMethods数组,如下所示。


<strong>static</strong> PyObject *
Extest_test(PyObject *self, PyObject *args) {
   test();
   <strong>return </strong>(PyObject*)Py_BuildValue("");
}
<strong>static</strong> PyMethodDef
ExtestMethods[] = {
   { "fac", Extest_fac, METH_VARARGS },
   { "doppel", Extest_doppel, METH_VARARGS },
   { "test", Extest_test, METH_VARARGS },
   { NULL, NULL },
};

Extest_test()模块函数仅仅运行test()并返回一个空字符串,在Python中是一个None值返回给调用者。

现在可以在Python中进行相同的测试。


>>> Extest.test()
4! == 24
8! == 40320
12! == 479001600
reversing 'abcdef', we get 'fedcba'
reversing 'madam', we get 'madam'
>>>

示例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的引用计数并显式返回这个对象,如下所示。


<strong>static </strong>PyObject *
Extest_test(PyObject *self, PyObject *args) {
       test();
       Py_INCREF(Py_None);
       <strong>return</strong> 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参考手册。


延伸推荐

2018年1月重磅新书

2018年初最新Python书单

小学生开始学Python,最接近AI的编程语言:安利一波Python书单

政策升温:大家都在学大数据,一大波好书推荐

一本基于Python语言的Selenium自动化测试书

AI经典书单| 入门人工智能该读哪些书?

点击关键词阅读更多新书:

Python|机器学习|Kotlin|Java|移动开发|机器人|有奖活动|Web前端|书单

异步图书”后台回复“关注”,即可免费获得2000门在线视频课程;推荐朋友关注根据提示获取赠书链接,免费得异步图书一本。赶紧来参加哦!

点击阅读原文,查看本书更多信息

扫一扫上方二维码,回复“关注”参与活动!











点击阅读原文,购买《Python核心编程(第3版)》


阅读原文阅读 6563投诉

写留言


登录 后评论
下一篇
云栖号资讯小编
1735人浏览
2020-06-29
相关推荐
程序员经典书籍清单
2784人浏览
2016-12-27 19:58:00
《Unix编程艺术》重读笔记(二)
1181人浏览
2017-05-18 14:32:24
0
2
0
2077