Dive-Into-Python3-中文版-五-

38 阅读1小时+

Dive Into Python3 中文版(五)

Chapter 16 打包 Python 类库

" You’ll find the shame is like the pain; you only feel it once. " — Marquise de Merteuil, Dangerous Liaisons

深入

读到这里,你可能是想要发布一个 Python 脚本,库,框架,或者应用程序。太棒了!世界需要更多的 Python 代码。

Python 3 自带一个名为 Distutils 的打包框架。Distutils 包含许多功能:构建工具(为你所准备),安装工具(为用户所准备),数据包格式(为搜索引擎所准备)等。它集成了 Python 安装包索引(“PyPI”),一个开源 Python 类库的中央资料库。

这些 Distutils 的不同功能以setup script为中心,一般被命名为 setup.py。事实上,你已经在本书中见过一些 Distutils 安装脚本。在 《HTTP Web Services》 一章中,我们使用 Distutils 来安装 httplib2 ,而在《案例研究:将 chardet 移植到 Python 3》一章中,我们用它安装 chardet

在本章中,你将学习 chardethttplib2 的安装脚本如何工作,并将逐步(学会)发布自己的 Python 软件。

# chardet's setup.py
from distutils.core import setup
setup(
    name = "chardet",
    packages = ["chardet"],
    version = "1.0.2",
    description = "Universal encoding detector",
    author = "Mark Pilgrim",
    author_email = "mark@diveintomark.org",
    url = "http://chardet.feedparser.org/",
    download_url = "http://chardet.feedparser.org/download/python3-chardet-1.0.1.tgz",
    keywords = ["encoding", "i18n", "xml"],
    classifiers = [
        "Programming Language :: Python",
        "Programming Language :: Python :: 3",
        "Development Status :: 4 - Beta",
        "Environment :: Other Environment",
        "Intended Audience :: Developers",
        "License :: OSI Approved :: GNU Library or Lesser General Public License (LGPL)",
        "Operating System :: OS Independent",
        "Topic :: Software Development :: Libraries :: Python Modules",
        "Topic :: Text Processing :: Linguistic",
        ],
    long_description = """\
Universal character encoding detector
-------------------------------------

Detects
 - ASCII, UTF-8, UTF-16 (2 variants), UTF-32 (4 variants)
 - Big5, GB2312, EUC-TW, HZ-GB-2312, ISO-2022-CN (Traditional and Simplified Chinese)
 - EUC-JP, SHIFT_JIS, ISO-2022-JP (Japanese)
 - EUC-KR, ISO-2022-KR (Korean)
 - KOI8-R, MacCyrillic, IBM855, IBM866, ISO-8859-5, windows-1251 (Cyrillic)
 - ISO-8859-2, windows-1250 (Hungarian)
 - ISO-8859-5, windows-1251 (Bulgarian)
 - windows-1252 (English)
 - ISO-8859-7, windows-1253 (Greek)
 - ISO-8859-8, windows-1255 (Visual and Logical Hebrew)
 - TIS-620 (Thai)

This version requires Python 3 or later; a Python 2 version is available separately.
"""
) 

chardethttplib2 都是开源的,但这并没有要求你在特定的许可下发布你自己的 Python 库。本章所描述的过程对任何 Python 软件都适用,无论它使用什么许可证

Distutils 无法为你完成的工作

发布第一个 Python 包是一项艰巨的过程。(发布第二个相对容易一些。)Distutils 试图尽可能多的自动完成一些工作,但是仍然有一些事情你必须自己做。

  • 选择一种许可协议 。. 这是一个复杂的话题,充满了派别斗争和危险。如果想将软件发布为开源软件,我冒昧地提出五点忠告:
    1. 不要撰写自己的许可证。
    2. 不要撰写自己的许可证。
    3. 不要撰写自己的许可证。
    4. 许可证并不一定必须是 GPL ,但它需要与 GPL 兼容 。
    5. 不要撰写自己的许可证。
  • 使用 PyPI 分类系统对软件进行分类。我将在本章后面的部分解释这是什么意思。
  • **写“自述”(read me)文件 。**不要在这一点吝惜精力投入。至少,它应该让你的用户了解你的软件可以干什么并知道如何安装它。

目录结构

要开始打包 Python 软件,必须先将文件和目录安排好。 httplib2 的目录树如下:

 |
   +--__init__.py
   |
   +--iri2uri.py 
  1. 创建根目录来保存所有的目录和文件。将其以 Python 模块的名字命名。
  2. 为了适应 Windows 用户,"自述"文件应包含 .txt 扩展名,而且它应该使用 Windows 风格回车符。不能仅仅因为使用了一个优秀的文本编辑器,它从命令行运行并包括它自己的宏语言,而需要让你的用户为难。(你的用户使用记事本。虽然可悲,但却是事实。)即使你工作在 Linux 或 Mac OS X 环境下,优秀的文本编辑器毫无疑问地会有一个选项,允许将文件以 Windows 风格回车符来保存。
  3. Distutils 安装脚本应命名为 setup.py,除非你有一个很好的理由不这样做。但你并没有一个很好的理由不这样做。
  4. 如果你的 Python 软件只包含一个单一的 .py 文件,你应该把它和"自述"文件以及安装脚本放到根目录下。但 httplib2 并不是单一的 .py 文件,它是一个多文件模块 。但是没关系!只需在根目录下放置 httplib2 目录,这样在 httplib2/ 根目录下就会有一个包含 __init__.py 文件的 httplib2/ 目录。这并不是一个难题,事实上,它可以简化打包过程。

chardet 目录看起来有些不同。像 httplib2 一样,它是一个多文件模块 ,所以在 chardet/ 根目录下有一个 chardet/ 目录。除了 README.txt 文件,在 docs/ 目录下, chardet 还有 HTML ——格式化文档。该 docs/ 目录包含多个 .html.css 文件和 images/ 子目录,其中包含几个 .png.gif 文件。(稍后你会发现,这将是很重要的。)此外,对于 (L)GPL 许可的软件,它包含一个单独的 COPYING.txt 文件,其中包含 LGPL 许可证的完整内容。

chardet/
|
+--COPYING.txt
|
+--setup.py
|
+--README.txt
|
+--docs/
|  |
|  +--index.html
|  |
|  +--usage.html
|  |
|  +--images/ ...
|
+--chardet/
   |
   +--__init__.py
   |
   +--big5freq.py
   |
   +--... 

编写安装脚本

Distutils 安装脚本是一份 Python 脚本。从理论上讲,它可以做任何 Python 可以做的事情。在实践中,安装脚本应该做尽可能少的事情并尽可能按标准的方式做。安装脚本应该简单。安装过程越奇异,错误报告也会更奇特。

每个 Distutils 安装脚本的第一行总是相同的:

from distutils.core import setup 

该行导入 setup() 函数,这是 Distutils 的主入口点。95% 的 Distutils 安装脚本仅由一个对 setup() 方法的调用组成。(这完全是我臆造的统计,但如果你的 Distutils 安装脚本所做的比仅仅调用 setup() 方法更多,你会有一个好的理由。你有一个好的理由吗?我并不这么认为。)

setup() 方法可以有几十个参数 。为了使每个参与者都能清楚,你必须对每个参数使用命名变量 。这不只是一项约定,还是一项硬性要求。如果尝试以非命名变量调用 setup() 方法,安装脚本会崩溃。

下面的命名变量是必需的:

  • name,安装包的名称。
  • version,安装包的版本。
  • author,您的全名。
  • author_email,您的邮件地址。
  • url,项目主页。如果没有一个单独的项目网站,这里可以是安装包的 PyPI 的页面地址。

虽然以下内容不是必须的,但我也建议你把他们包括在你的安装脚本里:

  • description,在线的项目摘要。
  • long_description,以 reStructuredText format 格式编写的多行字符串。PyPI 将其转换为 HTML 并在安装包中显示它。
  • classifiers,下一节中将讲述的特别格式化字符串。

☞安装脚本中用到的元数据具体定义在 PEP 314 中。

现在让我们看看 chardet 的安装脚本。它包含所有这些要求的和建议的参数,还有一个我没有提到: packages

from distutils.core import setup
setup(
    name = 'chardet',
    <mark>packages = ['chardet']</mark>,
    version = '1.0.2',
    description = 'Universal encoding detector',
    author='Mark Pilgrim',
    ...
) 

在分发过程中,这个 packages 参数凸显出一个不幸的词汇表重叠。我们一直在谈论正在构建的“安装包”(并将潜在地出现在 Python 包索引中)。但是,这并不是 packages 参数所指代的。它指代的是 chardet 模块是一个多文件模块这一事实 ,有时也被称为...“包”。packages 参数告诉 Distutils 去包含chardet/ 目录,它的 __init__.py 文件,以及所有其他构成 chardet 模块的 .py 文件。这还算比较重要;如果你忘记了包含实际的代码,那么所有这些关于文件和元数据的愉快交谈都将是无关紧要的。

将包分类

Python 包索引(“PyPI”)包含成千上万的 Python 库。正确的分类数据将让人们更容易找到你的包。PyPI 让你以类别的形式浏览包 。你甚至可以选择多个类别来缩小搜索范围。分类不是你可以忽略的不可见的元数据!

你可以通过传递 classifiers 参数给 Distutils 的 setup() 方法来给你的软件分类。classifers 参数是一个字符串列表。这些字符串不是任意形式的。所有的分类字符串应该来自 PyPI 上的列表

分类是可选的。你可以写一个不包含任何分类的 Distutils 安装脚本。不要这样做。 你应该总是至少包括以下分类:

  • <b0 编程语言. 特别的,你应该包括"Programming Language :: Python"和"Programming Language :: Python :: 3"。如果你不包括这些,你的包将不会出现在兼容 Python 3 的库列表中,它链接自每个pypi.python.org单页的侧边拦。
  • 许可证. 当我评价一个第三方库的时候,这绝对是我寻找的第一个东西。不要让我(花太多时间)寻找这个重要的信息。不要包含一个以上的许可证分类,除非你的软件明确地在多许可证下分发。(不要在多许可证下发布你的软件,除非你不得不这样做。不要强迫别人这样做。许可证已经足够让人头痛了,不要使情况变得更糟。)
  • 操作系统 0. 如果你的软件只能运行于 Windows(或 Mac OS X 或 Linux),我想要尽早知道。如果你的软件不包含任何特定平台的代码并可以在任何平台运行,请使用分类 "Operating System :: OS Independent"多操作系统 分类仅在你的软件在不同平台需要特别支持时使用。(这并不常见。)

我还建议你包括以下分类:

  • 开发状态. 你的软件品质适合 beta 发布么?适合 Alpha 发布么?还是 Pre-alpha?在这里面选择一个吧。要诚实点。
  • 目标用户. 谁会下载你的软件?最常见的选项包括: DevelopersEnd Users/DesktopScience/ResearchSystem Administrators
  • 框架. 如果你的软件是像 DjangoZope 这样较大的框架的插件,请包含适当的 Framework 分类。如果不是,请忽略它。
  • 主题. 有 大量的主题 可供选择 ,选择所有的适用项。

包分类的优秀范例

作为例子,下面是 Django 的分类。它是一个运行在 Web 服务器上的,可用于生产环境的,跨平台的,使用 BSD 授权的 Web 应用程序框架。(Django 还没有与 Python 3 兼容,因此, 并没有列出 Programming Language :: Python :: 3 分类。)

Programming Language :: Python
License :: OSI Approved :: BSD License
Operating System :: OS Independent
Development Status :: 5 - Production/Stable
Environment :: Web Environment
Framework :: Django
Intended Audience :: Developers
Topic :: Internet :: WWW/HTTP
Topic :: Internet :: WWW/HTTP :: Dynamic Content
Topic :: Internet :: WWW/HTTP :: WSGI
Topic :: Software Development :: Libraries :: Python Modules 

下面是 chardet 的分类。它就是在《案例研究:将 chardet 移植到 Python 3》一章提到的字符编码检测库。chardet 是高质量的,跨平台的,与 Python 3 兼容的, LGPL 许可的库。它旨在让开发者将其集成进自己的产品。

Programming Language :: Python
Programming Language :: Python :: 3
License :: OSI Approved :: GNU Library or Lesser General Public License (LGPL)
Operating System :: OS Independent
Development Status :: 4 - Beta
Environment :: Other Environment
Intended Audience :: Developers
Topic :: Text Processing :: Linguistic
Topic :: Software Development :: Libraries :: Python Modules 

以下是在本章开头我提到的 httplib2 模块——HTTP 的分类。httplib2 是一个测试品质的,跨平台的,MIT 许可证授权的,为 Python 开发者准备的模块。

Programming Language :: Python
Programming Language :: Python :: 3
License :: OSI Approved :: MIT License
Operating System :: OS Independent
Development Status :: 4 - Beta
Environment :: Web Environment
Intended Audience :: Developers
Topic :: Internet :: WWW/HTTP
Topic :: Software Development :: Libraries :: Python Modules 

通过清单指定附加文件

默认情况下,Distutils 将把下列文件包含在你的发布包中:

  • README.txt
  • setup.py
  • 由列在 packages 参数中的多模块文件所需的 .py 文件
  • py_modules 参数中列出的单独 .py 文件

这将覆盖httplib2 项目的所有文件。但对于 chardet 项目,我们还希望包含 COPYING.txt 许可文件和含有图像与 HTML 文件的整个 docs/ 目录。要让 Distutils 在构建 chardet 发布包时包含这些额外的文件和目录,你需要创建一个 manifest file

清单文件是一个名为 MANIFEST.in 的文本文件。将它放置在项目的根目录下,同 README.txtsetup.py 一起。清单文件并不是 Python 脚本,它是文本文件,其中包含一系列 Distutils 定义格式的命令。清单命令允许你包含或排除特定的文件和目录。

以下是 chardet 项目的全部清单文件:

  1. 第一行是不言自明的:包含项目根目录的 COPYING.txt 文件。
  2. 第二行有些复杂。recursive-include 命令需要一个目录名和至少一个文件名。文件名并不限于特定的文件,可以包含通配符。这行的意思是“看到在项目根目录下的 docs/ 目录了吗?在该目录下(递归地)查找 .html.css.png.gif 文件。我希望将他们都包含在我的发布包中。”

所有的清单命令都将保持你在项目目录中所设置的目录结构。recursive-include 命令不会将一组 .html.png 文件放置在你的发布包的根目录下。它将保持现有的 docs/ 目录结构,但只包含该目录内匹配给定的通配符的文件。(之前我并没有提到, chardet 的文档实际上由 XML 语言写成,并由一个单独的脚本转换为 HTML 。我不想在发布包中包含 XML 文件,只包含 HTML 文件和图像。)

☞清单文件有自己独特的格式。详见 分发指定文件清单文件命令

重申:仅仅在你需要包含一些 Distutils 不会默认包含的文件时才创建清单文件。I 如果你确实需要一个清单文件,它应该只包含那些 Distutils 不会自动包含的文件和目录。

检查安装脚本的错误

有许多事情需要留意。Distutils 带有一个内置的验证命令,它检查是否所有必须的元数据都体现在你的安装脚本中。例如,如果你忘记包含 version 参数,Distutils 会提醒你。

c:\Users\pilgrim\chardet> c:\python31\python.exe setup.py check
running check
warning: check: missing required meta-data: version 

当你包含了 version 参数(和所有其他所需的元数据)时, check 命令将如下所示:

c:\Users\pilgrim\chardet> c:\python31\python.exe setup.py check
running check 

创建发布源

Distutils 支持构建多种类型的发布包。至少,你应该建立一个“源代码分发”,其中包含源代码,你的 Distutils 安装脚本,“read me ”文件和你想要包含其他文件 。为了建立一个源代码分发,传递 sdist 命令给你的 Distutils 安装脚本。

c:\Users\pilgrim\chardet> <mark>c:\python31\python.exe setup.py sdist</mark>
running sdist
running check
reading manifest template 'MANIFEST.in'
writing manifest file 'MANIFEST'
creating chardet-1.0.2
creating chardet-1.0.2\chardet
creating chardet-1.0.2\docs
creating chardet-1.0.2\docs\images
copying files to chardet-1.0.2...
copying COPYING -> chardet-1.0.2
copying README.txt -> chardet-1.0.2
copying setup.py -> chardet-1.0.2
copying chardet\__init__.py -> chardet-1.0.2\chardet
copying chardet\big5freq.py -> chardet-1.0.2\chardet
...
copying chardet\universaldetector.py -> chardet-1.0.2\chardet
copying chardet\utf8prober.py -> chardet-1.0.2\chardet
copying docs\faq.html -> chardet-1.0.2\docs
copying docs\history.html -> chardet-1.0.2\docs
copying docs\how-it-works.html -> chardet-1.0.2\docs
copying docs\index.html -> chardet-1.0.2\docs
copying docs\license.html -> chardet-1.0.2\docs
copying docs\supported-encodings.html -> chardet-1.0.2\docs
copying docs\usage.html -> chardet-1.0.2\docs
copying docs\images\caution.png -> chardet-1.0.2\docs\images
copying docs\images\important.png -> chardet-1.0.2\docs\images
copying docs\images\note.png -> chardet-1.0.2\docs\images
copying docs\images\permalink.gif -> chardet-1.0.2\docs\images
copying docs\images\tip.png -> chardet-1.0.2\docs\images
copying docs\images\warning.png -> chardet-1.0.2\docs\images
creating dist
creating 'dist\chardet-1.0.2.zip' and adding 'chardet-1.0.2' to it
adding 'chardet-1.0.2\COPYING'
adding 'chardet-1.0.2\PKG-INFO'
adding 'chardet-1.0.2\README.txt'
adding 'chardet-1.0.2\setup.py'
adding 'chardet-1.0.2\chardet\big5freq.py'
adding 'chardet-1.0.2\chardet\big5prober.py'
...
adding 'chardet-1.0.2\chardet\universaldetector.py'
adding 'chardet-1.0.2\chardet\utf8prober.py'
adding 'chardet-1.0.2\chardet\__init__.py'
adding 'chardet-1.0.2\docs\faq.html'
adding 'chardet-1.0.2\docs\history.html'
adding 'chardet-1.0.2\docs\how-it-works.html'
adding 'chardet-1.0.2\docs\index.html'
adding 'chardet-1.0.2\docs\license.html'
adding 'chardet-1.0.2\docs\supported-encodings.html'
adding 'chardet-1.0.2\docs\usage.html'
adding 'chardet-1.0.2\docs\images\caution.png'
adding 'chardet-1.0.2\docs\images\important.png'
adding 'chardet-1.0.2\docs\images\note.png'
adding 'chardet-1.0.2\docs\images\permalink.gif'
adding 'chardet-1.0.2\docs\images\tip.png'
adding 'chardet-1.0.2\docs\images\warning.png'
removing 'chardet-1.0.2' (and everything under it) 

有几件事情需要注意:

  • Distutils 发现了清单文件( MANIFEST.in )
  • Distutils 成功地解析了清单文件,并添加了我们所需要的文件—— COPYING.txt 和在 docs/ 目录下的 HTML 与图像文件。
  • 如果你进入你的项目目录,你会看到 Distutils 创建了一个 dist/ 目录。你可以分发在 dist/ 目录中的 .zip 文件。
c:\Users\pilgrim\chardet> <mark>dir dist</mark>
 Volume in drive C has no label.
 Volume Serial Number is DED5-B4F8

 Directory of c:\Users\pilgrim\chardet\dist

07/30/2009  06:29 PM    <DIR>          .
07/30/2009  06:29 PM    <DIR>          ..
07/30/2009  06:29 PM           206,440 <mark>chardet-1.0.2.zip</mark>
               1 File(s)        206,440 bytes
               2 Dir(s)  61,424,635,904 bytes free 

创建图形化安装程序

在我看来,每一个 Python 库都应该为 Windows 用户提供图形安装程序。这很容易做(即使你并没有运行 Windows ),而且 Windows 用户会对此表示感激。

通过传递 bdist_wininst 命令到你的 Distutils 安装脚本,它可以为你创建一个图形化的 Windows 安装程序

c:\Users\pilgrim\chardet> <mark>c:\python31\python.exe setup.py bdist_wininst</mark>
running bdist_wininst
running build
running build_py
creating build
creating build\lib
creating build\lib\chardet
copying chardet\big5freq.py -> build\lib\chardet
copying chardet\big5prober.py -> build\lib\chardet
...
copying chardet\universaldetector.py -> build\lib\chardet
copying chardet\utf8prober.py -> build\lib\chardet
copying chardet\__init__.py -> build\lib\chardet
installing to build\bdist.win32\wininst
running install_lib
creating build\bdist.win32
creating build\bdist.win32\wininst
creating build\bdist.win32\wininst\PURELIB
creating build\bdist.win32\wininst\PURELIB\chardet
copying build\lib\chardet\big5freq.py -> build\bdist.win32\wininst\PURELIB\chardet
copying build\lib\chardet\big5prober.py -> build\bdist.win32\wininst\PURELIB\chardet
...
copying build\lib\chardet\universaldetector.py -> build\bdist.win32\wininst\PURELIB\chardet
copying build\lib\chardet\utf8prober.py -> build\bdist.win32\wininst\PURELIB\chardet
copying build\lib\chardet\__init__.py -> build\bdist.win32\wininst\PURELIB\chardet
running install_egg_info
Writing build\bdist.win32\wininst\PURELIB\chardet-1.0.2-py3.1.egg-info
creating 'c:\users\pilgrim\appdata\local\temp\tmp2f4h7e.zip' and adding '.' to it
adding 'PURELIB\chardet-1.0.2-py3.1.egg-info'
adding 'PURELIB\chardet\big5freq.py'
adding 'PURELIB\chardet\big5prober.py'
...
adding 'PURELIB\chardet\universaldetector.py'
adding 'PURELIB\chardet\utf8prober.py'
adding 'PURELIB\chardet\__init__.py'
removing 'build\bdist.win32\wininst' (and everything under it)
c:\Users\pilgrim\chardet> <mark>dir dist</mark>
c:\Users\pilgrim\chardet>dir dist
 Volume in drive C has no label.
 Volume Serial Number is AADE-E29F

 Directory of c:\Users\pilgrim\chardet\dist

07/30/2009  10:14 PM    <DIR>          .
07/30/2009  10:14 PM    <DIR>          ..
07/30/2009  10:14 PM           371,236 <mark>chardet-1.0.2.win32.exe</mark>
07/30/2009  06:29 PM           206,440 chardet-1.0.2.zip
               2 File(s)        577,676 bytes
               2 Dir(s)  61,424,070,656 bytes free 

为其它操作系统编译安装包

Distutils 可以帮助你为 Linux 用户构建可安装包 。我认为,这可能不值得你浪费时间。如果你希望在 Linux 中分发你的软件,你最好将时间花在与那些社区成员进行交流上,他们专门为主流 Linux 发行版打包软件。

例如,我的 chardet 库包含在 Debian GNU/Linux 软件仓库中(因而也包含在 Ubuntu 的软件仓库中)。我不曾做任何事情,我只在那里将安装包展示了一天。Debian 社区拥有他们自己的关于打包 Python 库的政策,并且 Debian 的 python-chardet 包被设计为遵循这些公约。由于这个包存在在 Debian 的软件仓库中,依赖于 Debian 用户所选择的管理自己计算机的系统设置,他们会收到该包的安全更新和(或)新版本。

Distutils 构建的包不具有 Linux 包所提供的任何优势。你的时间最好花在其他地方。

将软件添加到 Python 安装包列表

上传软件到 Python 包索引需要三个步骤。

  1. 注册你自己
  2. 注册你的软件
  3. 上传你通过 setup.py sdistsetup.py bdist_* 创建的包。

要注册自己,访问 PyPI 用户注册页面。输入你想要的用户名和密码,提供一个有效的电子邮件地址,然后点击 Register 按钮。(如果你有一个 PGP 或 GPG 密钥,你也可以提供。如果你没有或者不知道这是什么意思,不用担心。)检查你的电子邮件,在几分钟之内,你应该会收到一封来自 PyPI 的包含验证链接的邮件。点击链接以完成注册过程。

现在,你需要在 PyPI 注册你的软件并上传它。你可以用一步完成。

 running register
We need to know who you are, so please choose either:
 1\. use your existing login,
 2\. register as a new user,
 3\. have the server generate a new password for you (and email it to you), or
 4\. quit

Password:

Server response (200): OK

... output trimmed for brevity ...

... output trimmed for brevity ...

Submitting dist\chardet-1.0.2.zip to http://pypi.python.org/pypi
Server response (200): OK
Submitting dist\chardet-1.0.2.win32.exe to http://pypi.python.org/pypi
Server response (200): OK
I can store your PyPI login so future submissions will be faster.
(the login will be stored in c:\home\.pypirc) 
  1. 当你第一次发布你的项目时,Distutils 会将你的软件加入到 Python 包索引中并给出它的 URL。在这之后,它只会用你在 setup.py 参数所做的任何改变来更新项目的元数据。之后,它构建一个源代码发布 (sdist) 和一个 Windows 安装程序 (bdist_wininst) 并把他们上传到 PyPI (upload)。
  2. 键入 1ENTER 选择“ 使用已有的账户登录【use your existing login.】”。
  3. 输入你在 PyPI 用户注册页面所选择的用户名和密码。Distuils 不会回显你的密码,它甚至不会在相应的位置显示星号。只需输入你的密码,然后按 回车键
  4. Distutils 在 Python 包索引注册你的包……
  5. ……构建源代码分发……
  6. ……构建 Windows 安装程序……
  7. ……并把它们上传至 Python 包索引。
  8. 如果你想自动完成发布新版本的过程,你需要将你的 PyPI 凭据保存在一个本地文件中。这完全是不安全的而且是完全可选的。

恭喜你,现在,在 Python 包索引中有你自己的页面了!地址是 http://pypi.python.org/pypi/_NAME_,其中 NAME 是你在 setup.py 文件中 name 参数所传递的字符串。

如果你想发布一个新版本,只需以新的版本号更新 setup.py 文件,然后再一次运行相同的上传命令:

c:\Users\pilgrim\chardet> c:\python31\python.exe setup.py register sdist bdist_wininst upload 

Python 打包工具的一些可能的将来

Distutils 并非是一个代替所有并终结所有的 Python 打包,但在写本书时(2009 年 8 月),它是唯一可以工作在 Python 3 下的打包框架。对于 Python 2,还有许多其他的框架,有的重在安装,有的重在测试,还有的重在部署。在未来,它们中的一部分或全体都将移植到 Python 3。

以下框架重在安装:

以下框架重在测试和部署:

深入阅读

关于 Distutils:

其它打包框架:

你的位置: Home ‣ Dive Into Python 3 ‣

难度等级: ♦♦♦♦♦

Chapter A 使用2to3将代码移植到 Python 3

" Life is pleasant. Death is peaceful. It’s the transition that’s troublesome. " — Isaac Asimov (attributed)

概述

几乎所有的 Python 2 程序都需要一些修改才能正常地运行在 Python 3 的环境下。为了简化这个转换过程,Python 3 自带了一个叫做2to3的实用脚本(Utility Script),这个脚本会将你的 Python 2 程序源文件作为输入,然后自动将其转换到 Python 3 的形式。案例研究:将chardet移植到 Python 3(porting chardet to Python 3)描述了如何运行这个脚本,然后展示了一些它不能自动修复的情况。这篇附录描述了它能够自动修复的内容。

print语句

在 Python 2 里,print是一个语句。无论你想输出什么,只要将它们放在print关键字后边就可以。在 Python 3 里,print()是一个函数。就像其他的函数一样,print()需要你将想要输出的东西作为参数传给它。

NotesPython 2Python 3
printprint()
print 1print(1)
print 1, 2print(1, 2)
print 1, 2,print(1, 2, end=' ')
print &gt;&gt;sys.stderr, 1, 2, 3print(1, 2, 3, file=sys.stderr)
  1. 为输出一个空白行,需要调用不带参数的print()
  2. 为输出一个单独的值,需要将这这个值作为print()的一个参数就可以了。
  3. 为输出使用一个空格分隔的两个值,用两个参数调用print()即可。
  4. 这个例子有一些技巧。在 Python 2 里,如果你使用一个逗号(,)作为print语句的结尾,它将会用空格分隔输出的结果,然后在输出一个尾随的空格(trailing space),而不输出回车(carriage return)。在 Python 3 里,通过把end=' '作为一个关键字参数传给print()可以实现同样的效果。参数end的默认值为'\n',所以通过重新指定end参数的值,可以取消在末尾输出回车符。
  5. 在 Python 2 里,你可以通过使用&gt;&gt;pipe_name语法,把输出重定向到一个管道,比如sys.stderr。在 Python 3 里,你可以通过将管道作为关键字参数file的值传递给print()来完成同样的功能。参数file的默认值为std.stdout,所以重新指定它的值将会使print()输出到一个另外一个管道。

Unicode 字符串

Python 2 有两种字符串类型:Unicode 字符串和非 Unicode 字符串。Python 3 只有一种类型:Unicode 字符串(Unicode strings)。

NotesPython 2Python 3
u'PapayaWhip''PapayaWhip'
ur'PapayaWhip\foo'r'PapayaWhip\foo'
  1. Python 2 里的 Unicode 字符串在 Python 3 里即普通字符串,因为在 Python 3 里字符串总是 Unicode 形式的。
  2. Unicode 原始字符串(raw string)(使用这种字符串,Python 不会自动转义反斜线"")也被替换为普通的字符串,因为在 Python 3 里,所有原始字符串都是以 Unicode 编码的。

全局函数unicode()

Python 2 有两个全局函数可以把对象强制转换成字符串:unicode()把对象转换成 Unicode 字符串,还有str()把对象转换为非 Unicode 字符串。Python 3 只有一种字符串类型,Unicode 字符串,所以str()函数即可完成所有的功能。(unicode()函数在 Python 3 里不再存在了。)

NotesPython 2Python 3
unicode(anything)str(anything)

long 长整型

Python 2 有为非浮点数准备的intlong类型。int类型的最大值不能超过sys.maxint,而且这个最大值是平台相关的。可以通过在数字的末尾附上一个L来定义长整型,显然,它比int类型表示的数字范围更大。在 Python 3 里,只有一种整数类型int,大多数情况下,它很像 Python 2 里的长整型。由于已经不存在两种类型的整数,所以就没有必要使用特殊的语法去区别他们。

进一步阅读:PEP 237:统一长整型和整型

NotesPython 2Python 3
x = 1000000000000Lx = 1000000000000
x = 0xFFFFFFFFFFFFLx = 0xFFFFFFFFFFFF
long(x)int(x)
type(x) is longtype(x) is int
isinstance(x, long)isinstance(x, int)
  1. 在 Python 2 里的十进制长整型在 Python 3 里被替换为十进制的普通整数。
  2. 在 Python 2 里的十六进制长整型在 Python 3 里被替换为十六进制的普通整数。
  3. 在 Python 3 里,由于长整型已经不存在了,自然原来的long()函数也没有了。为了强制转换一个变量到整型,可以使用int()函数。
  4. 检查一个变量是否是整型,获得它的数据类型,并与一个int类型(不是long)的作比较。
  5. 你也可以使用isinstance()函数来检查数据类型;再强调一次,使用int,而不是long,来检查整数类型。

<> 比较运算符

Python 2 支持&lt;&gt;作为!=的同义词。Python 3 只支持!=,不再支持<>了。

NotesPython 2Python 3
if x &lt;&gt; y:if x != y:
if x &lt;&gt; y &lt;&gt; z:if x != y != z:
  1. 简单地比较。
  2. 相对复杂的三个值之间的比较。

字典类方法has_key()

在 Python 2 里,字典对象的has_key()方法用来测试字典是否包含特定的键(key)。Python 3 不再支持这个方法了。你需要使用in运算符。

NotesPython 2Python 3
a_dictionary.has_key('PapayaWhip')'PapayaWhip' in a_dictionary
a_dictionary.has_key(x) or a_dictionary.has_key(y)x in a_dictionary or y in a_dictionary
a_dictionary.has_key(x or y)(x or y) in a_dictionary
a_dictionary.has_key(x + y)(x + y) in a_dictionary
x + a_dictionary.has_key(y)x + (y in a_dictionary)
  1. 最简单的形式。
  2. 运算符or的优先级高于运算符in,所以这里不需要添加括号。
  3. 另一方面,出于同样的原因 — or的优先级大于in,这里需要添加括号。(注意:这里的代码与前面那行完全不同。Python 会先解释x or y,得到结果x(如果x在布尔上下文里的值是真)或者y。然后 Python 检查这个结果是不是a_dictionary的一个键。)
  4. 运算符in的优先级大于运算符+,所以代码里的这种形式从技术上说不需要括号,但是2to3还是添加了。
  5. 这种形式一定需要括号,因为in的优先级大于+

返回列表的字典类方法

在 Python 2 里,许多字典类方法的返回值是列表。其中最常用方法的有keysitemsvalues。在 Python 3 里,所有以上方法的返回值改为动态视图(dynamic view)。在一些上下文环境里,这种改变并不会产生影响。如果这些方法的返回值被立即传递给另外一个函数,并且那个函数会遍历整个序列,那么以上方法的返回值是列表或者视图并不会产生什么不同。在另外一些情况下,Python 3 的这些改变干系重大。如果你期待一个能被独立寻址元素的列表,那么 Python 3 的这些改变将会使你的代码卡住(choke),因为视图(view)不支持索引(indexing)。

NotesPython 2Python 3
a_dictionary.keys()list(a_dictionary.keys())
a_dictionary.items()list(a_dictionary.items())
a_dictionary.iterkeys()iter(a_dictionary.keys())
[i for i in a_dictionary.iterkeys()][i for i in a_dictionary.keys()]
min(a_dictionary.keys())no change
  1. 使用list()函数将keys()的返回值转换为一个静态列表,出于安全方面的考量,2to3可能会报错。这样的代码是有效的,但是对于使用视图来说,它的效率低一些。你应该检查转换后的代码,看看是否一定需要列表,也许视图也能完成同样的工作。
  2. 这是另外一种视图(关于items()方法的)到列表的转换。2to3values()方法返回值的转换也是一样的。
  3. Python 3 里不再支持iterkeys()了。如果必要,使用iter()keys()的返回值转换成为一个迭代器。
  4. 2to3能够识别出iterkeys()方法在列表解析里被使用,然后将它转换为 Python 3 里的keys()方法(不需要使用额外的iter()去包装其返回值)。这样是可行的,因为视图是可迭代的。
  5. 2to3也能识别出keys()方法的返回值被立即传给另外一个会遍历整个序列的函数,所以也就没有必要先把keys()的返回值转换到一个列表。相反的,min()函数会很乐意遍历视图。这个过程对min()max()sum()list()tuple()set()sorted()any()all()同样有效。

被重命名或者重新组织的模块

从 Python 2 到 Python 3,标准库里的一些模块已经被重命名了。还有一些相互关联的模块也被组合或者重新组织,以使得这种关联更有逻辑性。

http

在 Python 3 里,几个相关的 HTTP 模块被组合成一个单独的包,即http

NotesPython 2Python 3
import httplibimport http.client
import Cookieimport http.cookies
import cookielibimport http.cookiejar
import BaseHTTPServer import SimpleHTTPServer import CGIHttpServerimport http.server
  1. http.client模块实现了一个底层的库,可以用来请求 HTTP 资源,解析 HTTP 响应。
  2. http.cookies模块提供一个蟒样的(Pythonic)接口来获取通过 HTTP 头部(HTTP header)Set-Cookie 发送的 cookies
  3. 常用的流行的浏览器会把 cookies 以文件形式存放在磁盘上,http.cookiejar模块可以操作这些文件。
  4. http.server模块实现了一个基本的 HTTP 服务器

urllib

Python 2 有一些用来分析,编码和获取 URL 的模块,但是这些模块就像老鼠窝一样相互重叠。在 Python 3 里,这些模块被重构、组合成了一个单独的包,即urllib

NotesPython 2Python 3
import urllibimport urllib.request, urllib.parse, urllib.error
import urllib2import urllib.request, urllib.error
import urlparseimport urllib.parse
import robotparserimport urllib.robotparser
from urllib import FancyURLopener from urllib import urlencodefrom urllib.request import FancyURLopener from urllib.parse import urlencode
from urllib2 import Request from urllib2 import HTTPErrorfrom urllib.request import Request from urllib.error import HTTPError
  1. 以前,Python 2 里的urllib模块有各种各样的函数,包括用来获取数据的urlopen(),还有用来将 URL 分割成其组成部分的splittype()splithost()splituser()函数。在新的urllib包里,这些函数被组织得更有逻辑性。2to3 将会修改这些函数的调用以适应新的命名方案。
  2. 在 Python 3 里,以前的urllib2模块被并入了urllib包。同时,以urllib2里各种你最喜爱的东西将会一个不缺地出现在 Python 3 的urllib模块里,比如build_opener()方法,Request对象,HTTPBasicAuthHandler和 friends。
  3. Python 3 里的urllib.parse模块包含了原来 Python 2 里urlparse模块所有的解析函数。
  4. urllib.robotparse模块解析robots.txt文件
  5. 处理 HTTP 重定向和其他状态码的FancyURLopener类在 Python 3 里的urllib.request模块里依然有效。urlencode()函数已经被转移到了urllib.parse里。
  6. Request对象在urllib.request里依然有效,但是像HTTPError这样的常量已经被转移到了urllib.error里。

我是否有提到2to3也会重写你的函数调用?比如,如果你的 Python 2 代码里导入了urllib模块,调用了urllib.urlopen()函数获取数据,2to3会同时修改import语句和函数调用。

NotesPython 2Python 3
import urllib print urllib.urlopen('http://diveintopython3.org/').read()import urllib.request, urllib.parse, urllib.error``print(urllib.request.urlopen('http://diveintopython3.org/').read())

dbm

所有的 DBM 克隆(DBM clone)现在在单独的一个包里,即dbm。如果你需要其中某个特定的变体,比如 GNU DBM,你可以导入dbm包中合适的模块。

NotesPython 2Python 3
import dbmimport dbm.ndbm
import gdbmimport dbm.gnu
import dbhashimport dbm.bsd
import dumbdbmimport dbm.dumb
import anydbm import whichdbimport dbm

xmlrpc

XML-RPC 是一个通过 HTTP 协议执行远程 RPC 调用的轻重级方法。一些 XML-RPC 客户端和 XML-RPC 服务端的实现库现在被组合到了独立的包,即xmlrpc

NotesPython 2Python 3
import xmlrpclibimport xmlrpc.client
import DocXMLRPCServer import SimpleXMLRPCServerimport xmlrpc.server

其他模块

NotesPython 2Python 3
[1]import io
[2]import pickle
import __builtin__import builtins
import copy_regimport copyreg
import Queueimport queue
import SocketServerimport socketserver
import ConfigParserimport configparser
import reprimport reprlib
import commandsimport subprocess

[1]:

try:
    import cStringIO as StringIO
except ImportError:
    import StringIO 

[2]:

try:
    import cPickle as pickle
except ImportError:
    import pickle 
  1. 在 Python 2 里,你通常会这样做,首先尝试把cStringIO导入作为StringIO的替代,如果失败了,再导入StringIO。不要在 Python 3 里这样做;io模块会帮你处理好这件事情。它会找出可用的最快实现方法,然后自动使用它。
  2. 在 Python 2 里,导入最快的pickle实现也是一个与上边相似的能用方法。在 Python 3 里,pickle模块会自动为你处理,所以不要再这样做。
  3. builtins模块包含了在整个 Python 语言里都会使用的全局函数,类和常量。重新定义builtins模块里的某个函数意味着在每处都重定义了这个全局函数。这听起来很强大,但是同时也是很可怕的。
  4. copyreg模块为用 C 语言定义的用户自定义类型添加了pickle模块的支持。
  5. queue模块实现一个生产者消费者队列(multi-producer, multi-consumer queue)。
  6. socketserver模块为实现各种 socket server 提供了通用基础类。
  7. configparser模块用来解析 INI-style 配置文件。
  8. reprlib模块重新实现了内置函数repr(),并添加了对字符串表示被截断前长度的控制。
  9. subprocess模块允许你创建子进程,连接到他们的管道,然后获取他们的返回值。

包内的相对导入

包是由一组相关联的模块共同组成的单个实体。在 Python 2 的时候,为了实现同一个包内模块的相互引用,你会使用import foo或者from foo import Bar。Python 2 解释器会先在当前目录里搜索foo.py,然后再去 Python 搜索路径(sys.path)里搜索。在 Python 3 里这个过程有一点不同。Python 3 不会首先在当前路径搜索,它会直接在 Python 的搜索路径里寻找。如果你想要包里的一个模块导入包里的另外一个模块,你需要显式地提供两个模块的相对路径。

假设你有如下包,多个文件在同一个目录下:

chardet/
|
+--__init__.py
|
+--constants.py
|
+--mbcharsetprober.py
|
+--universaldetector.py 

现在假设universaldetector.py需要整个导入constants.py,另外还需要导入mbcharsetprober.py的一个类。你会怎样做?

NotesPython 2Python 3
import constantsfrom . import constants
from mbcharsetprober import MultiByteCharSetProberfrom .mbcharsetprober import MultiByteCharsetProber
  1. 当你需要从包的其他地方导入整个模块,使用新的from . import语法。这里的句号(.)即表示当前文件(universaldetector.py)和你想要导入文件(constants.py)之间的相对路径。在这个样例中,这两个文件在同一个目录里,所以使用了单个句号。你也可以从父目录(from .. import anothermodule)或者子目录里导入。
  2. 为了将一个特定的类或者函数从其他模块里直接导入到你的模块的名字空间里,在需要导入的模块名前加上相对路径,并且去掉最后一个斜线(slash)。在这个例子中,mbcharsetprober.pyuniversaldetector.py在同一个目录里,所以相对路径名就是一个句号。你也可以从父目录(from .. import anothermodule)或者子目录里导入。

迭代器方法next()

在 Python 2 里,迭代器有一个next()方法,用来返回序列里的下一项。在 Python 3 里这同样成立,但是现在有了一个新的全局的函数next(),它使用一个迭代器作为参数。

NotesPython 2Python 3
anIterator.next()next(anIterator)
a_function_that_returns_an_iterator().next()next(a_function_that_returns_an_iterator())
[1][2]
[3]no change
[4][5]

[1]:

class A:
    def next(self):
        pass 

[2]:

class A:
    def __next__(self):
        pass 

[3]:

class A:
    def next(self, x, y):
        pass 

[4]:

next = 42
for an_iterator in a_sequence_of_iterators:
    an_iterator.next() 

[5]:

next = 42
for an_iterator in a_sequence_of_iterators:
    an_iterator.__next__() 
  1. 最简单的例子,你不再调用一个迭代器的next()方法,现在你将迭代器自身作为参数传递给全局函数next()
  2. 假如你有一个返回值是迭代器的函数,调用这个函数然后把结果作为参数传递给next()函数。(2to3脚本足够智能以正确执行这种转换。)
  3. 假如你想定义你自己的类,然后把它用作一个迭代器,在 Python 3 里,你可以通过定义特殊方法__next__()来实现。
  4. 如果你定义的类里刚好有一个next(),它使用一个或者多个参数,2to3执行的时候不会动它。这个类不能被当作迭代器使用,因为它的next()方法带有参数。
  5. 这一个有些复杂。如果你恰好有一个叫做next的本地变量,在 Python 3 里它的优先级会高于全局函数next()。在这种情况下,你需要调用迭代器的特别方法__next__()来获取序列里的下一个元素。(或者,你也可以重构代码以使这个本地变量的名字不叫next,但是 2to3 不会为你做这件事。)

全局函数filter()

在 Python 2 里,filter()方法返回一个列表,这个列表是通过一个返回值为True或者False的函数来检测序列里的每一项得到的。在 Python 3 里,filter()函数返回一个迭代器,不再是列表。

NotesPython 2Python 3
filter(a_function, a_sequence)list(filter(a_function, a_sequence))
list(filter(a_function, a_sequence))no change
filter(None, a_sequence)[i for i in a_sequence if i]
for i in filter(None, a_sequence):no change
[i for i in filter(a_function, a_sequence)]no change
  1. 最简单的情况下,2to3会用一个list()函数来包装filter()list()函数会遍历它的参数然后返回一个列表。
  2. 然而,如果filter()调用已经被list()包裹,2to3不会再做处理,因为这种情况下filter()的返回值是否是一个迭代器是无关紧要的。
  3. 为了处理filter(None, ...)这种特殊的语法,2to3会将这种调用从语法上等价地转换为列表解析。
  4. 由于for循环会遍历整个序列,所以没有必要再做修改。
  5. 与上面相同,不需要做修改,因为列表解析会遍历整个序列,即使filter()返回一个迭代器,它仍能像以前的filter()返回列表那样正常工作。

全局函数map()

filter()作的改变一样,map()函数现在返回一个迭代器。(在 Python 2 里,它返回一个列表。)

NotesPython 2Python 3
map(a_function, 'PapayaWhip')list(map(a_function, 'PapayaWhip'))
map(None, 'PapayaWhip')list('PapayaWhip')
map(lambda x: x+1, range(42))[x+1 for x in range(42)]
for i in map(a_function, a_sequence):no change
[i for i in map(a_function, a_sequence)]no change
  1. 类似对filter()的处理,在最简单的情况下,2to3会用一个list()函数来包装map()调用。
  2. 对于特殊的map(None, ...)语法,跟filter(None, ...)类似,2to3会将其转换成一个使用list()的等价调用
  3. 如果map()的第一个参数是一个 lambda 函数,2to3会将其等价地转换成列表解析。
  4. 对于会遍历整个序列的for循环,不需要做改变。
  5. 再一次地,这里不需要做修改,因为列表解析会遍历整个序列,即使map()的返回值是迭代器而不是列表它也能正常工作。

全局函数reduce()

在 Python 3 里,reduce()函数已经被从全局名字空间里移除了,它现在被放置在fucntools模块里。

NotesPython 2Python 3
reduce(a, b, c)from functools import reduce reduce(a, b, c)

全局函数apply()

Python 2 有一个叫做apply()的全局函数,它使用一个函数f和一个列表[a, b, c]作为参数,返回值是f(a, b, c)。你也可以通过直接调用这个函数,在列表前添加一个星号(*)作为参数传递给它来完成同样的事情。在 Python 3 里,apply()函数不再存在了;必须使用星号标记法。

NotesPython 2Python 3
apply(a_function, a_list_of_args)a_function(*a_list_of_args)
apply(a_function, a_list_of_args, a_dictionary_of_named_args)a_function(*a_list_of_args, **a_dictionary_of_named_args)
apply(a_function, a_list_of_args + z)a_function(*a_list_of_args + z)
apply(aModule.a_function, a_list_of_args)aModule.a_function(*a_list_of_args)
  1. 最简单的形式,可以通过在参数列表(就像[a, b, c]一样)前添加一个星号来调用函数。这跟 Python 2 里的apply()函数是等价的。
  2. 在 Python 2 里,apply()函数实际上可以带 3 个参数:一个函数,一个参数列表,一个字典命名参数(dictionary of named arguments)。在 Python 3 里,你可以通过在参数列表前添加一个星号(*),在字典命名参数前添加两个星号(**)来达到同样的效果。
  3. 运算符+在这里用作连接列表的功能,它的优先级高于运算符*,所以没有必要在a_list_of_args + z周围添加额外的括号。
  4. 2to3脚本足够智能来转换复杂的apply()调用,包括调用导入模块里的函数。

全局函数intern()

在 Python 2 里,你可以用intern()函数作用在一个字符串上来限定(intern)它以达到性能优化。在 Python 3 里,intern()函数被转移到sys模块里了。

NotesPython 2Python 3
intern(aString)sys.intern(aString)

exec语句

就像print语句在 Python 3 里变成了一个函数一样,exec语句也是这样的。exec()函数使用一个包含任意 Python 代码的字符串作为参数,然后就像执行语句或者表达式一样执行它。exec()eval()是相似的,但是exec()更加强大并更具有技巧性。eval()函数只能执行单独一条表达式,但是``exec()能够执行多条语句,导入(import),函数声明 — 实际上整个 Python 程序的字符串表示也可以。

NotesPython 2Python 3
exec codeStringexec(codeString)
exec codeString in a_global_namespaceexec(codeString, a_global_namespace)
exec codeString in a_global_namespace, a_local_namespaceexec(codeString, a_global_namespace, a_local_namespace)
  1. 在最简单的形式下,因为exec()现在是一个函数,而不是语句,2to3会把这个字符串形式的代码用括号围起来。
  2. Python 2 里的exec语句可以指定名字空间,代码将在这个由全局对象组成的私有空间里执行。Python 3 也有这样的功能;你只需要把这个名字空间作为第二个参数传递给exec()函数。
  3. 更加神奇的是,Python 2 里的exec语句还可以指定一个本地名字空间(比如一个函数里声明的变量)。在 Python 3 里,exec()函数也有这样的功能。

execfile语句

就像以前的exec语句,Python 2 里的execfile语句也可以像执行 Python 代码那样使用字符串。不同的是exec使用字符串,而execfile则使用文件。在 Python 3 里,execfile语句已经被去掉了。如果你真的想要执行一个文件里的 Python 代码(但是你不想导入它),你可以通过打开这个文件,读取它的内容,然后调用compile()全局函数强制 Python 解释器编译代码,然后调用新的exec()函数。

NotesPython 2Python 3
execfile('a_filename')exec(compile(open('a_filename').read(), 'a_filename', 'exec'))

repr(反引号)

在 Python 2 里,为了得到一个任意对象的字符串表示,有一种把对象包装在反引号里(比如x)的特殊语法。在 Python 3 里,这种能力仍然存在,但是你不能再使用反引号获得这种字符串表示了。你需要使用全局函数repr()

NotesPython 2Python 3
xrepr(x)
'PapayaWhip' + 2repr('PapayaWhip' + repr(2))
  1. 记住,x可以是任何东西 — 一个类,函数,模块,基本数据类型,等等。repr()函数可以使用任何类型的参数。
  2. 在 Python 2 里,反引号可以嵌套,导致了这种令人费解的(但是有效的)表达式。2to3足够智能以将这种嵌套调用转换到repr()函数。

try...except语句

从 Python 2 到 Python 3,捕获异常的语法有些许变化。

NotesPython 2Python 3
[1][2]
[3][4]
[5]no change
[6]no change

[1]:

try:
    import mymodule
except ImportError, e
    pass 

[2]:

try:
    import mymodule
except ImportError as e:
    pass 

[3]:

try:
    import mymodule
except (RuntimeError, ImportError), e
    pass 

[4]:

try:
    import mymodule
except (RuntimeError, ImportError) as e:
    pass 

[5]:

try:
    import mymodule
except ImportError:
    pass 

[6]:

try:
    import mymodule
except:
    pass 
  1. 相对于 Python 2 里在异常类型后添加逗号,Python 3 使用了一个新的关键字,as
  2. 关键字as也可以用在一次捕获多种类型异常的情况下。
  3. 如果你捕获到一个异常,但是并不在意访问异常对象本身,Python 2 和 Python 3 的语法是一样的。
  4. 类似地,如果你使用一个保险方法(fallback)来捕获所有异常,Python 2 和 Python 3 的语法是一样的。

☞在导入模块(或者其他大多数情况)的时候,你绝对不应该使用这种方法(指以上的 fallback)。不然的话,程序可能会捕获到像KeyboardInterrupt(如果用户按Ctrl-C来中断程序)这样的异常,从而使调试变得更加困难。

raise语句

Python 3 里,抛出自定义异常的语法有细微的变化。

NotesPython 2Python 3
raise MyExceptionunchanged
raise MyException, 'error message'raise MyException('error message')
raise MyException, 'error message', a_tracebackraise MyException('error message').with_traceback(a_traceback)
raise 'error message'unsupported
  1. 抛出不带用户自定义错误信息的异常,这种最简单的形式下,语法没有改变。
  2. 当你想要抛出一个带用户自定义错误信息的异常时,改变就显而易见了。Python 2 用一个逗号来分隔异常类和错误信息;Python 3 把错误信息作为参数传递给异常类。
  3. Python 2 支持一种更加复杂的语法来抛出一个带用户自定义回溯(stack trace,堆栈追踪)的异常。在 Python 3 里你也可以这样做,但是语法完全不同。
  4. 在 Python 2 里,你可以抛出一个不带异常类的异常,仅仅只有一个异常信息。在 Python 3 里,这种形式不再被支持。2to3将会警告你它不能自动修复这种语法。

生成器的throw方法

在 Python 2 里,生成器有一个throw()方法。调用a_generator.throw()会在生成器被暂停的时候抛出一个异常,然后返回由生成器函数获取的下一个值。在 Python 3 里,这种功能仍然可用,但是语法上有一点不同。

NotesPython 2Python 3
a_generator.throw(MyException)no change
a_generator.throw(MyException, 'error message')a_generator.throw(MyException('error message'))
a_generator.throw('error message')unsupported
  1. 最简单的形式下,生成器抛出不带用户自定义错误信息的异常。这种情况下,从 Python 2 到 Python 3 语法上没有变化 。
  2. 如果生成器抛出一个带用户自定义错误信息的异常,你需要将这个错误信息字符串(error string)传递给异常类来以实例化它。
  3. Python 2 还支持抛出只有异常信息的异常。Python 3 不支持这种语法,并且2to3会显示一个警告信息,告诉你需要手动地来修复这处代码。

全局函数xrange()

在 Python 2 里,有两种方法来获得一定范围内的数字:range(),它返回一个列表,还有range(),它返回一个迭代器。在 Python 3 里,range()返回迭代器,xrange()不再存在了。

NotesPython 2Python 3
xrange(10)range(10)
a_list = range(10)a_list = list(range(10))
[i for i in xrange(10)][i for i in range(10)]
for i in range(10):no change
sum(range(10))no change
  1. 在最简单的情况下,2to3会简单地把xrange()转换为range()
  2. 如果你的 Python 2 代码使用range()2to3不知道你是否需要一个列表,或者是否一个迭代器也行。出于谨慎,2to3可能会报错,然后使用list()range()的返回值强制转换为列表类型。
  3. 如果在列表解析里有xrange()函数,就没有必要将其返回值转换为一个列表,因为列表解析对迭代器同样有效。
  4. 类似的,for循环也能作用于迭代器,所以这里也没有改变任何东西。
  5. 函数sum()能作用于迭代器,所以2to3也没有在这里做出修改。就像返回值为视图(view)而不再是列表的字典类方法一样,这同样适用于min()max()sum(),list(),tuple()set()sorted()any()all()

全局函数raw_input()input()

Python 2 有两个全局函数,用来在命令行请求用户输入。第一个叫做input(),它等待用户输入一个 Python 表达式(然后返回结果)。第二个叫做raw_input(),用户输入什么它就返回什么。这让初学者非常困惑,并且这被广泛地看作是 Python 语言的一个“肉赘”(wart)。Python 3 通过重命名raw_input()input(),从而切掉了这个肉赘,所以现在的input()就像每个人最初期待的那样工作。

NotesPython 2Python 3
raw_input()input()
raw_input('prompt')input('prompt')
input()eval(input())
  1. 最简单的形式,raw_input()被替换成input()
  2. 在 Python 2 里,raw_input()函数可以指定一个提示符作为参数。Python 3 里保留了这个功能。
  3. 如果你真的想要请求用户输入一个 Python 表达式,计算结果,可以通过调用input()函数然后把返回值传递给eval()

函数属性func_*

在 Python 2 里,函数的里的代码可以访问到函数本身的特殊属性。在 Python 3 里,为了一致性,这些特殊属性被重新命名了。

NotesPython 2Python 3
a_function.func_namea_function.__name__
a_function.func_doca_function.__doc__
a_function.func_defaultsa_function.__defaults__
a_function.func_dicta_function.__dict__
a_function.func_closurea_function.__closure__
a_function.func_globalsa_function.__globals__
a_function.func_codea_function.__code__
  1. __name__属性(原func_name)包含了函数的名字。
  2. __doc__属性(原funcdoc)包含了你在函数源代码里定义的文档字符串(docstring)
  3. __defaults__属性(原func_defaults)是一个保存参数默认值的元组。
  4. __dict__属性(原func_dict)是一个支持任意函数属性的名字空间。
  5. __closure__属性(原func_closure)是一个由 cell 对象组成的元组,它包含了函数对自由变量(free variable)的绑定。
  6. __globals__属性(原func_globals)是一个对模块全局名字空间的引用,函数本身在这个名字空间里被定义。
  7. __code__属性(原func_code)是一个代码对象,表示编译后的函数体。

I/O 方法xreadlines()

在 Python 2 里,文件对象有一个xreadlines()方法,它返回一个迭代器,一次读取文件的一行。这在for循环中尤其有用。事实上,后来的 Python 2 版本给文件对象本身添加了这样的功能。

在 Python 3 里,xreadlines()方法不再可用了。2to3可以解决简单的情况,但是一些边缘案例则需要人工介入。

NotesPython 2Python 3
for line in a_file.xreadlines():for line in a_file:
for line in a_file.xreadlines(5):no change (broken)
  1. 如果你以前调用没有参数的xreadlines()2to3会把它转换成文件对象本身。在 Python 3 里,这种转换后的代码可以完成前同样的工作:一次读取文件的一行,然后执行for循环的循环体。
  2. 如果你以前使用一个参数(每次读取的行数)调用xreadlines()2to3不能为你完成从 Python 2 到 Python 3 的转换,你的代码会以这样的方式失败:AttributeError: '_io.TextIOWrapper' object has no attribute 'xreadlines'。你可以手工的把xreadlines()改成readlines()以使代码能在 Python 3 下工作。(readline()方法在 Python 3 里返回迭代器,所以它跟 Python 2 里的xreadlines()效率是不相上下的。)

使用元组而非多个参数的lambda函数

在 Python 2 里,你可以定义匿名lambda函数(anonymous lambda function),通过指定作为参数的元组的元素个数,使这个函数实际上能够接收多个参数。事实上,Python 2 的解释器把这个元组“解开”(unpack)成命名参数(named arguments),然后你可以在lambda函数里引用它们(通过名字)。在 Python 3 里,你仍然可以传递一个元组作为lambda函数的参数,但是 Python 解释器不会把它解析成命名参数。你需要通过位置索引(positional index)来引用每个参数。

NotesPython 2Python 3
lambda (x,): x + f(x)lambda x1: x1[0] + f(x1[0])
lambda (x, y): x + f(y)lambda x_y: x_y[0] + f(x_y[1])
lambda (x, (y, z)): x + y + zlambda x_y_z: x_y_z[0] + x_y_z[1][0] + x_y_z[1][1]
lambda x, y, z: x + y + zunchanged
  1. 如果你已经定义了一个lambda函数,它使用包含一个元素的元组作为参数,在 Python 3 里,它会被转换成一个包含到x1[0]的引用的lambda函数。x12to3脚本基于原来元组里的命名参数自动生成的。
  2. 使用含有两个元素的元组(x, y)作为参数的lambda函数被转换为x_y,它有两个位置参数,即x_y[0]x_y[1]
  3. 2to3脚本甚至可以处理使用嵌套命名参数的元组作为参数的lambda函数。产生的结果代码有点难以阅读,但是它在 Python 3 下跟原来的代码在 Python 2 下的效果是一样的。
  4. 你可以定义使用多个参数的lambda函数。如果没有括号包围在参数周围,Python 2 会把它当作一个包含多个参数的lambda函数;在这个lambda函数体里,你通过名字引用这些参数,就像在其他类型的函数里所做的一样。这种语法在 Python 3 里仍然有效。

特殊的方法属性

在 Python 2 里,类方法可以访问到定义他们的类对象(class object),也能访问方法对象(method object)本身。im_self是类的实例对象;im_func是函数对象,im_class是类本身。在 Python 3 里,这些属性被重新命名,以遵循其他属性的命名约定。

NotesPython 2Python 3
aClassInstance.aClassMethod.im_funcaClassInstance.aClassMethod.__func__
aClassInstance.aClassMethod.im_selfaClassInstance.aClassMethod.__self__
aClassInstance.aClassMethod.im_classaClassInstance.aClassMethod.__self__.__class__

__nonzero__特殊方法

在 Python 2 里,你可以创建自己的类,并使他们能够在布尔上下文(boolean context)中使用。举例来说,你可以实例化这个类,并把这个实例对象用在一个if语句中。为了实现这个目的,你定义一个特别的__nonzero__()方法,它的返回值为True或者False,当实例对象处在布尔上下文中的时候这个方法就会被调用 。在 Python 3 里,你仍然可以完成同样的功能,但是这个特殊方法的名字变成了__bool__()

NotesPython 2Python 3
[1][2]
[3]no change

[1]:

class A:
    def __nonzero__(self):
        pass 

[2]:

class A:
    def __bool__(self):
        pass 

[3]:

class A:
    def __nonzero__(self, x, y):
        pass 
  1. 当在布尔上下文使用一个类对象时,Python 3 会调用__bool__(),而非__nonzero__()
  2. 然而,如果你有定义了一个使用两个参数的__nonzero__()方法,2to3脚本会假设你定义的这个方法有其他用处,因此不会对代码做修改。

八进制类型

在 Python 2 和 Python 3 之间,定义八进制(octal)数的语法有轻微的改变。

NotesPython 2Python 3
x = 0755x = 0o755

sys.maxint

由于长整型和整型被整合在一起了,sys.maxint常量不再精确。但是因为这个值对于检测特定平台的能力还是有用处的,所以它被 Python 3 保留,并且重命名为sys.maxsize

NotesPython 2Python 3
from sys import maxintfrom sys import maxsize
a_function(sys.maxint)a_function(sys.maxsize)
  1. maxint变成了maxsize
  2. 所有的sys.maxint都变成了sys.maxsize

全局函数callable()

在 Python 2 里,你可以使用全局函数callable()来检查一个对象是否可调用(callable,比如函数)。在 Python 3 里,这个全局函数被取消了。为了检查一个对象是否可调用,可以检查特殊方法__call__()的存在性。

NotesPython 2Python 3
callable(anything)hasattr(anything, '__call__')

全局函数zip()

在 Python 2 里,全局函数zip()可以使用任意多个序列作为参数,它返回一个由元组构成的列表。第一个元组包含了每个序列的第一个元素;第二个元组包含了每个序列的第二个元素;依次递推下去。在 Python 3 里,zip()返回一个迭代器,而非列表。

NotesPython 2Python 3
zip(a, b, c)list(zip(a, b, c))
d.join(zip(a, b, c))no change
  1. 最简单的形式,你可以通过调用list()函数包装zip()的返回值来恢复zip()函数以前的功能,list()函数会遍历这个zip()函数返回的迭代器,然后返回结果的列表表示。
  2. 在已经会遍历序列所有元素的上下文环境里(比如这里对join()方法的调用),zip()返回的迭代器能够正常工作。2to3脚本会检测到这些情况,不会对你的代码作出改变。

StandardError异常

在 Python 2 里,StandardError是除了StopIterationGeneratorExitKeyboardInterruptSystemExit之外所有其他内置异常的基类。在 Python 3 里,StandardError已经被取消了;使用Exception替代。

NotesPython 2Python 3
x = StandardError()x = Exception()
x = StandardError(a, b, c)x = Exception(a, b, c)

types模块中的常量

types模块里各种各样的常量能帮助你决定一个对象的类型。在 Python 2 里,它包含了代表所有基本数据类型的常量,如dictint。在 Python 3 里,这些常量被已经取消了。只需要使用基础类型的名字来替代。

NotesPython 2Python 3
types.UnicodeTypestr
types.StringTypebytes
types.DictTypedict
types.IntTypeint
types.LongTypeint
types.ListTypelist
types.NoneTypetype(None)
types.BooleanTypebool
types.BufferTypememoryview
types.ClassTypetype
types.ComplexTypecomplex
types.EllipsisTypetype(Ellipsis)
types.FloatTypefloat
types.ObjectTypeobject
types.NotImplementedTypetype(NotImplemented)
types.SliceTypeslice
types.TupleTypetuple
types.TypeTypetype
types.XRangeTyperange

types.StringType被映射为bytes,而非str,因为 Python 2 里的“string”(非 Unicode 编码的字符串,即普通字符串)事实上只是一些使用某种字符编码的字节序列(a sequence of bytes)。

全局函数isinstance()

isinstance()函数检查一个对象是否是一个特定类(class)或者类型(type)的实例。在 Python 2 里,你可以传递一个由类型(types)构成的元组给isinstance(),如果该对象是元组里的任意一种类型,函数返回True。在 Python 3 里,你依然可以这样做,但是不推荐使用把一种类型作为参数传递两次。

NotesPython 2Python 3
isinstance(x, (int, float, int))isinstance(x, (int, float))

basestring数据类型

Python 2 有两种字符串类型:Unicode 编码的字符串和非 Unicode 编码的字符串。但是其实还有另外 一种类型,即basestring。它是一个抽象数据类型,是strunicode类型的超类(superclass)。它不能被直接调用或者实例化,但是你可以把它作为isinstance()的参数来检测一个对象是否是一个 Unicode 字符串或者非 Unicode 字符串。在 Python 3 里,只有一种字符串类型,所以basestring就没有必要再存在了。

NotesPython 2Python 3
isinstance(x, basestring)isinstance(x, str)

itertools模块

Python 2.3 引入了itertools模块,它定义了全局函数zip()map()filter()的变体(variant),这些变体的返回类型为迭代器,而非列表。在 Python 3 里,由于这些全局函数的返回类型本来就是迭代器,所以这些itertools里的这些变体函数就被取消了。(在itertools模块里仍然还有许多其他的有用的函数,而不仅仅是以上列出的这些。)

NotesPython 2Python 3
itertools.izip(a, b)zip(a, b)
itertools.imap(a, b)map(a, b)
itertools.ifilter(a, b)filter(a, b)
from itertools import imap, izip, foofrom itertools import foo
  1. 使用全局的zip()函数,而非itertools.izip()
  2. 使用map()而非itertools.imap()
  3. itertools.ifilter()变成了filter()
  4. itertools模块在 Python 3 里仍然存在,它只是不再包含那些已经转移到全局名字空间的函数。2to3脚本能够足够智能地去移除那些不再有用的导入语句,同时保持其他的导入语句的完整性。

sys.exc_type, sys.exc_value, sys.exc_traceback

处理异常的时候,在sys模块里有三个你可以访问的变量:sys.exc_type,sys.exc_value,sys.exc_traceback。(实际上这些在 Python 1 的时代就有。)从 Python 1.5 开始,由于新出的sys.exc_info,不再推荐使用这三个变量了,这是一个包含所有以上三个元素的元组。在 Python 3 里,这三个变量终于不再存在了;这意味着,你必须使用sys.exc_info。``

NotesPython 2Python 3
sys.exc_typesys.exc_info()[0]
sys.exc_valuesys.exc_info()[1]
sys.exc_tracebacksys.exc_info()[2]

对元组的列表解析

在 Python 2 里,如果你需要编写一个遍历元组的列表解析,你不需要在元组值的周围加上括号。在 Python 3 里,这些括号是必需的。

NotesPython 2Python 3
[i for i in 1, 2][i for i in (1, 2)]

os.getcwdu()函数

Python 2 有一个叫做os.getcwd()的函数,它将当前的工作目录作为一个(非 Unicode 编码的)字符串返回。由于现代的文件系统能够处理能何字符编码的目录名,Python 2.3 引入了os.getcwdu()函数。os.getcwdu()函数把当前工作目录用 Unicode 编码的字符串返回。在 Python 3 里,由于只有一种字符串类型(Unicode 类型的),所以你只需要os.getcwd()就可以了。

NotesPython 2Python 3
os.getcwdu()os.getcwd()

元类(metaclass)

在 Python 2 里,你可以通过在类的声明中定义metaclass参数,或者定义一个特殊的类级别的(class-level)__metaclass__属性,来创建元类。在 Python 3 里,__metaclass__属性已经被取消了。

NotesPython 2Python 3
[1]unchanged
[2][3]
[4][5]

[1]:

class C(metaclass=PapayaMeta):
    pass 

[2]:

class Whip:
    __metaclass__ = PapayaMeta 

[3]:

class Whip(metaclass=PapayaMeta):
    pass 

[4]:

class C(Whipper, Beater):
    __metaclass__ = PapayaMeta 

[5]:

class C(Whipper, Beater, metaclass=PapayaMeta):
    pass 
  1. 在声明类的时候声明metaclass参数,这在 Python 2 和 Python 3 里都有效,它们是一样的。
  2. 在类的定义里声明__metaclass__属性在 Python 2 里有效,但是在 Python 3 里不再有效。
  3. 2to3能够构建一个有效的类声明,即使这个类继承自多个父类。

关于代码风格

以下所列的“修补”(fixes)实质上并不算真正的修补。意思就是,他们只是代码的风格上的事情,而不涉及到代码的本质。但是 Python 的开发者们在使得代码风格尽可能一致方面非常有兴趣(have a vested interest)。为此,有一个专门o 描述 Python 代码风格的官方指导手册 — 细致到能使人痛苦 — 都是一些你不太可能关心的在各种各样的细节上的挑剔。鉴于2to3为转换代码提供了一个这么好的条件,脚本的作者们添加了一些可选的特性以使你的代码更具可读性。

set()字面值(literal)(显式的)

在 Python 2 城,定义一个字面值集合(literal set)的唯一方法就是调用set(a_sequence)。在 Python 3 里这仍然有效,但是使用新的标注记号(literal notation):大括号({})是一种更清晰的方法。这种方法除了空集以外都有效,因为字典也用大括号标记,所以{}表示一个空的字典,而不是一个空集。

2to3脚本默认不会修复set()字面值。为了开启这个功能,在命令行调用2to3的时候指定-f set_literal参数。

NotesBeforeAfter
set([1, 2, 3]){1, 2, 3}
set((1, 2, 3)){1, 2, 3}
set([i for i in a_sequence]){i for i in a_sequence}

全局函数buffer()(显式的)

用 C 实现的 Python 对象可以导出一个“缓冲区接口”(buffer interface),它允许其他的 Python 代码直接读写一块内存。(这听起来很强大,它也同样可怕。)在 Python 3 里,buffer()被重新命名为memoryview()。(实际的修改更加复杂,但是你几乎可以忽略掉这些不同之处。)

2to3脚本默认不会修复buffer()函数。为了开启这个功能,在命令行调用2to3的时候指定-f buffer参数。

NotesBeforeAfter
x = buffer(y)x = memoryview(y)

逗号周围的空格(显式的)

尽管 Python 对用于缩进和凸出(indenting and outdenting)的空格要求很严格,但是对于空格在其他方面的使用 Python 还是很自由的。在列表,元组,集合和字典里,空格可以出现在逗号的前面或者后面,这不会有什么坏影响。但是,Python 代码风格指导手册上指出,逗号前不能有空格,逗号后应该包含一个空格。尽管这纯粹只是一个美观上的考量(代码仍然可以正常工作,在 Python 2 和 Python 3 里都可以),但是2to3脚本可以依据手册上的标准为你完成这个修复。

2to3脚本默认不会修复逗号周围的空格。为了开启这个功能,在命令行调用2to3的时候指定-f wscomma参数。

NotesBeforeAfter
a ,ba, b
{a :b}{a: b}

惯例(Common idioms)(显式的)

在 Python 社区里建立起来了许多惯例。有一些比如while 1: loop,它可以追溯到 Python 1。(Python 直到 Python 2.3 才有真正意义上的布尔类型,所以开发者以前使用10替代。)当代的 Python 程序员应该锻炼他们的大脑以使用这些惯例的现代版。

2to3脚本默认不会为这些惯例做修复。为了开启这个功能,在命令行调用2to3的时候指定-f idioms参数。

NotesBeforeAfter
[1][2]
type(x) == Tisinstance(x, T)
type(x) is Tisinstance(x, T)
[3][4]

[1]:

while 1:
    do_stuff() 

[2]:

while True:
    do_stuff() 

[3]:

a_list = list(a_sequence)
a_list.sort()
do_stuff(a_list) 

[4]:

a_list = sorted(a_sequence)
do_stuff(a_list) 

Chapter B 特殊方法名称

" My specialty is being right when other people are wrong. " — George Bernard Shaw

深入

在本书其它几处,我们已经见识过一些特殊方法——即在使用某些语法时 Python 所调用的“神奇”方法。使用特殊方法,类用起来如同序列、字典、函数、迭代器,或甚至像个数字!本附录为我们已经见过特殊方法提供了参考,并对一些更加深奥的特殊方法进行了简要介绍。

基础知识

如果曾阅读 《类的简介》一章,你可能已经见识过了最常见的特殊方法: __init__() 方法。盖章结束时,我写的类多数需要进行一些初始化工作。还有一些其它的基础特殊方法对调试自定义类也特别有用。

序号目的所编写代码Python 实际调用
初始化一个实例x = MyClass()x.__init__()
字符串的“官方”表现形式repr(x)x.__repr__()
字符串的“非正式”值str(x)x.__str__()
字节数组的“非正式”值bytes(x)x.__bytes__()
格式化字符串的值format(x,format_spec)x.__format__(format_spec)
  1. __init__() 方法的调用发生在实例被创建 之后 。如果要控制实际创建进程,请使用 __new__() 方法。
  2. 按照约定, __repr__() 方法所返回的字符串为合法的 Python 表达式。
  3. 在调用 print(x) 的同时也调用了 __str__() 方法。
  4. 由于 bytes 类型的引入而从 Python 3 开始出现
  5. 按照约定,format_spec 应当遵循 迷你语言格式规范【Format Specification Mini-Language】。Python 标准类库中的 decimal.py 提供了自己的 __format__() 方法。

行为方式与迭代器类似的类

在 《迭代器》一章中,我们已经学习了如何使用 __iter__()__next__() 方法从零开始创建迭代器。

序号目的所编写代码Python 实际调用
遍历某个序列iter(seq)seq.__iter__()
从迭代器中获取下一个值next(seq)seq.__next__()
按逆序创建一个迭代器reversed(seq)seq.__reversed__()
  1. 无论何时创建迭代器都将调用 __iter__() 方法。这是用初始值对迭代器进行初始化的绝佳之处。
  2. 无论何时从迭代器中获取下一个值都将调用 __next__() 方法。
  3. __reversed__() 方法并不常用。它以一个现有序列为参数,并将该序列中所有元素从尾到头以逆序排列生成一个新的迭代器。

正如我们在 《迭代器》一章中看到的,for 循环也可用作迭代器。在下面的循环中:

for x in seq:
    print(x) 

Python 3 将会调用 seq.__iter__() 以创建一个迭代器,然后对迭代器调用 __next__() 方法以获取 x 的每个值。当 __next__() 方法引发 StopIteration 例外时, for 循环正常结束。

计算属性

序号目的所编写代码Python 实际调用
获取一个计算属性(无条件的)x.my_propertyx.__getattribute__('my_property')
------------
获取一个计算属性(后备)x.my_propertyx.__getattr__('my_property')
------------
设置某属性x.my_property = valuex.__setattr__('my_property',value)
------------
删除某属性del x.my_propertyx.__delattr__('my_property')
------------
列出所有属性和方法dir(x)x.__dir__()
------------
  1. 如果某个类定义了 __getattribute__() 方法,在 每次引用属性或方法名称时 Python 都调用它(特殊方法名称除外,因为那样将会导致讨厌的无限循环)。
  2. 如果某个类定义了 __getattr__() 方法,Python 将只在正常的位置查询属性时才会调用它。如果实例 x 定义了属性 colorx.color不会 调用 x.__getattr__('color');而只会返回 x.color 已定义好的值。
  3. 无论何时给属性赋值,都会调用 __setattr__() 方法。
  4. 无论何时删除一个属性,都将调用 __delattr__() 方法。
  5. 如果定义了 __getattr__()__getattribute__() 方法, __dir__() 方法将非常有用。通常,调用 dir(x) 将只显示正常的属性和方法。如果 __getattr()__ 方法动态处理 color 属性, dir(x) 将不会将 color 列为可用属性。可通过覆盖 __dir__() 方法允许将 color 列为可用属性,对于想使用你的类但却不想深入其内部的人来说,该方法非常有益。

__getattr__()__getattribute__() 方法的区别非常细微,但非常重要。可以用两个例子来解释一下:

class Dynamo:
    def __getattr__(self, key):

            return 'PapayaWhip'
        else:

>>> dyn = Dynamo()

'PapayaWhip'
>>> dyn.color = 'LemonChiffon'

'LemonChiffon' 
  1. 属性名称以字符串的形式传入 __getattr()__ 方法。如果名称为 'color',该方法返回一个值。(在此情况下,它只是一个硬编码的字符串,但可以正常地进行某些计算并返回结果。)
  2. 如果属性名称未知, __getattr()__ 方法必须引发一个 AttributeError 例外,否则在访问未定义属性时,代码将只会默默地失败。(从技术角度而言,如果方法不引发例外或显式地返回一个值,它将返回 None ——Python 的空值。这意味着 所有 未显式定义的属性将为 None,几乎可以肯定这不是你想看到的。)
  3. dyn 实例没有名为 color 的属性,因此在提供计算值时将调用 __getattr__()
  4. 在显式地设置 dyn.color 之后,将不再为提供 dyn.color 的值而调用 __getattr__() 方法,因为 dyn.color 已在该实例中定义。

另一方面,__getattribute__() 方法是绝对的、无条件的。

class SuperDynamo:
    def __getattribute__(self, key):
        if key == 'color':
            return 'PapayaWhip'
        else:
            raise AttributeError

>>> dyn = SuperDynamo()

'PapayaWhip'
>>> dyn.color = 'LemonChiffon'

'PapayaWhip' 
  1. 在获取 dyn.color 的值时将调用 __getattribute__() 方法。
  2. 即便已经显式地设置 dyn.color,在获取 dyn.color 的值时, 仍将调用 __getattribute__() 方法。如果存在 __getattribute__() 方法,将在每次查找属性和方法时 无条件地调用 它,哪怕在创建实例之后已经显式地设置了属性。

☞ 如果定义了类的 __getattribute__() 方法,你可能还想定义一个 __setattr__() 方法,并在两者之间进行协同,以跟踪属性的值。否则,在创建实例之后所设置的值将会消失在黑洞中。

必须特别小心 __getattribute__() 方法,因为 Python 在查找类的方法名称时也将对其进行调用。

class Rastan:
    def __getattribute__(self, key):

    def swim(self):
        pass

>>> hero = Rastan()

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 3, in __getattribute__
AttributeError 
  1. 该类定义了一个总是引发 AttributeError 例外的 __getattribute__() 方法。没有属性或方法的查询会成功。
  2. 调用 hero.swim() 时,Python 将在 Rastan 类中查找 swim() 方法。该查找将执行整个 __getattribute__() 方法,因为所有的属性和方法查找都通过 __getattribute__() 方法。在此例中, __getattribute__() 方法引发 AttributeError 例外,因此该方法查找过程将会失败,而方法调用也将失败。

行为方式与函数类似的类

可以让类的实例变得可调用——就像函数可以调用一样——通过定义 __call__() 方法。

序号目的所编写代码Python 实际调用
像调用函数一样“调用”一个实例my_instance()my_instance.__call__()

zipfile 模块 通过该方式定义了一个可以使用给定密码解密 经加密 zip 文件的类。该 zip 解密 算法需要在解密的过程中保存状态。通过将解密器定义为类,使我们得以在 decryptor 类的单个实例中对该状态进行维护。状态在 __init__() 方法中进行初始化,如果文件 经加密 则进行更新。但由于该类像函数一样“可调用”,因此可以将实例作为 map() 函数的第一个参数传入,代码如下:

# excerpt from zipfile.py
class _ZipDecrypter:
.
.
.
    def __init__(self, pwd):

        self.key1 = 591751049
        self.key2 = 878082192
        for p in pwd:
            self._UpdateKeys(p)

        assert isinstance(c, int)
        k = self.key2 | 2
        c = c ^ (((k * (k¹)) >> 8) & 255)
        self._UpdateKeys(c)
        return c
.
.
.

bytes = zef_file.read(12) 
  1. _ZipDecryptor 类维护了以三个旋转密钥形式出现的状态,该状态稍后将在 _UpdateKeys() 方法中更新(此处未展示)。
  2. 该类定义了一个 __call__() 方法,使得该类可像函数一样调用。在此例中,__call__() 对 zip 文件的单个字节进行解密,然后基于经解密的字节对旋转密码进行更新。
  3. zd_ZipDecryptor 类的一个实例。变量 pwd 被传入 __init__() 方法,并在其中被存储和用于首次旋转密码更新。
  4. 给出 zip 文件的头 12 个字节,将这些字节映射给 zd 进行解密,实际上这将导致调用 __call__() 方法 12 次,也就是 更新内部状态并返回结果字节 12 次。

行为方式与序列类似的类

如果类作为一系列值的容器出现——也就是说如果对某个类来说,是否“包含”某值是件有意义的事情——那么它也许应该定义下面的特殊方法已,让它的行为方式与序列类似。

序号目的所编写代码Python 实际调用
序列的长度len(seq)seq.__len__()
了解某序列是否包含特定的值x in seqseq.__contains__(x)

cgi 模块 在其 FieldStorage 类中使用了这些方法,该类用于表示提交给动态网页的所有表单字段或查询参数。

# A script which responds to http://example.com/search?q=cgi
import cgi
fs = cgi.FieldStorage()

  do_search()

# An excerpt from cgi.py that explains how that works
class FieldStorage:
.
.
.

        if self.list is None:
            raise TypeError('not indexable') 
  1. 一旦创建了 cgi.FieldStorage 类的实例,就可以使用 “in” 运算符来检查查询字符串中是否包含了某个特定参数。
  2. __contains__() 方法是令该魔法生效的主角。
  3. 如果代码为 if 'q' in fs,Python 将在 fs 对象中查找 __contains__() 方法,而该方法在 cgi.py 中已经定义。'q' 的值被当作 key 参数传入 __contains__() 方法。
  4. 同样的 FieldStorage 类还支持返回其长度,因此可以编写代码 len(fs) 而其将调用 FieldStorage__len__() 方法,并返回其识别的查询参数个数。
  5. self.keys() 方法检查 self.list is None 是否为真值,因此 __len__ 方法无需重复该错误检查。

行为方式与字典类似的类

在前一节的基础上稍作拓展,就不仅可以对 “in” 运算符和 len() 函数进行响应,还可像全功能字典一样根据键来返回值。

序号目的所编写代码Python 实际调用
通过键来获取值x[key]x.__getitem__(key)
通过键来设置值x[key] = valuex.__setitem__(key,value)
删除一个键值对del x[key]x.__delitem__(key)
为缺失键提供默认值x[nonexistent_key]x.__missing__(nonexistent_key)

cgi 模块FieldStorage 类 同样定义了这些特殊方法,也就是说可以像下面这样编码:

# A script which responds to http://example.com/search?q=cgi
import cgi
fs = cgi.FieldStorage()
if 'q' in fs:

# An excerpt from cgi.py that shows how it works
class FieldStorage:
.
.
.

        if self.list is None:
            raise TypeError('not indexable')
        found = []
        for item in self.list:
            if item.name == key: found.append(item)
        if not found:
            raise KeyError(key)
        if len(found) == 1:
            return found[0]
        else:
            return found 
  1. fs 对象是 cgi.FieldStorage 类的一个实例,但仍然可以像 fs['q'] 这样估算表达式。
  2. fs['q']key 参数设置为 'q' 来调用 __getitem__() 方法。然后它将在其内部维护的查询参数列表 (self.list) 中查找一个 .name 与给定键相符的字典项。

行为方式与数值类似的类

使用适当的特殊方法,可以将类的行为方式定义为与数字相仿。也就是说,可以进行相加、相减,并进行其它数学运算。这就是 分数 的实现方式—— Fraction 类实现了这些特殊方法,然后就可以进行下列运算了:

>>> from fractions import Fraction
>>> x = Fraction(1, 3)
>>> x / 3
Fraction(1, 9) 

以下是实现“类数字”类的完整特殊方法清单:

序号目的所编写代码Python 实际调用
加法x + yx.__add__(y)
减法x - yx.__sub__(y)
乘法x * yx.__mul__(y)
除法x / yx.__truediv__(y)
地板除x // yx.__floordiv__(y)
取模(取余)x % yx.__mod__(y)
地板除 & 取模divmod(x, y)x.__divmod__(y)
乘幂x ** yx.__pow__(y)
左位移x &lt;&lt; yx.__lshift__(y)
右位移x &gt;&gt; yx.__rshift__(y)
按位 andx & yx.__and__(y)
按位 xorx ^ yx.__xor__(y)
按位 orx &#124; yx.__or__(y)

如果 x 是某个实现了所有这些方法的类的实例,那么万事大吉。但如果未实现其中之一呢?或者更糟,如果实现了,但却无法处理某几类参数会怎么样?例如:

>>> from fractions import Fraction
>>> x = Fraction(1, 3)
>>> 1 / x
Fraction(3, 1) 

这并 不是 传入一个 分数 并将其除以一个整数(如前例那样)的情况。前例中的情况非常直观: x / 3 调用 x.__truediv__(3),而Fraction__truediv__() 方法处理所有的数学运算。但整数并不“知道”如何对分数进行数学计算。因此本例该如何运作呢?

反映操作 相关的还有第二部分算数特殊方法。给定一个二元算术运算 (例如: x / y),有两种方法来实现它:

  1. 告诉 x 将自己除以 y,或者
  2. 告诉 y 去除 x

之前提到的特殊方法集合采用了第一种方式:对于给定 x / y,它们为 x 提供了一种途径来表述“我知道如何将自己除以 y。”下面的特殊方法集合采用了第二种方法:它们向 y 提供了一种途径来表述“我知道如何成为分母,并用自己去除 x。”

序号目的所编写代码Python 实际调用
加法x + yy.__radd__(x)
减法x - yy.__rsub__(x)
乘法x * yy.__rmul__(x)
除法x / yy.__rtruediv__(x)
地板除x // yy.__rfloordiv__(x)
取模(取余)x % yy.__rmod__(x)
地板除 & 取模divmod(x, y)y.__rdivmod__(x)
乘幂x ** yy.__rpow__(x)
左位移x &lt;&lt; yy.__rlshift__(x)
右位移x &gt;&gt; yy.__rrshift__(x)
按位 andx & yy.__rand__(x)
按位 xorx ^ yy.__rxor__(x)
按位 orx &#124; yy.__ror__(x)

但是等一下!还有更多特殊方法!如果在进行“原地”操作,如: x /= 3,还可定义更多的特殊方法。

序号目的所编写代码Python 实际调用
原地加法x += yx.__iadd__(y)
原地减法x -= yx.__isub__(y)
原地乘法x *= yx.__imul__(y)
原地除法x /= yx.__itruediv__(y)
原地地板除法x //= yx.__ifloordiv__(y)
原地取模x %= yx.__imod__(y)
原地乘幂x **= yx.__ipow__(y)
原地左位移x &lt;&lt;= yx.__ilshift__(y)
原地右位移x &gt;&gt;= yx.__irshift__(y)
原地按位 andx &= yx.__iand__(y)
原地按位 xorx ^= yx.__ixor__(y)
原地按位 orx &#124;= yx.__ior__(y)

注意:多数情况下,并不需要原地操作方法。如果未对特定运算定义“就地”方法,Python 将会试着使用(普通)方法。例如,为执行表达式 x /= y,Python 将会:

  1. 试着调用 x.__itruediv__(y)。如果该方法已经定义,并返回了 NotImplemented 之外的值,那已经大功告成了。
  2. 试图调用 x.__truediv__(y)。如果该方法已定义并返回一个 NotImplemented 之外的值, x 的旧值将被丢弃,并将所返回的值替代它,就像是进行了 x = x / y 运算。
  3. 试图调用 y.__rtruediv__(x)。如果该方法已定义并返回了一个 NotImplemented 之外的值,x 的旧值将被丢弃,并用所返回值进行替换。

因此如果想对原地运算进行优化,仅需像 __itruediv__() 方法一样定义“原地”方法。否则,基本上 Python 将会重新生成原地运算公式,以使用常规的运算及变量赋值。

还有一些“一元”数学运算,可以对“类-数字”对象自己执行。

序号目的所编写代码Python 实际调用
负数-xx.__neg__()
正数+xx.__pos__()
绝对值abs(x)x.__abs__()
取反~xx.__invert__()
复数complex(x)x.__complex__()
整数转换int(x)x.__int__()
浮点数float(x)x.__float__()
四舍五入至最近的整数round(x)x.__round__()
四舍五入至最近的 n 位小数round(x, n)x.__round__(n)
&gt;= x 的最小整数math.ceil(x)x.__ceil__()
&lt;= x的最大整数math.floor(x)x.__floor__()
x 朝向 0 取整math.trunc(x)x.__trunc__()
PEP 357作为列表索引的数字a_list[x]a_list[x.__index__()]

可比较的类

我将此内容从前一节中拿出来使其单独成节,是因为“比较”操作并不局限于数字。许多数据类型都可以进行比较——字符串、列表,甚至字典。如果要创建自己的类,且对象之间的比较有意义,可以使用下面的特殊方法来实现比较。

序号目的所编写代码Python 实际调用
相等x == yx.__eq__(y)
不相等x != yx.__ne__(y)
小于x &lt; yx.__lt__(y)
小于或等于x &lt;= yx.__le__(y)
大于x &gt; yx.__gt__(y)
大于或等于x &gt;= yx.__ge__(y)
布尔上上下文环境中的真值if x:x.__bool__()

☞如果定义了 __lt__() 方法但没有定义 __gt__() 方法,Python 将通过经交换的算子调用 __lt__() 方法。然而,Python 并不会组合方法。例如,如果定义了 __lt__() 方法和 __eq()__ 方法,并试图测试是否 x &lt;= y,Python 不会按顺序调用 __lt__()__eq()__ 。它将只调用 __le__() 方法。

可序列化的类

Python 支持 任意对象的序列化和反序列化。(多数 Python 参考资料称该过程为 “pickling” 和 “unpickling”)。该技术对与将状态保存为文件并在稍后恢复它非常有意义。所有的 内置数据类型 均已支持 pickling 。如果创建了自定义类,且希望它能够 pickle,阅读 pickle 协议 了解下列特殊方法何时以及如何被调用。

序号目的所编写代码Python 实际调用
自定义对象的复制copy.copy(x)x.__copy__()
自定义对象的深度复制copy.deepcopy(x)x.__deepcopy__()
在 pickling 之前获取对象的状态pickle.dump(x,file)x.__getstate__()
序列化某对象pickle.dump(x,file)x.__reduce__()
序列化某对象(新 pickling 协议)pickle.dump(x,file,protocol_version)x.__reduce_ex__(protocol_version)
*控制 unpickling 过程中对象的创建方式x = pickle.load(file)x.__getnewargs__()
*在 unpickling 之后还原对象的状态x = pickle.load(file)x.__setstate__()
  • 要重建序列化对象,Python 需要创建一个和被序列化的对象看起来一样的新对象,然后设置新对象的所有属性。__getnewargs__() 方法控制新对象的创建过程,而 __setstate__() 方法控制属性值的还原方式。

可在 with 语块中使用的类

with 语块定义了 运行时刻上下文环境;在执行 with 语句时将“进入”该上下文环境,而执行该语块中的最后一条语句将“退出”该上下文环境。

序号目的所编写代码Python 实际调用
在进入 with 语块时进行一些特别操作with x:x.__enter__()
在退出 with 语块时进行一些特别操作with x:x.__exit__()

以下是 withfile`` 习惯用法 的运作方式:

# excerpt from io.py:
def _checkClosed(self, msg=None):
    '''Internal: raise an ValueError if file is closed
    '''
    if self.closed:
        raise ValueError('I/O operation on closed file.'
                         if msg is None else msg)

def __enter__(self):
    '''Context management protocol.  Returns self.'''

def __exit__(self, *args):
    '''Context management protocol.  Calls close()''' 
  1. 该文件对象同时定义了一个 __enter__() 和一个 __exit__() 方法。该 __enter__() 方法检查文件是否处于打开状态;如果没有, _checkClosed() 方法引发一个例外。
  2. __enter__() 方法将始终返回 self —— 这是 with 语块将用于调用属性和方法的对象
  3. with 语块结束后,文件对象将自动关闭。怎么做到的?在 __exit__() 方法中调用了 self.close() .

☞该 __exit__() 方法将总是被调用,哪怕是在 with 语块中引发了例外。实际上,如果引发了例外,该例外信息将会被传递给 __exit__() 方法。查阅 With 状态上下文环境管理器 了解更多细节。

要了解关于上下文管理器的更多内容,请查阅 《自动关闭文件》 和 《重定向标准输出》。

真正神奇的东西

如果知道自己在干什么,你几乎可以完全控制类是如何比较的、属性如何定义,以及类的子类是何种类型。

序号目的所编写代码Python 实际调用
类构造器x = MyClass()x.__new__()
*类析构器del xx.__del__()
只定义特定集合的某些属性x.__slots__()
自定义散列值hash(x)x.__hash__()
获取某个属性的值x.colortype(x).__dict__['color'].__get__(x, type(x))
设置某个属性的值x.color = 'PapayaWhip'type(x).__dict__['color'].__set__(x, 'PapayaWhip')
删除某个属性del x.colortype(x).__dict__['color'].__del__(x)
控制某个对象是否是该对象的实例 your classisinstance(x, MyClass)MyClass.__instancecheck__(x)
控制某个类是否是该类的子类issubclass(C, MyClass)MyClass.__subclasscheck__(C)
控制某个类是否是该抽象基类的子类issubclass(C, MyABC)MyABC.__subclasshook__(C)

深入阅读

本附录中提到的模块:

其它启发式阅读:

Chapter C 接下来阅读什么?

Chapter C 接下来阅读什么?

" Go forth on your path, as it exists only through your walking. " — St. Augustine of Hippo (attributed)

要阅读的对象

鉴于一些主题有免费的教程,因此我决定不在本书中加以阐述。

修饰器:

属性:

描述符:

线程 & 多进程:

元类

此外,Doug Hellman 之 本周 Python 模块 是对 Python 标准类库模块的极好指南

到哪里找与 Python 3-兼容的代码

由于 Python 3 相对较新,其非常缺乏兼容类库。以下地方可用于查找在 Python 3 之下能够正常运作的代码: